meshagent-computers 0.45.4__tar.gz → 0.45.6__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 (34) hide show
  1. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/CHANGELOG.md +9 -0
  2. {meshagent_computers-0.45.4/meshagent_computers.egg-info → meshagent_computers-0.45.6}/PKG-INFO +4 -4
  3. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/agent.py +2 -2
  4. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/agent_test.py +379 -1
  5. meshagent_computers-0.45.6/meshagent/computers/browserbase_test.py +133 -0
  6. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/container_playwright_test.py +2 -2
  7. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/docker.py +2 -2
  8. meshagent_computers-0.45.6/meshagent/computers/docker_test.py +167 -0
  9. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/stagehand_test.py +141 -2
  10. meshagent_computers-0.45.6/meshagent/computers/utils_test.py +102 -0
  11. meshagent_computers-0.45.6/meshagent/computers/version.py +1 -0
  12. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6/meshagent_computers.egg-info}/PKG-INFO +4 -4
  13. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/SOURCES.txt +1 -0
  14. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/requires.txt +3 -3
  15. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/pyproject.toml +3 -3
  16. meshagent_computers-0.45.4/meshagent/computers/docker_test.py +0 -22
  17. meshagent_computers-0.45.4/meshagent/computers/utils_test.py +0 -47
  18. meshagent_computers-0.45.4/meshagent/computers/version.py +0 -1
  19. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/LICENSE +0 -0
  20. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/MANIFEST.in +0 -0
  21. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/README.md +0 -0
  22. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/__init__.py +0 -0
  23. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/base_playwright.py +0 -0
  24. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/browserbase.py +0 -0
  25. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/computer.py +0 -0
  26. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/container_playwright.py +0 -0
  27. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/local_playwright.py +0 -0
  28. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/operator.py +0 -0
  29. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/operator_test.py +0 -0
  30. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/stagehand.py +0 -0
  31. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/utils.py +0 -0
  32. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/dependency_links.txt +0 -0
  33. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/top_level.txt +0 -0
  34. {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/setup.cfg +0 -0
@@ -1,3 +1,12 @@
1
+ ## [0.45.6]
2
+ - Added strict dataset prompt rendering with escaped-brace support and simple field placeholders, replacing generic `str.format` expansion.
3
+ - Tightened OpenAI adapter handling so code-interpreter results must be structured objects with required string fields, and invalid payloads now error out earlier.
4
+ - Aligned computer/browser tooling with Python behavior for Browserbase setup, Docker geometry handling, and optional rule initialization.
5
+ - Synchronized Python package dependencies and release metadata across the API, agents, CLI, OpenAI, tools, computers, codex, and related companion packages.
6
+
7
+ ## [0.45.5]
8
+ - Shell command analysis now handles mixed absolute and dynamic write targets more safely, keeping generated file previews and path grouping accurate.
9
+
1
10
  ## [0.45.4]
2
11
  - Stability
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-computers
3
- Version: 0.45.4
3
+ Version: 0.45.6
4
4
  Summary: Computer Building Blocks for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -12,9 +12,9 @@ License-File: LICENSE
12
12
  Requires-Dist: pytest~=8.4
13
13
  Requires-Dist: pytest-asyncio~=0.26
14
14
  Requires-Dist: openai~=2.25.0
15
- Requires-Dist: meshagent-api==0.45.4
16
- Requires-Dist: meshagent-agents==0.45.4
17
- Requires-Dist: meshagent-tools==0.45.4
15
+ Requires-Dist: meshagent-api==0.45.6
16
+ Requires-Dist: meshagent-agents==0.45.6
17
+ Requires-Dist: meshagent-tools==0.45.6
18
18
  Requires-Dist: playwright~=1.58.0
19
19
  Requires-Dist: stagehand~=3.6.0
20
20
  Requires-Dist: browserbase~=1.2
@@ -45,7 +45,7 @@ class ComputerTool(OpenAIResponsesTool):
45
45
  computer: Computer,
46
46
  title="computer_call",
47
47
  description="handle computer tool calls",
48
- rules=[],
48
+ rules: Optional[list[str]] = None,
49
49
  render_screen: Optional[Callable[[bytes], Awaitable[None] | None]] = None,
50
50
  toolkit: "ComputerToolkit",
51
51
  ):
@@ -54,7 +54,7 @@ class ComputerTool(OpenAIResponsesTool):
54
54
  # TODO: give a correct schema
55
55
  title=title,
56
56
  description=description,
57
- rules=rules,
57
+ rules=[] if rules is None else rules,
58
58
  )
59
59
  self.operator = operator
60
60
  self.computer = computer
@@ -7,7 +7,7 @@ from meshagent.computers import agent as agent_module
7
7
  from meshagent.computers import base_playwright as base_playwright_module
8
8
  from meshagent.computers.agent import ComputerToolkit
9
9
  from meshagent.computers.base_playwright import BasePlaywrightComputer
10
- from meshagent.computers.computer import ComputerContext
10
+ from meshagent.computers.computer import Computer, ComputerContext
11
11
  from meshagent.tools import ToolContext
12
12
 
13
13
 
@@ -43,6 +43,20 @@ class _FakeRoom:
43
43
  self.local_participant = _FakeParticipant(name=name)
44
44
 
45
45
 
46
+ class _FakeStorage:
47
+ def __init__(self) -> None:
48
+ self.uploads: list[dict[str, Any]] = []
49
+
50
+ async def upload(self, **kwargs: Any) -> None:
51
+ self.uploads.append(kwargs)
52
+
53
+
54
+ class _FakeRoomWithStorage(_FakeRoom):
55
+ def __init__(self, name: str):
56
+ super().__init__(name=name)
57
+ self.storage = _FakeStorage()
58
+
59
+
46
60
  class _FakeOperator:
47
61
  def __init__(self):
48
62
  self.calls: list[dict[str, Any]] = []
@@ -67,6 +81,204 @@ def test_computer_toolkit_has_no_default_render_screen():
67
81
  assert toolkit.render_screen is None
68
82
 
69
83
 
