mcp-virtual-computer 0.2.3__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 (48) hide show
  1. kilntainers/__init__.py +3 -0
  2. kilntainers/__main__.py +6 -0
  3. kilntainers/auth.py +34 -0
  4. kilntainers/backends/__init__.py +17 -0
  5. kilntainers/backends/base.py +380 -0
  6. kilntainers/backends/docker.py +1498 -0
  7. kilntainers/backends/test___init__.py +37 -0
  8. kilntainers/backends/test_base.py +355 -0
  9. kilntainers/backends/test_docker.py +841 -0
  10. kilntainers/backends/test_docker_integration.py +310 -0
  11. kilntainers/backends/test_docker_management.py +122 -0
  12. kilntainers/backends/test_utils.py +152 -0
  13. kilntainers/backends/test_virtual_docker.py +387 -0
  14. kilntainers/cli.py +375 -0
  15. kilntainers/computers.py +284 -0
  16. kilntainers/config.py +71 -0
  17. kilntainers/dashboard.html +4263 -0
  18. kilntainers/dashboard.py +23 -0
  19. kilntainers/desktop.py +47 -0
  20. kilntainers/desktop_control.py +105 -0
  21. kilntainers/desktop_image/Dockerfile +67 -0
  22. kilntainers/desktop_image/desktop-control.py +661 -0
  23. kilntainers/desktop_image/start-desktop.sh +206 -0
  24. kilntainers/desktop_image/visual-action.py +185 -0
  25. kilntainers/desktop_image/wsproxy.py +218 -0
  26. kilntainers/errors.py +36 -0
  27. kilntainers/file_tools.py +320 -0
  28. kilntainers/server.py +1760 -0
  29. kilntainers/test_cli.py +704 -0
  30. kilntainers/test_cli_integration.py +213 -0
  31. kilntainers/test_computers.py +122 -0
  32. kilntainers/test_config.py +135 -0
  33. kilntainers/test_dashboard.py +113 -0
  34. kilntainers/test_desktop_control.py +109 -0
  35. kilntainers/test_e2e_mcp.py +310 -0
  36. kilntainers/test_errors.py +62 -0
  37. kilntainers/test_file_tools.py +175 -0
  38. kilntainers/test_http_lifecycle.py +353 -0
  39. kilntainers/test_lifecycle_integration.py +389 -0
  40. kilntainers/test_server.py +849 -0
  41. kilntainers/test_windows_docker.py +300 -0
  42. kilntainers/windows_docker.py +565 -0
  43. mcp_virtual_computer-0.2.3.dist-info/METADATA +179 -0
  44. mcp_virtual_computer-0.2.3.dist-info/RECORD +48 -0
  45. mcp_virtual_computer-0.2.3.dist-info/WHEEL +5 -0
  46. mcp_virtual_computer-0.2.3.dist-info/entry_points.txt +5 -0
  47. mcp_virtual_computer-0.2.3.dist-info/licenses/LICENSE +7 -0
  48. mcp_virtual_computer-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,387 @@