84
+ def test_computer_tool_does_not_share_default_rules_list():
85
+ first = ComputerToolkit(
86
+ computer=_FakeComputer(),
87
+ operator=_FakeOperator(),
88
+ room=_FakeRoom(name="agent"),
89
+ )
90
+ second = ComputerToolkit(
91
+ computer=_FakeComputer(),
92
+ operator=_FakeOperator(),
93
+ room=_FakeRoom(name="agent"),
94
+ )
95
+ first_tool = next(tool for tool in first.tools if tool.name == "computer_call")
96
+ second_tool = next(tool for tool in second.tools if tool.name == "computer_call")
97
+
98
+ first_tool.rules.append("first only")
99
+
100
+ assert first_tool.rules == ["first only"]
101
+ assert second_tool.rules == []
102
+
103
+
104
+ def test_computer_tool_preserves_explicit_rules_list_reference():
105
+ rules: list[str] = []
106
+ toolkit = ComputerToolkit(
107
+ computer=_FakeComputer(),
108
+ operator=_FakeOperator(),
109
+ room=_FakeRoom(name="agent"),
110
+ )
111
+ tool = type(next(tool for tool in toolkit.tools if tool.name == "computer_call"))(
112
+ computer=_FakeComputer(),
113
+ operator=_FakeOperator(),
114
+ toolkit=toolkit,
115
+ rules=rules,
116
+ )
117
+
118
+ rules.append("later")
119
+
120
+ assert tool.rules is rules
121
+ assert tool.rules == ["later"]
122
+
123
+
124
+ def test_computer_context_emit_invokes_event_handler_like_python():
125
+ events: list[dict[str, Any]] = []
126
+ room = _FakeRoom(name="agent")
127
+ context = ComputerContext(
128
+ room=room,
129
+ caller=room.local_participant,
130
+ event_handler=events.append,
131
+ )
132
+
133
+ event = {"type": "custom", "nested": {"value": 1}}
134
+ context.emit(event)
135
+ ComputerContext(room=room, caller=room.local_participant).emit({"ignored": True})
136
+
137
+ assert events == [event]
138
+
139
+
140
+ def test_computer_context_startup_events_match_python_dedupe_and_details():
141
+ events: list[dict[str, Any]] = []
142
+ room = _FakeRoom(name="agent")
143
+
144
+ def make_startup_event(state: str, details: tuple[str, ...]) -> dict[str, Any]:
145
+ return {"type": "startup", "state": state, "details": list(details)}
146
+
147
+ context = ComputerContext(
148
+ room=room,
149
+ caller=room.local_participant,
150
+ event_handler=events.append,
151
+ startup_event_factory=make_startup_event,
152
+ )
153
+
154
+ assert context.room is room
155
+ assert context.caller is room.local_participant
156
+ assert context.on_behalf_of is None
157
+ assert context.last_startup_state is None
158
+ assert context.last_startup_details == ()
159
+
160
+ context.emit_startup(state="queued", details=[" one ", "", 7, "two"]) # type: ignore[list-item]
161
+ context.emit_startup(state="queued", details=("one", "two"))
162
+ context.emit_startup(state="in_progress")
163
+
164
+ assert events == [
165
+ {"type": "startup", "state": "queued", "details": ["one", "two"]},
166
+ {"type": "startup", "state": "in_progress", "details": []},
167
+ ]
168
+ assert context.last_startup_state == "in_progress"
169
+ assert context.last_startup_details == ()
170
+
171
+ no_factory = ComputerContext(room=room, caller=room.local_participant)
172
+ no_factory.emit_startup(state="completed", details=["ignored"])
173
+ assert no_factory.last_startup_state is None
174
+ assert no_factory.last_startup_details == ()
175
+
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_computer_protocol_default_context_manager_returns_self() -> None:
179
+ class _DefaultContextManagerComputer(Computer):
180
+ environment = "browser"
181
+ dimensions = (1, 1)
182
+
183
+ computer = _DefaultContextManagerComputer()
184
+
185
+ assert await computer.__aenter__(object()) is computer # type: ignore[arg-type]
186
+ assert (
187
+ await computer.__aexit__(Exception, Exception("boom"), object()) # type: ignore[arg-type]
188
+ is computer
189
+ )
190
+
191
+
192
+ def test_base_playwright_constructor_helpers_match_python_branches(
193
+ monkeypatch: pytest.MonkeyPatch,
194
+ ) -> None:
195
+ assert base_playwright_module._parse_dimensions("") is None # noqa: SLF001
196
+ assert base_playwright_module._parse_dimensions(" 1440x900 ") == ( # noqa: SLF001
197
+ 1440,
198
+ 900,
199
+ )
200
+ assert base_playwright_module._parse_dimensions("1440X900") == ( # noqa: SLF001
201
+ 1440,
202
+ 900,
203
+ )
204
+ assert base_playwright_module._parse_dimensions("1600, 900") == ( # noqa: SLF001
205
+ 1600,
206
+ 900,
207
+ )
208
+ assert base_playwright_module._parse_dimensions("1600:900") is None # noqa: SLF001
209
+ assert base_playwright_module._parse_dimensions("1x2x3") is None # noqa: SLF001
210
+ assert base_playwright_module._parse_dimensions("1,2,3") is None # noqa: SLF001
211
+ assert base_playwright_module._parse_dimensions("wide x 900") is None # noqa: SLF001
212
+
213
+ monkeypatch.delenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", raising=False)
214
+ assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
215
+ 1440,
216
+ 900,
217
+ )
218
+ monkeypatch.setenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", "1600x900")
219
+ assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
220
+ 1600,
221
+ 900,
222
+ )
223
+ monkeypatch.setenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", "1200x800")
224
+ assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
225
+ 1440,
226
+ 900,
227
+ )
228
+
229
+ default_computer = BasePlaywrightComputer(starting_url=" ")
230
+ assert default_computer.dimensions == (1440, 900)
231
+ assert default_computer.starting_url == "https://google.com"
232
+ custom_computer = BasePlaywrightComputer(
233
+ dimensions=(1600, 900),
234
+ starting_url=" https://example.test ",
235
+ )
236
+ assert custom_computer.dimensions == (1600, 900)
237
+ assert custom_computer.starting_url == " https://example.test "
238
+ with pytest.raises(
239
+ ValueError,
240
+ match=r"playwright dimensions must be one of: \(1440, 900\), \(1600, 900\)",
241
+ ):
242
+ BasePlaywrightComputer(dimensions=(1200, 800))
243
+
244
+ for input_key, expected in [
245
+ ("/", "Divide"),
246
+ ("\\", "Backslash"),
247
+ ("alt", "Alt"),
248
+ ("arrowdown", "ArrowDown"),
249
+ ("arrowleft", "ArrowLeft"),
250
+ ("arrowright", "ArrowRight"),
251
+ ("arrowup", "ArrowUp"),
252
+ ("backspace", "Backspace"),
253
+ ("capslock", "CapsLock"),
254
+ ("cmd", "Meta"),
255
+ ("ctrl", "Control"),
256
+ ("delete", "Delete"),
257
+ ("end", "End"),
258
+ ("enter", "Enter"),
259
+ ("esc", "Escape"),
260
+ ("home", "Home"),
261
+ ("insert", "Insert"),
262
+ ("option", "Alt"),
263
+ ("pagedown", "PageDown"),
264
+ ("pageup", "PageUp"),
265
+ ("shift", "Shift"),
266
+ ("space", " "),
267
+ ("super", "Meta"),
268
+ ("tab", "Tab"),
269
+ ("win", "Meta"),
270
+ ("CTRL", "Control"),
271
+ ("A", "A"),
272
+ ]:
273
+ assert (
274
+ base_playwright_module.CUA_KEY_TO_PLAYWRIGHT_KEY.get(
275
+ input_key.lower(),
276
+ input_key,
277
+ )
278
+ == expected
279
+ )
280
+
281
+
70
282
  @pytest.mark.asyncio
71
283
  async def test_computer_tool_emits_startup_progress_events():
72
284
  computer = _FakeComputer()
@@ -148,6 +360,172 @@ async def test_computer_tool_propagates_computer_startup_events_via_tool_context
148
360
  assert events[2]["state"] == "completed"
149
361
 
150
362
 
363
+ @pytest.mark.asyncio
364
+ async def test_computer_tool_startup_failure_event_matches_python_suppression():
365
+ class _FailingComputer(_FakeComputer):
366
+ def __init__(self, emitted_state: str | None) -> None:
367
+ super().__init__()
368
+ self.emitted_state = emitted_state
369
+
370
+ async def __aenter__(
371
+ self,
372
+ context: ComputerContext,
373
+ ):
374
+ self.enter_contexts.append(context)
375
+ if self.emitted_state is not None:
376
+ context.emit_startup(state=self.emitted_state)
377
+ raise RuntimeError("boom")
378
+
379
+ for emitted_state, expected_states in (
380
+ (None, ["in_progress", "failed"]),
381
+ ("failed", ["in_progress", "failed"]),
382
+ ("cancelled", ["in_progress", "cancelled"]),
383
+ ):
384
+ computer = _FailingComputer(emitted_state=emitted_state)
385
+ room = _FakeRoom(name="agent")
386
+ toolkit = ComputerToolkit(
387
+ computer=computer,
388
+ operator=_FakeOperator(),
389
+ room=room,
390
+ render_screen=None,
391
+ )
392
+ events: list[dict[str, Any]] = []
393
+ context = ToolContext(
394
+ caller=room.local_participant,
395
+ event_handler=events.append,
396
+ )
397
+
398
+ computer_tool = next(
399
+ tool for tool in toolkit.tools if tool.name == "computer_call"
400
+ )
401
+ with pytest.raises(RuntimeError, match="boom"):
402
+ await computer_tool.handle_computer_call(
403
+ context=context,
404
+ type="computer_call",
405
+ action={"type": "wait"},
406
+ )
407
+
408
+ assert [event["state"] for event in events] == expected_states
409
+
410
+
411
+ @pytest.mark.asyncio
412
+ async def test_screenshot_tool_execute_matches_python_side_effects():
413
+ class _ScreenshotComputer(_FakeComputer):
414
+ def __init__(self) -> None:
415
+ super().__init__()
416
+ self.screenshot_calls: list[dict[str, Any]] = []
417
+
418
+ async def screenshot_bytes(
419
+ self,
420
+ context: ComputerContext,
421
+ *,
422
+ full_page: bool,
423
+ ) -> bytes:
424
+ self.screenshot_calls.append(
425
+ {"context": context, "full_page": full_page},
426
+ )
427
+ return b"png-bytes"
428
+
429
+ computer = _ScreenshotComputer()
430
+ room = _FakeRoomWithStorage(name="agent")
431
+ toolkit = ComputerToolkit(
432
+ computer=computer,
433
+ operator=_FakeOperator(),
434
+ room=room,
435
+ render_screen=None,
436
+ )
437
+ events: list[dict[str, Any]] = []
438
+ context = ToolContext(caller=room.local_participant, event_handler=events.append)
439
+ tool = agent_module.ScreenshotTool(
440
+ room=room,
441
+ computer=computer,
442
+ toolkit=toolkit,
443
+ )
444
+
445
+ result = await tool.execute(context=context, save_path="screen.png", full_page=True)
446
+
447
+ assert result == "saved screenshot to screen.png"
448
+ assert len(computer.enter_contexts) == 1
449
+ assert computer.screenshot_calls == [
450
+ {"context": computer.enter_contexts[0], "full_page": True},
451
+ ]
452
+ assert room.storage.uploads == [
453
+ {"path": "screen.png", "data": b"png-bytes", "overwrite": True},
454
+ ]
455
+ assert [event["state"] for event in events] == ["in_progress", "completed"]
456
+
457
+
458
+ @pytest.mark.asyncio
459
+ async def test_goto_tool_execute_matches_python_navigation_and_rendering():
460
+ class _GotoComputer(_FakeComputer):
461
+ def __init__(self) -> None:
462
+ super().__init__()
463
+ self.goto_calls: list[dict[str, Any]] = []
464
+ self.screenshot_calls: list[dict[str, Any]] = []
465
+
466
+ async def goto(
467
+ self,
468
+ context: ComputerContext,
469
+ *,
470
+ url: str,
471
+ ) -> None:
472
+ self.goto_calls.append({"context": context, "url": url})
473
+
474
+ async def screenshot_bytes(
475
+ self,
476
+ context: ComputerContext,
477
+ *,
478
+ full_page: bool,
479
+ ) -> bytes:
480
+ self.screenshot_calls.append(
481
+ {"context": context, "full_page": full_page},
482
+ )
483
+ return b"rendered-png"
484
+
485
+ computer = _GotoComputer()
486
+ room = _FakeRoom(name="agent")
487
+ rendered: list[bytes] = []
488
+ render_called = asyncio.Event()
489
+
490
+ async def render_screen(data: bytes) -> None:
491
+ rendered.append(data)
492
+ render_called.set()
493
+
494
+ toolkit = ComputerToolkit(
495
+ computer=computer,
496
+ operator=_FakeOperator(),
497
+ room=room,
498
+ render_screen=render_screen,
499
+ )
500
+ events: list[dict[str, Any]] = []
501
+ context = ToolContext(caller=room.local_participant, event_handler=events.append)
502
+ tool = agent_module.GotoURL(
503
+ computer=computer,
504
+ toolkit=toolkit,
505
+ render_screen=render_screen,
506
+ )
507
+
508
+ result = await tool.execute(context=context, url="example.test/path")
509
+
510
+ assert result is None
511
+ assert render_called.is_set()
512
+ assert rendered == [b"rendered-png"]
513
+ assert len(computer.enter_contexts) == 1
514
+ assert computer.goto_calls == [
515
+ {
516
+ "context": computer.enter_contexts[0],
517
+ "url": "https://example.test/path",
518
+ },
519
+ ]
520
+ assert computer.screenshot_calls == [
521
+ {"context": computer.enter_contexts[0], "full_page": False},
522
+ ]
523
+ assert [event["state"] for event in events] == ["in_progress", "completed"]
524
+
525
+ await tool.execute(context=context, url="http://example.test/next")
526
+ assert computer.goto_calls[-1]["url"] == "http://example.test/next"
527
+
528
+
151
529
  @pytest.mark.asyncio