1
+ """Docker-only virtual computer configuration and desktop integration tests."""
2
+
3
+ import os
4
+ from importlib.resources import files
5
+ from typing import cast
6
+
7
+ import pytest
8
+
9
+ from kilntainers.backends.base import ExecRequest
10
+ from kilntainers.backends.docker import (
11
+ DEFAULT_DESKTOP_IMAGE,
12
+ DESKTOP_IMAGE_VERSION,
13
+ DESKTOP_IMAGE_VERSION_LABEL,
14
+ DESKTOP_LABEL,
15
+ IMAGE_LABEL,
16
+ WORKSPACE_LABEL,
17
+ DockerBackend,
18
+ DockerBackendConfig,
19
+ )
20
+ from kilntainers.desktop import animate_file_operation
21
+ from kilntainers.desktop_control import (
22
+ accessibility_snapshot,
23
+ capture_screen,
24
+ desktop_action,
25
+ visible_terminal_execute,
26
+ )
27
+ from kilntainers.errors import BackendError
28
+ from kilntainers.file_tools import list_directory, read_text_file, write_text_file
29
+
30
+
31
+ def test_desktop_image_owns_complete_user_local_prefix() -> None:
32
+ """User-scoped installers must be able to create commands below ~/.local."""
33
+ image_files = files("kilntainers").joinpath("desktop_image")
34
+ dockerfile = image_files.joinpath("Dockerfile").read_text(encoding="utf-8")
35
+ entrypoint = image_files.joinpath("start-desktop.sh").read_text(encoding="utf-8")
36
+
37
+ assert "computer -g computer /home/computer/.local \\" in dockerfile
38
+ assert "computer -g computer /home/computer/.local/bin \\" in dockerfile
39
+ assert "/home/computer/.local/bin \\" in entrypoint
40
+ assert (
41
+ f'LABEL {DESKTOP_IMAGE_VERSION_LABEL}="{DESKTOP_IMAGE_VERSION}"' in dockerfile
42
+ )
43
+
44
+
45
+ def test_headless_computer_is_persistent_and_uses_workspace() -> None:
46
+ backend = DockerBackend(DockerBackendConfig(network_enabled=True))
47
+ command = backend._build_run_command(computer_id="desk", temporary=False)
48
+
49
+ assert "--rm" not in command
50
+ assert command[-4:] == ["debian:bookworm-slim", "tail", "-f", "/dev/null"]
51
+ assert command[command.index("--workdir") + 1] == "/workspace"
52
+ assert "127.0.0.1::6080" not in command
53
+
54
+
55
+ def test_desktop_capable_computer_can_start_with_xfce_disabled() -> None:
56
+ backend = DockerBackend(
57
+ DockerBackendConfig(
58
+ image=DEFAULT_DESKTOP_IMAGE,
59
+ desktop_environment=False,
60
+ network_enabled=True,
61
+ )
62
+ )
63
+ command = backend._build_run_command(computer_id="desk", temporary=False)
64
+
65
+ assert command[-1] == DEFAULT_DESKTOP_IMAGE
66
+ assert "tail" not in command
67
+ assert "--init" in command
68
+ assert command[command.index("--publish") + 1] == "127.0.0.1::6080"
69
+ assert "DESKTOP_ENVIRONMENT=false" in command
70
+ assert f"{DESKTOP_LABEL}=false" in command
71
+
72
+
73
+ def test_desktop_computer_publishes_only_loopback_websocket() -> None:
74
+ backend = DockerBackend(
75
+ DockerBackendConfig(
76
+ image=DEFAULT_DESKTOP_IMAGE,
77
+ desktop_environment=True,
78
+ network_enabled=True,
79
+ )
80
+ )
81
+ command = backend._build_run_command(computer_id="desk", temporary=False)
82
+
83
+ assert command[-1] == DEFAULT_DESKTOP_IMAGE
84
+ assert "tail" not in command
85
+ assert command[command.index("--publish") + 1] == "127.0.0.1::6080"
86
+ assert command[command.index("--shm-size") + 1] == "256m"
87
+ assert "DESKTOP_ENVIRONMENT=true" in command
88
+ assert f"{DESKTOP_LABEL}=true" in command
89
+
90
+
91
+ async def test_desktop_no_network_keeps_local_screen_and_requests_firewall() -> None:
92
+ backend = DockerBackend(
93
+ DockerBackendConfig(
94
+ image=DEFAULT_DESKTOP_IMAGE,
95
+ desktop_environment=True,
96
+ network_enabled=False,
97
+ )
98
+ )
99
+
100
+ backend._validated = True
101
+ command = backend._build_run_command(computer_id="desk", temporary=False)
102
+
103
+ assert "--network" not in command
104
+ assert command[command.index("--cap-add") + 1] == "NET_ADMIN"
105
+ assert "NETWORK_ACCESS=false" in command
106
+
107
+
108
+ def test_xfce_disabled_no_network_keeps_local_transport_for_runtime_switch() -> None:
109
+ backend = DockerBackend(
110
+ DockerBackendConfig(
111
+ image=DEFAULT_DESKTOP_IMAGE,
112
+ desktop_environment=False,
113
+ network_enabled=False,
114
+ )
115
+ )
116
+ command = backend._build_run_command(computer_id="desk", temporary=False)
117
+
118
+ assert "--network" not in command
119
+ assert "127.0.0.1::6080" in command
120
+ assert "NETWORK_ACCESS=false" in command
121
+ assert "DESKTOP_ENVIRONMENT=false" in command
122
+
123
+
124
+ def test_attached_desktop_exposes_browser_websocket_url() -> None:
125
+ backend = DockerBackend(DockerBackendConfig(desktop_environment=True))
126
+ sandbox = backend._sandbox_from_inspect(
127
+ {
128
+ "Id": "a" * 64,
129
+ "Config": {
130
+ "Labels": {
131
+ "kilntainers.computer-id": "desk",
132
+ "kilntainers.temporary": "false",
133
+ IMAGE_LABEL: DEFAULT_DESKTOP_IMAGE,
134
+ DESKTOP_LABEL: "true",
135
+ WORKSPACE_LABEL: "/workspace",
136
+ }
137
+ },
138
+ "NetworkSettings": {
139
+ "Ports": {"6080/tcp": [{"HostIp": "127.0.0.1", "HostPort": "49153"}]}
140
+ },
141
+ }
142
+ )
143
+
144
+ assert sandbox.computer_id == "desk"
145
+ assert sandbox.temporary is False
146
+ assert sandbox.desktop_url == "ws://127.0.0.1:49153/websockify"
147
+
148
+
149
+ async def test_legacy_headless_computer_is_not_replaced_to_enable_xfce() -> None:
150
+ backend = DockerBackend(DockerBackendConfig())
151
+ sandbox = backend._sandbox_from_inspect(
152
+ {
153
+ "Id": "b" * 64,
154
+ "Config": {
155
+ "Labels": {
156
+ "kilntainers.computer-id": "legacy",
157
+ "kilntainers.temporary": "false",
158
+ IMAGE_LABEL: "debian:bookworm-slim",
159
+ DESKTOP_LABEL: "false",
160
+ WORKSPACE_LABEL: "/workspace",
161
+ }
162
+ },
163
+ "NetworkSettings": {"Ports": {}, "Networks": {"bridge": {}}},
164
+ }
165
+ )
166
+
167
+ with pytest.raises(BackendError, match="data was left untouched"):
168
+ await backend._set_desktop_mode(sandbox, True)
169
+
170
+
171
+ @pytest.mark.integration
172
+ async def test_real_xfce_desktop_replays_and_preserves_file_write() -> None:
173
+ """Build/run the live desktop and verify real UI replay plus reattachment."""
174
+ computer_id = f"virtual-computer-e2e-{os.getpid()}"
175
+ backend = DockerBackend(
176
+ DockerBackendConfig(
177
+ image=DEFAULT_DESKTOP_IMAGE,
178
+ desktop_environment=True,
179
+ network_enabled=True,
180
+ )
181
+ )
182
+ sandbox = await backend.create_sandbox(
183
+ computer_id=computer_id,
184
+ temporary=False,
185
+ )
186
+ try:
187
+ assert sandbox.desktop_url is not None
188
+ screen = await capture_screen(sandbox)
189
+ assert screen.startswith(b"\x89PNG\r\n\x1a\n")
190
+ snapshot = await accessibility_snapshot(sandbox)
191
+ assert snapshot["format"] == "at-spi-snapshot-v1"
192
+ assert snapshot["elements"]
193
+ terminal = await visible_terminal_execute(
194
+ sandbox,
195
+ command="printf 'visible terminal e2e\\n'",
196
+ args=None,
197
+ stdin=None,
198
+ working_directory="/workspace",
199
+ timeout=30,
200
+ output_limit=131_072,
201
+ )
202
+ assert terminal.stdout == "visible terminal e2e\n"
203
+ assert terminal.exit_code == 0
204
+ windows = await desktop_action(sandbox, "list_windows")
205
+ assert any("MCP Terminal" in window["title"] for window in windows["windows"])
206
+ saved = await write_text_file(
207
+ sandbox,
208
+ "desktop-e2e.txt",
209
+ "written through the real desktop\n",
210
+ workspace_directory="/workspace",
211
+ timeout=30,
212
+ output_limit=131_072,
213
+ text_limit=131_072,
214
+ )
215
+ warning = await animate_file_operation(
216
+ sandbox,
217
+ operation="write_file",
218
+ path=saved.path,
219
+ content=saved.content,
220
+ original_content=None,
221
+ old_text=None,
222
+ new_text=None,
223
+ replace_all=False,
224
+ terminal_was_last=False,
225
+ workspace_directory="/workspace",
226
+ )
227
+ assert warning is None
228
+ actual = await read_text_file(
229
+ sandbox,
230
+ saved.path,
231
+ workspace_directory="/workspace",
232
+ timeout=30,
233
+ output_limit=131_072,
234
+ text_limit=131_072,
235
+ )
236
+ assert actual.content == saved.content
237
+ listing = await list_directory(
238
+ sandbox,
239
+ "/workspace",
240
+ workspace_directory="/workspace",
241
+ timeout=30,
242
+ output_limit=131_072,
243
+ )
244
+ assert any(
245
+ entry.name == "desktop-e2e.txt" and entry.kind == "file"
246
+ for entry in listing.entries
247
+ )
248
+
249
+ attached = await backend.attach_sandbox(computer_id)
250
+ assert attached is not None
251
+ assert attached.computer_id == computer_id
252
+ assert attached.desktop_url == sandbox.desktop_url
253
+ finally:
254
+ await backend.delete_computer(computer_id)
255
+
256
+
257
+ @pytest.mark.integration
258
+ async def test_runtime_switches_preserve_the_full_container() -> None:
259
+ """Exercise dashboard toggles without replacing the Docker computer."""
260
+ computer_id = f"virtual-computer-toggle-e2e-{os.getpid()}"
261
+ backend = DockerBackend(
262
+ DockerBackendConfig(
263
+ image=DEFAULT_DESKTOP_IMAGE,
264
+ desktop_environment=False,
265
+ network_enabled=True,
266
+ )
267
+ )
268
+ sandbox = await backend.create_sandbox(computer_id=computer_id, temporary=False)
269
+ try:
270
+ original_container_id = sandbox.sandbox_id
271
+ assert sandbox.desktop_environment is False
272
+ assert sandbox.desktop_url is None
273
+ await write_text_file(
274
+ sandbox,
275
+ "toggle-preserved.txt",
276
+ "workspace survives both mug faces\n",
277
+ workspace_directory="/workspace",
278
+ timeout=30,
279
+ output_limit=131_072,
280
+ text_limit=131_072,
281
+ )
282
+ outside_workspace = await sandbox.exec(
283
+ ExecRequest(
284
+ command="mkdir -p /opt/mcp-test && printf persistent > /opt/mcp-test/mode",
285
+ timeout=10,
286
+ output_limit=131_072,
287
+ )
288
+ )
289
+ assert outside_workspace.exit_code == 0
290
+
291
+ desktop = await backend.switch_desktop_environment(computer_id, True)
292
+ assert desktop is not None
293
+ assert desktop.sandbox_id == original_container_id
294
+ assert desktop.desktop_environment is True
295
+ assert desktop.desktop_url is not None
296
+ assert (await capture_screen(desktop)).startswith(b"\x89PNG\r\n\x1a\n")
297
+
298
+ isolated = await backend.set_network_access(computer_id, False)
299
+ assert isolated is not None
300
+ assert isolated.network_access is False
301
+ assert isolated.desktop_url is not None
302
+ blocked = await isolated.exec(
303
+ ExecRequest(
304
+ command=(
305
+ 'python3 -c "import socket; '
306
+ "socket.getaddrinfo('deb.debian.org', 443); print('online')\""
307
+ ),
308
+ timeout=10,
309
+ output_limit=131_072,
310
+ )
311
+ )
312
+ assert blocked.exit_code != 0
313
+
314
+ online = await backend.set_network_access(computer_id, True)
315
+ assert online is not None
316
+ reachable = await online.exec(
317
+ ExecRequest(
318
+ command=(
319
+ 'python3 -c "import socket; '
320
+ "socket.getaddrinfo('deb.debian.org', 443); print('online')\""
321
+ ),
322
+ timeout=10,
323
+ output_limit=131_072,
324
+ )
325
+ )
326
+ assert reachable.exit_code == 0
327
+ assert reachable.stdout == "online\n"
328
+
329
+ headless = await backend.switch_desktop_environment(computer_id, False)
330
+ assert headless is not None
331
+ assert headless.sandbox_id == original_container_id
332
+ assert headless.desktop_environment is False
333
+ assert headless.desktop_url is None
334
+ preserved = await read_text_file(
335
+ headless,
336
+ "toggle-preserved.txt",
337
+ workspace_directory="/workspace",
338
+ timeout=30,
339
+ output_limit=131_072,
340
+ text_limit=131_072,
341
+ )
342
+ assert preserved.content == "workspace survives both mug faces\n"
343
+ persistent = await headless.exec(
344
+ ExecRequest(
345
+ command="cat /opt/mcp-test/mode",
346
+ timeout=10,
347
+ output_limit=131_072,
348
+ )
349
+ )
350
+ assert persistent.exit_code == 0
351
+ assert persistent.stdout == "persistent"
352
+
353
+ disconnected = await backend.set_network_access(computer_id, False)
354
+ assert disconnected is not None
355
+ assert disconnected.network_access is False
356
+ inspect = await backend._inspect_computer(computer_id)
357
+ assert inspect is not None
358
+ network_settings = inspect["NetworkSettings"]
359
+ assert isinstance(network_settings, dict)
360
+ network_settings = cast("dict[str, object]", network_settings)
361
+ networks = network_settings.get("Networks")
362
+ assert isinstance(networks, dict)
363
+ assert "none" not in networks
364
+
365
+ restored = await backend.switch_desktop_environment(computer_id, True)
366
+ assert restored is not None
367
+ assert restored.sandbox_id == original_container_id
368
+ assert restored.desktop_url is not None
369
+ healed_vnc = await restored.exec(
370
+ ExecRequest(
371
+ command=(
372
+ "old=$(pgrep -o x11vnc) || exit 1; kill \"$old\"; i=0; "
373
+ "while [ $i -lt 50 ]; do i=$((i + 1)); "
374
+ "new=$(pgrep -o x11vnc 2>/dev/null || true); "
375
+ "if [ -n \"$new\" ] && [ \"$new\" != \"$old\" ]; then "
376
+ "printf '%s->%s\\n' \"$old\" \"$new\"; exit 0; fi; "
377
+ "sleep 0.2; done; exit 1"
378
+ ),
379
+ timeout=15,
380
+ output_limit=131_072,
381
+ )
382
+ )
383
+ assert healed_vnc.exit_code == 0
384
+ assert "->" in healed_vnc.stdout
385
+ assert (await capture_screen(restored)).startswith(b"\x89PNG\r\n\x1a\n")
386
+ finally:
387
+ await backend.delete_computer(computer_id)