152
530
  async def test_computer_tool_render_screen_uses_python_lenient_base64_decode():
153
531
  class _OutputOperator:
@@ -0,0 +1,133 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pytest
4
+
5
+ from meshagent.computers import browserbase as browserbase_module
6
+ from meshagent.computers.browserbase import BrowserbaseBrowser
7
+ from meshagent.computers.computer import ComputerContext
8
+
9
+
10
+ class _FakeParticipant:
11
+ def get_attribute(self, key: str):
12
+ del key
13
+ return None
14
+
15
+
16
+ class _FakeRoom:
17
+ local_participant = _FakeParticipant()
18
+
19
+
20
+ class _FakeBrowserbaseSessions:
21
+ def __init__(self) -> None:
22
+ self.create_calls: list[dict[str, object]] = []
23
+
24
+ async def create(self, **kwargs):
25
+ self.create_calls.append(kwargs)
26
+ return SimpleNamespace(id="session-1", connect_url="wss://connect.test")
27
+
28
+
29
+ class _FakeBrowserbase:
30
+ instances: list["_FakeBrowserbase"] = []
31
+
32
+ def __init__(self, *, api_key: str | None = None):
33
+ self.api_key = api_key
34
+ self.sessions = _FakeBrowserbaseSessions()
35
+ self.instances.append(self)
36
+
37
+
38
+ class _FakePage:
39
+ def __init__(self) -> None:
40
+ self.goto_calls: list[str] = []
41
+ self.handlers: list[tuple[str, object]] = []
42
+
43
+ def on(self, event: str, handler) -> None:
44
+ self.handlers.append((event, handler))
45
+
46
+ async def goto(self, url: str) -> None:
47
+ self.goto_calls.append(url)
48
+
49
+
50
+ class _FakeBrowserContext:
51
+ def __init__(self, page: _FakePage) -> None:
52
+ self.pages = [page]
53
+ self.handlers: list[tuple[str, object]] = []
54
+ self.init_scripts: list[str] = []
55
+
56
+ def on(self, event: str, handler) -> None:
57
+ self.handlers.append((event, handler))
58
+
59
+ async def add_init_script(self, script: str) -> None:
60
+ self.init_scripts.append(script)
61
+
62
+
63
+ class _FakeBrowser:
64
+ def __init__(self, context: _FakeBrowserContext) -> None:
65
+ self.contexts = [context]
66
+
67
+
68
+ class _FakeChromium:
69
+ def __init__(self, browser: _FakeBrowser) -> None:
70
+ self.browser = browser
71
+ self.connect_calls: list[dict[str, object]] = []
72
+
73
+ async def connect_over_cdp(self, connect_url: str, *, timeout: int):
74
+ self.connect_calls.append({"connect_url": connect_url, "timeout": timeout})
75
+ return self.browser
76
+
77
+
78
+ def _make_context() -> ComputerContext:
79
+ room = _FakeRoom()
80
+ return ComputerContext(room=room, caller=room.local_participant)
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_browserbase_get_browser_and_page_matches_python_session_shape(
85
+ monkeypatch: pytest.MonkeyPatch,
86
+ ) -> None:
87
+ _FakeBrowserbase.instances.clear()
88
+ monkeypatch.setenv("BROWSERBASE_API_KEY", "api-key")
89
+ monkeypatch.setenv("BROWSERBASE_PROJECT_ID", "project-id")
90
+ monkeypatch.setattr(browserbase_module, "AsyncBrowserbase", _FakeBrowserbase)
91
+
92
+ page = _FakePage()
93
+ browser_context = _FakeBrowserContext(page)
94
+ browser = _FakeBrowser(browser_context)
95
+ chromium = _FakeChromium(browser)
96
+
97
+ computer = BrowserbaseBrowser(
98
+ width=1200,
99
+ height=700,
100
+ region="eu-central-1",
101
+ proxy=True,
102
+ virtual_mouse=True,
103
+ ad_blocker=True,
104
+ starting_url=" https://browserbase.test ",
105
+ )
106
+ computer._playwright = SimpleNamespace(chromium=chromium)
107
+
108
+ returned_browser, returned_page = await computer._get_browser_and_page(
109
+ _make_context()
110
+ )
111
+
112
+ assert returned_browser is browser
113
+ assert returned_page is page
114
+ assert computer.session.id == "session-1"
115
+ assert _FakeBrowserbase.instances[0].api_key == "api-key"
116
+ assert _FakeBrowserbase.instances[0].sessions.create_calls == [
117
+ {
118
+ "project_id": "project-id",
119
+ "browser_settings": {
120
+ "viewport": {"width": 1200, "height": 700},
121
+ "blockAds": True,
122
+ },
123
+ "region": "eu-central-1",
124
+ "proxies": True,
125
+ }
126
+ ]
127
+ assert chromium.connect_calls == [
128
+ {"connect_url": "wss://connect.test", "timeout": 60000}
129
+ ]
130
+ assert browser_context.handlers == [("page", computer._handle_new_page)]
131
+ assert len(browser_context.init_scripts) == 1
132
+ assert page.handlers == [("close", computer._handle_page_close)]
133
+ assert page.goto_calls == [" https://browserbase.test "]
@@ -350,7 +350,7 @@ async def test_container_playwright_uses_custom_starting_url() -> None:
350
350
  computer = ContainerPlaywrightComputer(
351
351
  room=room,
352
352
  headless=True,
353
- starting_url="https://example.com",
353
+ starting_url=" https://example.com ",
354
354
  )
355
355
 
356
356
  async def _ensure_container(context: ComputerContext) -> str:
@@ -378,7 +378,7 @@ async def test_container_playwright_uses_custom_starting_url() -> None:
378
378
  assert connected_browser is browser
379
379
  assert connected_page is page
380
380
  assert page.viewport_calls == [{"width": 1440, "height": 900}]
381
- assert page.goto_calls == ["https://example.com"]
381
+ assert page.goto_calls == [" https://example.com "]
382
382
 
383
383
 
384
384
  @pytest.mark.asyncio
@@ -63,8 +63,8 @@ class DockerComputer:
63
63
  )
64
64
 
65
65
  # Fetch display geometry
66
- geometry = await self._exec(
67
- f"DISPLAY={self.display} xdotool getdisplaygeometry"
66
+ geometry = (
67
+ await self._exec(f"DISPLAY={self.display} xdotool getdisplaygeometry")
68
68
  ).strip()
69
69
  if geometry:
70
70
  w, h = geometry.split()
@@ -0,0 +1,167 @@
1
+ import subprocess
2
+
3
+ import pytest
4
+
5
+ from meshagent.computers import docker as docker_module
6
+ from meshagent.computers.docker import DockerComputer, _async_check_output
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ async def test_async_check_output_shell_executes_and_preserves_failure_fields():
11
+ assert await _async_check_output("printf stdout", shell=True) == b"stdout"
12
+
13
+ with pytest.raises(subprocess.CalledProcessError) as exc_info:
14
+ await _async_check_output(
15
+ "printf stdout; printf stderr >&2; exit 7",
16
+ shell=True,
17
+ )
18
+
19
+ exc = exc_info.value
20
+ assert exc.returncode == 7
21
+ assert exc.cmd == ("printf stdout; printf stderr >&2; exit 7",)
22
+ assert exc.output == b"stdout"
23
+ assert exc.stderr == b"stderr"
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_docker_computer_aenter_awaits_exec_before_stripping_geometry(
28
+ monkeypatch,
29
+ ):
30
+ class Result:
31
+ stdout = "container-id\n"
32
+
33
+ run_calls = []
34
+
35
+ def fake_run(*args, **kwargs):
36
+ run_calls.append((args, kwargs))
37
+ return Result()
38
+
39
+ async def fake_exec(self, cmd):
40
+ exec_calls.append(cmd)
41
+ return "1600 900\n"
42
+
43
+ exec_calls = []
44
+ monkeypatch.setattr(docker_module.subprocess, "run", fake_run)
45
+ monkeypatch.setattr(DockerComputer, "_exec", fake_exec)
46
+
47
+ computer = DockerComputer()
48
+ result = await computer.__aenter__(context=object())
49
+
50
+ assert result is computer
51
+ assert computer.dimensions == (1600, 900)
52
+ assert run_calls == [
53
+ (
54
+ (
55
+ [
56
+ "docker",
57
+ "ps",
58
+ "-q",
59
+ "-f",
60
+ "name=cua-sample-app",
61
+ ],
62
+ ),
63
+ {"capture_output": True, "text": True},
64
+ )
65
+ ]
66
+ assert exec_calls == ["DISPLAY=:99 xdotool getdisplaygeometry"]
67
+
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_docker_computer_commands_match_python_source(monkeypatch) -> None:
71
+ calls: list[tuple[tuple, dict]] = []
72
+
73
+ async def fake_check_output(*args, **kwargs):
74
+ calls.append((args, kwargs))
75
+ return b"ok"
76
+
77
+ monkeypatch.setattr(docker_module, "_async_check_output", fake_check_output)
78
+ computer = DockerComputer()
79
+
80
+ assert await computer._exec('echo "hi"') == "ok"
81
+ assert calls.pop(0) == (
82
+ ('docker exec cua-sample-app sh -c "echo \\"hi\\""',),
83
+ {"shell": True},
84
+ )
85
+
86
+ assert await computer.screenshot(context=object()) == "ok"
87
+ assert calls.pop(0) == (
88
+ (
89
+ 'docker exec cua-sample-app sh -c "export DISPLAY=:99 && import -window root png:- | base64 -w 0"',
90
+ ),
91
+ {"shell": True},
92
+ )
93
+
94
+ await computer.click(context=object(), x=10, y=20, button="middle")
95
+ await computer.click(context=object(), x=10, y=20, button="unknown")
96
+ await computer.double_click(context=object(), x=3, y=4)
97
+ await computer.scroll(context=object(), x=1, y=2, scroll_x=99, scroll_y=-2)
98
+ await computer.type(context=object(), text="don't")
99
+ await computer.move(context=object(), x=5, y=6)
100
+ await computer.keypress(context=object(), keys=["ENTER", "SPACE", "x"])
101
+
102
+ assert calls == [
103
+ (
104
+ (
105
+ 'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 10 20 click 2"',
106
+ ),
107
+ {"shell": True},
108
+ ),
109
+ (
110
+ (
111
+ 'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 10 20 click 1"',
112
+ ),
113
+ {"shell": True},
114
+ ),
115
+ (
116
+ (
117
+ 'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 3 4 click --repeat 2 1"',
118
+ ),
119
+ {"shell": True},
120
+ ),
121
+ (
122
+ ('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 1 2"',),
123
+ {"shell": True},
124
+ ),
125
+ (
126
+ ('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool click 4"',),
127
+ {"shell": True},
128
+ ),
129
+ (
130
+ ('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool click 4"',),
131
+ {"shell": True},
132
+ ),
133
+ (
134
+ (
135
+ "docker exec cua-sample-app sh -c \"DISPLAY=:99 xdotool type -- 'don'\\''t'\"",
136
+ ),
137
+ {"shell": True},
138
+ ),
139
+ (
140
+ ('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 5 6"',),
141
+ {"shell": True},
142
+ ),
143
+ (
144
+ (
145
+ 'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool key Return+space+x"',
146
+ ),
147
+ {"shell": True},
148
+ ),
149
+ ]
150
+
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_docker_computer_unsupported_url_methods_raise_runtime_error() -> None:
154
+ computer = DockerComputer()
155
+
156
+ with pytest.raises(
157
+ RuntimeError, match="get_current_url is not supported by DockerComputer"
158
+ ):
159
+ await computer.get_current_url(context=object())
160
+ with pytest.raises(RuntimeError, match="goto is not supported by DockerComputer"):
161
+ await computer.goto(context=object(), url="https://example.test")
162
+ with pytest.raises(RuntimeError, match="back is not supported by DockerComputer"):
163
+ await computer.back(context=object())
164
+ with pytest.raises(
165
+ RuntimeError, match="forward is not supported by DockerComputer"
166
+ ):
167
+ await computer.forward(context=object())
@@ -61,6 +61,41 @@ class _FakeChromium:
61
61
  return self.browser
62
62
 
63
63
 
64
+ class _FakeRemoteBrowserContext:
65
+ def __init__(self, page: _FakePage | None = None) -> None:
66
+ self.pages = [] if page is None else [page]
67
+ self.new_context_calls: list[dict[str, object]] = []
68
+ self.new_page_calls = 0
69
+ self.page = page or _FakePage()
70
+
71
+ async def new_page(self) -> _FakePage:
72
+ self.new_page_calls += 1
73
+ self.pages.append(self.page)
74
+ return self.page
75
+
76
+
77
+ class _FakeRemoteBrowser:
78
+ def __init__(self, context: _FakeRemoteBrowserContext | None = None) -> None:
79
+ self.contexts = [] if context is None else [context]
80
+ self.context = context or _FakeRemoteBrowserContext()
81
+ self.new_context_calls: list[dict[str, object]] = []
82
+
83
+ async def new_context(self, **kwargs) -> _FakeRemoteBrowserContext:
84
+ self.new_context_calls.append(kwargs)
85
+ self.contexts.append(self.context)
86
+ return self.context
87
+
88
+
89
+ class _FakeRemoteChromium:
90
+ def __init__(self, browser: _FakeRemoteBrowser) -> None:
91
+ self.browser = browser
92
+ self.connect_calls: list[dict[str, object]] = []
93
+
94
+ async def connect_over_cdp(self, cdp_url: str, **kwargs):
95
+ self.connect_calls.append({"cdp_url": cdp_url, **kwargs})
96
+ return self.browser
97
+
98
+
64
99
  class _FakeStagehandSessions:
65
100
  def __init__(self) -> None:
66
101
  self.start_calls: list[dict[str, object]] = []
@@ -126,7 +161,7 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
126
161
 
127
162
  computer = StagehandComputer(
128
163
  dimensions=(1600, 900),
129
- starting_url="https://example.com",
164
+ starting_url=" https://example.com ",
130
165
  )
131
166
  computer._playwright = SimpleNamespace(chromium=chromium)
132
167
 
@@ -151,7 +186,7 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
151
186
  assert page.viewport_calls == [{"width": 1600, "height": 900}]
152
187
  assert page.goto_calls == [
153
188
  ("about:blank", "domcontentloaded"),
154
- ("https://example.com", None),
189
+ (" https://example.com ", None),
155
190
  ]
156
191
 
157
192
  assert len(_FakeStagehand.init_calls) == 1
@@ -188,6 +223,67 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
188
223
  assert stagehand.closed is True
189
224
 
190
225
 
226
+ @pytest.mark.asyncio
227
+ async def test_stagehand_computer_remote_session_plan_matches_python(
228
+ monkeypatch: pytest.MonkeyPatch,
229
+ ) -> None:
230
+ _FakeStagehand.init_calls.clear()
231
+ remote_browser = _FakeRemoteBrowser()
232
+ chromium = _FakeRemoteChromium(browser=remote_browser)
233
+
234
+ monkeypatch.setattr(
235
+ stagehand_module, "_require_stagehand_class", lambda: _FakeStagehand
236
+ )
237
+ monkeypatch.setattr(
238
+ stagehand_module,
239
+ "stagehand_available",
240
+ lambda **_: True,
241
+ )
242
+
243
+ computer = StagehandComputer(
244
+ dimensions=(1600, 900),
245
+ starting_url="https://remote.example",
246
+ stagehand_config=StagehandComputerConfig(
247
+ server="remote",
248
+ browser={"type": "browserbase"},
249
+ ),
250
+ )
251
+ computer._playwright = SimpleNamespace(chromium=chromium)
252
+
253
+ browser, page = await computer._get_browser_and_page(_make_context())
254
+
255
+ assert browser is remote_browser
256
+ assert page is remote_browser.context.page
257
+ assert chromium.connect_calls == [
258
+ {
259
+ "cdp_url": "ws://127.0.0.1:9222/devtools/browser/example",
260
+ "timeout": 60_000,
261
+ }
262
+ ]
263
+ assert remote_browser.new_context_calls == [
264
+ {"viewport": {"width": 1600, "height": 900}}
265
+ ]
266
+ assert remote_browser.context.new_page_calls == 1
267
+ assert page.viewport_calls == [{"width": 1600, "height": 900}]
268
+ assert page.goto_calls == [("https://remote.example", None)]
269
+
270
+ assert len(_FakeStagehand.init_calls) == 1
271
+ init_call = _FakeStagehand.init_calls[0]
272
+ assert init_call["server"] == "remote"
273
+ assert init_call["model_api_key"] == "room_token"
274
+ assert init_call["local_openai_api_key"] == "room_token"
275
+
276
+ stagehand = computer._stagehand
277
+ assert isinstance(stagehand, _FakeStagehand)
278
+ assert stagehand.sessions.start_calls == [
279
+ {
280
+ "model_name": "openai/gpt-5.4",
281
+ "browser": {"type": "browserbase"},
282
+ }
283
+ ]
284
+ assert computer._stagehand_session_id == "stagehand_session_1"
285
+
286
+
191
287
  def test_stagehand_computer_can_update_config() -> None:
192
288
  computer = StagehandComputer()
193
289
 
@@ -202,6 +298,49 @@ def test_stagehand_computer_can_update_config() -> None:
202
298
  )
203
299
 
204
300
 
301
+ def test_stagehand_start_kwargs_and_update_conflict_match_python() -> None:
302
+ computer = StagehandComputer(
303
+ stagehand_config=StagehandComputerConfig(
304
+ model_name="openai/custom",
305
+ browserbase_session_create_params={"keepAlive": True},
306
+ browserbase_session_id="session-1",
307
+ dom_settle_timeout_ms=1234.0,
308
+ experimental=True,
309
+ self_heal=False,
310
+ system_prompt="system",
311
+ verbose=2,
312
+ )
313
+ )
314
+
315
+ assert computer._runtime_stagehand_start_kwargs(browser={"type": "remote"}) == {
316
+ "model_name": "openai/custom",
317
+ "browser": {"type": "remote"},
318
+ "browserbase_session_create_params": {"keepAlive": True},
319
+ "browserbase_session_id": "session-1",
320
+ "dom_settle_timeout_ms": 1234.0,
321
+ "experimental": True,
322
+ "self_heal": False,
323
+ "system_prompt": "system",
324
+ "verbose": 2,
325
+ }
326
+ assert computer._runtime_stagehand_start_kwargs(browser=None) == {
327
+ "model_name": "openai/custom",
328
+ "browserbase_session_create_params": {"keepAlive": True},
329
+ "browserbase_session_id": "session-1",
330
+ "dom_settle_timeout_ms": 1234.0,
331
+ "experimental": True,
332
+ "self_heal": False,
333
+ "system_prompt": "system",
334
+ "verbose": 2,
335
+ }
336
+
337
+ with pytest.raises(ValueError, match="pass config or keyword changes, not both"):
338
+ computer.update_stagehand_config(
339
+ config=StagehandComputerConfig(),
340
+ model_name="openai/other",
341
+ )
342
+
343
+
205
344
  def test_stagehand_local_browser_config_uses_python_dict_coercion() -> None:
206
345
  computer = StagehandComputer(
207
346
  stagehand_config=StagehandComputerConfig(
@@ -0,0 +1,102 @@
1
+ import base64
2
+
3
+ import pytest
4
+
5
+ from meshagent.computers import utils as utils_module
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "url",
10
+ [
11
+ "https://maliciousbook.com",
12
+ "https://login.maliciousbook.com/path",
13
+ "https://evilvideos.com/watch?v=1",
14
+ "https://EVILVIDEOS.COM/watch?v=1",
15
+ "https://user:pass@evilvideos.com:443/path",
16
+ "https://user@sub.ilanbigio.com/path",
17
+ "https://evilvideos.com:bad/path",
18
+ "//evilvideos.com/path",
19
+ ],
20
+ )
21
+ def test_check_blocklisted_url_rejects_blocklisted_domains(url: str) -> None:
22
+ with pytest.raises(ValueError, match="Blocked URL"):
23
+ utils_module.check_blocklisted_url(url)
24
+
25
+
26
+ def test_check_blocklisted_url_allows_other_domains() -> None:
27
+ utils_module.check_blocklisted_url("https://example.com/path")
28
+
29
+
30
+ @pytest.mark.parametrize(
31
+ "url",
32
+ [
33
+ "EVILVIDEOS.COM/path",
34
+ "evilvideos.com/path",
35
+ "http://evilvideos.com\\@example.com/path",
36
+ "http://sub.ilanbigio.com.",
37
+ "http:// evilvideos.com /path",
38
+ ],
39
+ )
40
+ def test_check_blocklisted_url_preserves_urlparse_hostname_edges(url: str) -> None:
41
+ utils_module.check_blocklisted_url(url)
42
+
43
+
44
+ def test_check_blocklisted_url_raises_urlparse_errors() -> None:
45
+ with pytest.raises(ValueError, match="Invalid IPv6 URL"):
46
+ utils_module.check_blocklisted_url("http://[::1")
47
+
48
+
49
+ @pytest.mark.parametrize("value", [None, [], "x", 3])
50
+ def test_sanitize_message_non_dict_inputs_raise_python_get_error(value) -> None:
51
+ with pytest.raises(AttributeError, match="object has no attribute 'get'"):
52
+ utils_module.sanitize_message(value)
53
+
54
+
55
+ def test_calculate_image_dimensions_supports_xbm_like_pillow() -> None:
56
+ xbm = (
57
+ b"#define sample_width 17\n"
58
+ b"#define sample_height 9\n"
59
+ b"static unsigned char sample_bits[] = { 0x00 };\n"
60
+ )
61
+ assert utils_module.calculate_image_dimensions(base64.b64encode(xbm).decode()) == (
62
+ 17,
63
+ 9,
64
+ )
65
+
66
+
67
+ def test_calculate_image_dimensions_supports_xpm_like_pillow() -> None:
68
+ xpm = (
69
+ b"/* XPM */\n"
70
+ b"static char * sample[] = {\n"
71
+ b'"13 7 1 1",\n'
72
+ b'"a c #000000",\n'
73
+ b'"aaaaaaaaaaaaa",\n'
74
+ b'"aaaaaaaaaaaaa",\n'
75
+ b'"aaaaaaaaaaaaa",\n'
76
+ b'"aaaaaaaaaaaaa",\n'
77
+ b'"aaaaaaaaaaaaa",\n'
78
+ b'"aaaaaaaaaaaaa",\n'
79
+ b'"aaaaaaaaaaaaa"};\n'
80
+ )
81
+ assert utils_module.calculate_image_dimensions(base64.b64encode(xpm).decode()) == (
82
+ 13,
83
+ 7,
84
+ )
85
+
86
+
87
+ @pytest.mark.parametrize(
88
+ "payload",
89
+ [
90
+ bytes([0, 0, 1, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 22, 0, 0, 0])
91
+ + b"abcd",
92
+ bytes([0, 0, 2, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 22, 0, 0, 0])
93
+ + b"abcd",
94
+ bytes([0, 0, 1, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 100, 0, 0, 0])
95
+ + b"abcd",
96
+ ],
97
+ )
98
+ def test_calculate_image_dimensions_rejects_invalid_ico_payloads_like_pillow(
99
+ payload: bytes,
100
+ ) -> None:
101
+ with pytest.raises(Exception):
102
+ utils_module.calculate_image_dimensions(base64.b64encode(payload).decode())
@@ -0,0 +1 @@
1
+ __version__ = "0.45.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-computers
3
- Version: 0.45.4
3
+ Version: 0.45.6
4
4
  Summary: Computer Building Blocks for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -12,9 +12,9 @@ License-File: LICENSE
12
12
  Requires-Dist: pytest~=8.4
13
13
  Requires-Dist: pytest-asyncio~=0.26
14
14
  Requires-Dist: openai~=2.25.0
15
- Requires-Dist: meshagent-api==0.45.4
16
- Requires-Dist: meshagent-agents==0.45.4
17
- Requires-Dist: meshagent-tools==0.45.4
15
+ Requires-Dist: meshagent-api==0.45.6
16
+ Requires-Dist: meshagent-agents==0.45.6
17
+ Requires-Dist: meshagent-tools==0.45.6
18
18
  Requires-Dist: playwright~=1.58.0
19
19
  Requires-Dist: stagehand~=3.6.0
20
20
  Requires-Dist: browserbase~=1.2
@@ -8,6 +8,7 @@ meshagent/computers/agent.py
8
8
  meshagent/computers/agent_test.py
9
9
  meshagent/computers/base_playwright.py
10
10
  meshagent/computers/browserbase.py
11
+ meshagent/computers/browserbase_test.py
11
12
  meshagent/computers/computer.py
12
13
  meshagent/computers/container_playwright.py
13
14
  meshagent/computers/container_playwright_test.py
@@ -1,9 +1,9 @@
1
1
  pytest~=8.4
2
2
  pytest-asyncio~=0.26
3
3
  openai~=2.25.0
4
- meshagent-api==0.45.4
5
- meshagent-agents==0.45.4
6
- meshagent-tools==0.45.4
4
+ meshagent-api==0.45.6
5
+ meshagent-agents==0.45.6
6
+ meshagent-tools==0.45.6
7
7
  playwright~=1.58.0
8
8
  stagehand~=3.6.0
9
9
  browserbase~=1.2
@@ -15,9 +15,9 @@ dependencies = [
15
15
  "pytest~=8.4",
16
16
  "pytest-asyncio~=0.26",
17
17
  "openai~=2.25.0",
18
- "meshagent-api==0.45.4",
19
- "meshagent-agents==0.45.4",
20
- "meshagent-tools==0.45.4",
18
+ "meshagent-api==0.45.6",
19
+ "meshagent-agents==0.45.6",
20
+ "meshagent-tools==0.45.6",
21
21
  "playwright~=1.58.0",
22
22
  "stagehand~=3.6.0",
23
23
  "browserbase~=1.2",
@@ -1,22 +0,0 @@
1
- import subprocess
2
-
3
- import pytest
4
-
5
- from meshagent.computers.docker import _async_check_output
6
-
7
-
8
- @pytest.mark.asyncio
9
- async def test_async_check_output_shell_executes_and_preserves_failure_fields():
10
- assert await _async_check_output("printf stdout", shell=True) == b"stdout"
11
-
12
- with pytest.raises(subprocess.CalledProcessError) as exc_info:
13
- await _async_check_output(
14
- "printf stdout; printf stderr >&2; exit 7",
15
- shell=True,
16
- )
17
-
18
- exc = exc_info.value
19
- assert exc.returncode == 7
20
- assert exc.cmd == ("printf stdout; printf stderr >&2; exit 7",)
21
- assert exc.output == b"stdout"
22
- assert exc.stderr == b"stderr"
@@ -1,47 +0,0 @@
1
- import pytest
2
-
3
- from meshagent.computers import utils as utils_module
4
-
5
-
6
- @pytest.mark.parametrize(
7
- "url",
8
- [
9
- "https://maliciousbook.com",
10
- "https://login.maliciousbook.com/path",
11
- "https://evilvideos.com/watch?v=1",
12
- "https://user:pass@evilvideos.com:443/path",
13
- "//evilvideos.com/path",
14
- ],
15
- )
16
- def test_check_blocklisted_url_rejects_blocklisted_domains(url: str) -> None:
17
- with pytest.raises(ValueError, match="Blocked URL"):
18
- utils_module.check_blocklisted_url(url)
19
-
20
-
21
- def test_check_blocklisted_url_allows_other_domains() -> None:
22
- utils_module.check_blocklisted_url("https://example.com/path")
23
-
24
-
25
- @pytest.mark.parametrize(
26
- "url",
27
- [
28
- "EVILVIDEOS.COM/path",
29
- "evilvideos.com/path",
30
- "http://evilvideos.com\\@example.com/path",
31
- "http://sub.ilanbigio.com.",
32
- "http:// evilvideos.com /path",
33
- ],
34
- )
35
- def test_check_blocklisted_url_preserves_urlparse_hostname_edges(url: str) -> None:
36
- utils_module.check_blocklisted_url(url)
37
-
38
-
39
- def test_check_blocklisted_url_raises_urlparse_errors() -> None:
40
- with pytest.raises(ValueError, match="Invalid IPv6 URL"):
41
- utils_module.check_blocklisted_url("http://[::1")
42
-
43
-
44
- @pytest.mark.parametrize("value", [None, [], "x", 3])
45
- def test_sanitize_message_non_dict_inputs_raise_python_get_error(value) -> None:
46
- with pytest.raises(AttributeError, match="object has no attribute 'get'"):
47
- utils_module.sanitize_message(value)
@@ -1 +0,0 @@
1
- __version__ = "0.45.4"