aisolate-client 0.3.0__tar.gz → 0.3.2__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.
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/PKG-INFO +2 -2
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/README.md +1 -1
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/pyproject.toml +1 -1
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/exception.py +26 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/sandbox.py +18 -0
- aisolate_client-0.3.2/src/aisolate/client/tests/integration/conftest.py +24 -0
- aisolate_client-0.3.2/src/aisolate/common/output_window.py +58 -0
- aisolate_client-0.3.2/src/aisolate/common/tests/fake_vm.py +118 -0
- aisolate_client-0.3.2/src/aisolate/common/tests/test_output_window.py +74 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/PKG-INFO +2 -2
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/SOURCES.txt +3 -0
- aisolate_client-0.3.0/src/aisolate/client/tests/integration/conftest.py +0 -101
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/setup.cfg +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/gateway_client.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/models.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/remote_executer.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/sandbox_manager.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/test_gateway_client.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/test_sandbox.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/test_gateway_client_demux.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/test_sandbox_workspace_prefix.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/codec.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/errors.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/models.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/__init__.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/fixtures.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/test_codec_channel.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/utils.py +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/dependency_links.txt +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/requires.txt +0 -0
- {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aisolate-client
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Client library for sandbox environments
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -16,7 +16,7 @@ Client library for sandbox environments
|
|
|
16
16
|
## Local Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
pip install path/to/aisolate_client-0.3.
|
|
19
|
+
pip install path/to/aisolate_client-0.3.2-py3-none-any.whl
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
@@ -48,3 +48,29 @@ class SandboxError(Exception):
|
|
|
48
48
|
"""Raised for any errors related to the Sandbox lifecycle or execution."""
|
|
49
49
|
|
|
50
50
|
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_infrastructure_fault(exc: BaseException) -> bool:
|
|
54
|
+
"""True when a sandbox call failed because the sandbox itself is broken —
|
|
55
|
+
VM gone, websocket dropped, gateway 5xx, or a request timeout — rather than
|
|
56
|
+
because executed user code raised.
|
|
57
|
+
|
|
58
|
+
Executed user-code errors come back as kernel *results* (the kernel captures
|
|
59
|
+
the traceback), never as these exceptions, so a caught ``Sandbox*Error`` is
|
|
60
|
+
always an infrastructure fault the run can recover from by re-provisioning a
|
|
61
|
+
fresh VM. A gateway 4xx (client misuse, e.g. a bad request) is deliberately
|
|
62
|
+
excluded — re-provisioning would not help.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(
|
|
65
|
+
exc,
|
|
66
|
+
(
|
|
67
|
+
SandboxConnectionError,
|
|
68
|
+
SandboxTimeoutError,
|
|
69
|
+
SandboxWebSocketError,
|
|
70
|
+
SandboxError,
|
|
71
|
+
),
|
|
72
|
+
):
|
|
73
|
+
return True
|
|
74
|
+
if isinstance(exc, SandboxAPIError):
|
|
75
|
+
return exc.status_code is None or exc.status_code >= 500
|
|
76
|
+
return False
|
|
@@ -222,6 +222,24 @@ class Sandbox:
|
|
|
222
222
|
"""Async context manager exit point. Closes the sandbox."""
|
|
223
223
|
await self._async_close(exc_type, exc, tb)
|
|
224
224
|
|
|
225
|
+
async def areset(self) -> "Sandbox":
|
|
226
|
+
"""Force a fresh VM under the same id: tear the current sandbox down
|
|
227
|
+
(best-effort — the VM may already be gone server-side) and start a new
|
|
228
|
+
one.
|
|
229
|
+
|
|
230
|
+
The recovery path when the gateway reports the VM lost (guest
|
|
231
|
+
crash/OOM). A plain ``astart()`` cannot do this: it no-ops while
|
|
232
|
+
``_is_active`` is set, and that client-side flag cannot observe a
|
|
233
|
+
server-side VM death. Dropping ``_client`` forces ``_async_start`` to
|
|
234
|
+
build a fresh connection rather than reuse the broken one.
|
|
235
|
+
"""
|
|
236
|
+
with contextlib.suppress(Exception):
|
|
237
|
+
await self._async_close(None, None, None)
|
|
238
|
+
self._is_active = False
|
|
239
|
+
self._client = None
|
|
240
|
+
await self._async_start()
|
|
241
|
+
return self
|
|
242
|
+
|
|
225
243
|
def run_code(
|
|
226
244
|
self,
|
|
227
245
|
code: str,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
import pytest_asyncio
|
|
4
|
+
|
|
5
|
+
# The driver lives in the shared test-support module so the agent e2e
|
|
6
|
+
# harness can use the same no-VM wiring.
|
|
7
|
+
from aisolate.common.tests.fake_vm import FakeDriver
|
|
8
|
+
|
|
9
|
+
# Import fixtures from common fixtures module
|
|
10
|
+
from aisolate.common.tests.fixtures import *
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest_asyncio.fixture(autouse=True)
|
|
14
|
+
async def _inject_fake_vm_driver(kernel_gateway_proc):
|
|
15
|
+
"""Set the driver override BEFORE gateway_test_client enters the app
|
|
16
|
+
lifespan (autouse fixtures at the same scope run first), clear it and
|
|
17
|
+
close the fake sandbox stacks after the client has torn down."""
|
|
18
|
+
gateway_app = importlib.import_module("aisolate.gateway.app.main").app
|
|
19
|
+
driver = FakeDriver()
|
|
20
|
+
gateway_app.state.vm_driver_override = driver
|
|
21
|
+
yield
|
|
22
|
+
del gateway_app.state.vm_driver_override
|
|
23
|
+
for stack in driver.stacks:
|
|
24
|
+
await stack.aclose()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The one honest output window, shared by every execution lane.
|
|
2
|
+
|
|
3
|
+
``window_stream`` is the single windowing policy for observation channels
|
|
4
|
+
(shell stdout/stderr, jupyter streams and expression results): output at or
|
|
5
|
+
under the budget passes through BYTE-IDENTICAL; anything larger keeps head
|
|
6
|
+
AND tail and carries one explicit footer in the ``[TRUNCATED …]`` marker
|
|
7
|
+
family the agent prompts promise ("truncation is always explicit").
|
|
8
|
+
|
|
9
|
+
This module is imported by the sandbox guest (``ShellExecutor``) and by the
|
|
10
|
+
agent host (``RemotePythonExecutor``) — imports must stay stdlib-only, like
|
|
11
|
+
``codec.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
#: Default window per stream/field, in characters (~1.5k tokens). Chosen for
|
|
17
|
+
#: the post ``-ub 2048``/``--swa-full`` era: a complete 31-row table
|
|
18
|
+
#: (~2.4 KB) is a normal observation and must pass whole.
|
|
19
|
+
DEFAULT_OUTPUT_WINDOW = 6000
|
|
20
|
+
|
|
21
|
+
#: Env var that overrides the window — delivered via the context-creation
|
|
22
|
+
#: env (shell lane) and read at construction (jupyter lane).
|
|
23
|
+
OUTPUT_WINDOW_ENV = "AISOLATE_OUTPUT_WINDOW"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def window_stream(
|
|
27
|
+
text: str,
|
|
28
|
+
*,
|
|
29
|
+
budget: int,
|
|
30
|
+
label: str,
|
|
31
|
+
spool_ref: str | None = None,
|
|
32
|
+
guidance: str = "",
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Window ``text`` to ``budget`` chars: head + footer + tail.
|
|
35
|
+
|
|
36
|
+
Returns ``text`` unchanged (byte-identical — prefix-cache stability of
|
|
37
|
+
the common case) when it fits the budget or the budget is unlimited
|
|
38
|
+
(``<= 0``). Otherwise keeps the first 2/3 and last 1/3 of the budget
|
|
39
|
+
around one footer line naming the stream, the total size, where the
|
|
40
|
+
full output lives (``spool_ref``), and what to do instead of re-running
|
|
41
|
+
(``guidance``).
|
|
42
|
+
"""
|
|
43
|
+
if not text or budget <= 0 or len(text) <= budget:
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
tail = budget // 3
|
|
47
|
+
head = budget - tail
|
|
48
|
+
|
|
49
|
+
footer = f"... [TRUNCATED {label} — {len(text):,} chars total"
|
|
50
|
+
if spool_ref:
|
|
51
|
+
footer += f"; full output: {spool_ref}"
|
|
52
|
+
if guidance:
|
|
53
|
+
footer += f"; {guidance}"
|
|
54
|
+
footer += "]"
|
|
55
|
+
|
|
56
|
+
return f"{text[:head]}\n{footer}\n{text[-tail:]}" if tail else (
|
|
57
|
+
f"{text[:head]}\n{footer}"
|
|
58
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""In-process stand-in for the VM layer — the no-firecracker test driver.
|
|
2
|
+
|
|
3
|
+
``FakeDriver`` implements the gateway's ``VMDriver`` protocol by booting the
|
|
4
|
+
REAL sandbox ASGI app in-process (lifespan and all — including its
|
|
5
|
+
DirectKernelExecutor python3 kernel) and handing the gateway a ``VMHandle``
|
|
6
|
+
whose session/WS are ASGI test clients. Injected through the
|
|
7
|
+
``app.state.vm_driver_override`` seam (``gateway/app/factory.py``): the
|
|
8
|
+
gateway lifespan constructs the real FirecrackerDriver only when no override
|
|
9
|
+
is present, so this fake is the app's one driver for the whole test —
|
|
10
|
+
including lifespan-shutdown teardown of the handles it created.
|
|
11
|
+
|
|
12
|
+
``process_match`` returns ``False`` for everything: nothing the fake creates
|
|
13
|
+
is a real VM process, so the dummy pid must never be signalled, sampled for
|
|
14
|
+
PSS, or reaped.
|
|
15
|
+
|
|
16
|
+
Shared home for the driver previously defined inline in
|
|
17
|
+
``client/tests/integration/conftest.py`` (which now imports from here), used
|
|
18
|
+
by the agent e2e harness (``agent/tests/test_e2e/``) as well.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import importlib
|
|
23
|
+
from contextlib import AsyncExitStack
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from types import SimpleNamespace
|
|
26
|
+
from urllib.parse import unquote
|
|
27
|
+
|
|
28
|
+
from asgi_lifespan import LifespanManager
|
|
29
|
+
|
|
30
|
+
from aisolate.common.tests.utils import WsAliasTestClient
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SingleEncodeTestClient(WsAliasTestClient):
|
|
34
|
+
"""Keep pre-quoted query strings single-encoded through the ASGI client.
|
|
35
|
+
|
|
36
|
+
``async_asgi_testclient`` re-quotes the query it splits off the URL
|
|
37
|
+
(``utils.make_test_headers_path_and_query_string``, ``safe="&="``).
|
|
38
|
+
The GatewayClient and the gateway's proxy routers bake PRE-QUOTED
|
|
39
|
+
queries into their URLs (the vsock session forwards method+url only,
|
|
40
|
+
so the query must ride the URL) — the re-quote turns ``%2F`` into
|
|
41
|
+
``%252F`` and workspace paths like ``<lineage>/task.md`` degrade into
|
|
42
|
+
literal ``<lineage>%2Ftask.md`` files. Un-quote once before dispatch
|
|
43
|
+
so the client's own re-quote restores the wire encoding.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
async def open(self, path, **kwargs):
|
|
47
|
+
base, sep, query = path.partition("?")
|
|
48
|
+
if sep and kwargs.get("query_string") is None:
|
|
49
|
+
path = f"{base}?{unquote(query)}"
|
|
50
|
+
return await super().open(path, **kwargs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DummyProcess(SimpleNamespace):
|
|
54
|
+
returncode = None
|
|
55
|
+
pid = 12345
|
|
56
|
+
|
|
57
|
+
async def wait(self):
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
def kill(self):
|
|
61
|
+
self.returncode = -9
|
|
62
|
+
|
|
63
|
+
def terminate(self):
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FakeDriver:
|
|
68
|
+
"""In-process VM 'driver': boots the sandbox ASGI app instead of a VM."""
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
# One stack per sandbox to avoid WebSocket reuse issues; closed
|
|
72
|
+
# by the injecting fixture after the gateway client has exited.
|
|
73
|
+
self.stacks = []
|
|
74
|
+
|
|
75
|
+
def process_match(self, cmdline: str) -> bool:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
async def create(self, sbx):
|
|
79
|
+
from aisolate.gateway.app.models.sandbox import (
|
|
80
|
+
SandboxConnection,
|
|
81
|
+
SocatProcess,
|
|
82
|
+
VMHandle,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
sandbox_app = importlib.import_module("aisolate.sandbox.app.main").app
|
|
86
|
+
|
|
87
|
+
stack = AsyncExitStack()
|
|
88
|
+
self.stacks.append(stack)
|
|
89
|
+
|
|
90
|
+
# bring up the ASGI sandbox app so WS paths exist
|
|
91
|
+
await stack.enter_async_context(LifespanManager(sandbox_app))
|
|
92
|
+
# get a test client that supports HTTP + WS
|
|
93
|
+
async_client = await stack.enter_async_context(
|
|
94
|
+
SingleEncodeTestClient(sandbox_app)
|
|
95
|
+
)
|
|
96
|
+
ws = await stack.enter_async_context(async_client.websocket_connect("/execute"))
|
|
97
|
+
|
|
98
|
+
conn = SandboxConnection(
|
|
99
|
+
session=async_client,
|
|
100
|
+
ws=ws,
|
|
101
|
+
base_url="http://sandbox.local",
|
|
102
|
+
)
|
|
103
|
+
socat = SocatProcess(
|
|
104
|
+
process=DummyProcess(),
|
|
105
|
+
socket_path=Path("/tmp/dummy.sock"),
|
|
106
|
+
vsock_port=1080,
|
|
107
|
+
protocol="TCP",
|
|
108
|
+
remote_host="127.0.0.1",
|
|
109
|
+
remote_port=1080,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return VMHandle(
|
|
113
|
+
sandbox=sbx,
|
|
114
|
+
vm_proc=DummyProcess(),
|
|
115
|
+
conn=conn,
|
|
116
|
+
socat=socat,
|
|
117
|
+
log_forwarder_task=asyncio.create_task(asyncio.sleep(0)), # dummy task
|
|
118
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Unit tests for the shared output window policy."""
|
|
2
|
+
|
|
3
|
+
from aisolate.common.output_window import window_stream
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestWindowStream:
|
|
7
|
+
def test_under_budget_is_byte_identical(self):
|
|
8
|
+
text = "a" * 100
|
|
9
|
+
assert window_stream(text, budget=100, label="stdout") is text
|
|
10
|
+
|
|
11
|
+
def test_at_budget_boundary_passes_unchanged(self):
|
|
12
|
+
text = "x" * 6000
|
|
13
|
+
assert window_stream(text, budget=6000, label="stdout") == text
|
|
14
|
+
|
|
15
|
+
def test_over_budget_keeps_head_and_tail(self):
|
|
16
|
+
text = "H" * 4000 + "M" * 50000 + "T" * 2000
|
|
17
|
+
out = window_stream(text, budget=6000, label="stdout")
|
|
18
|
+
assert out.startswith("H" * 4000)
|
|
19
|
+
assert out.endswith("T" * 2000)
|
|
20
|
+
assert "M" * 100 not in out
|
|
21
|
+
|
|
22
|
+
def test_footer_names_stream_and_total(self):
|
|
23
|
+
text = "y" * 51_340
|
|
24
|
+
out = window_stream(text, budget=6000, label="stdout")
|
|
25
|
+
assert "[TRUNCATED stdout — 51,340 chars total]" in out
|
|
26
|
+
|
|
27
|
+
def test_footer_carries_spool_ref_and_guidance(self):
|
|
28
|
+
out = window_stream(
|
|
29
|
+
"z" * 10_000,
|
|
30
|
+
budget=600,
|
|
31
|
+
label="stdout",
|
|
32
|
+
spool_ref=".aisolate/out/7.stdout",
|
|
33
|
+
guidance="view a window: sed -n '1,120p' .aisolate/out/7.stdout",
|
|
34
|
+
)
|
|
35
|
+
assert "full output: .aisolate/out/7.stdout" in out
|
|
36
|
+
assert "sed -n '1,120p'" in out
|
|
37
|
+
assert out.count("[TRUNCATED") == 1
|
|
38
|
+
|
|
39
|
+
def test_guidance_without_spool(self):
|
|
40
|
+
out = window_stream(
|
|
41
|
+
"z" * 10_000,
|
|
42
|
+
budget=600,
|
|
43
|
+
label="stdout",
|
|
44
|
+
guidance="the kernel still holds your data",
|
|
45
|
+
)
|
|
46
|
+
assert "the kernel still holds your data" in out
|
|
47
|
+
assert "full output:" not in out
|
|
48
|
+
|
|
49
|
+
def test_head_tail_split_is_two_thirds_one_third(self):
|
|
50
|
+
text = "".join(str(i % 10) for i in range(20_000))
|
|
51
|
+
out = window_stream(text, budget=6000, label="stderr")
|
|
52
|
+
head, _, rest = out.partition("\n... [TRUNCATED")
|
|
53
|
+
tail = rest.partition("]\n")[2]
|
|
54
|
+
assert head == text[:4000]
|
|
55
|
+
assert tail == text[-2000:]
|
|
56
|
+
|
|
57
|
+
def test_zero_or_negative_budget_disables_windowing(self):
|
|
58
|
+
text = "q" * 10_000
|
|
59
|
+
assert window_stream(text, budget=0, label="stdout") == text
|
|
60
|
+
assert window_stream(text, budget=-1, label="stdout") == text
|
|
61
|
+
|
|
62
|
+
def test_empty_text_passes(self):
|
|
63
|
+
assert window_stream("", budget=100, label="stdout") == ""
|
|
64
|
+
|
|
65
|
+
def test_tiny_budget_still_has_head(self):
|
|
66
|
+
out = window_stream("abcdefghij" * 10, budget=2, label="stdout")
|
|
67
|
+
assert out.startswith("ab")
|
|
68
|
+
assert "[TRUNCATED stdout" in out
|
|
69
|
+
|
|
70
|
+
def test_unicode_slicing_is_char_safe(self):
|
|
71
|
+
text = "ä" * 10_000
|
|
72
|
+
out = window_stream(text, budget=600, label="stdout")
|
|
73
|
+
assert out.startswith("ä" * 400)
|
|
74
|
+
assert out.endswith("ä" * 200)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aisolate-client
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Client library for sandbox environments
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -16,7 +16,7 @@ Client library for sandbox environments
|
|
|
16
16
|
## Local Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
pip install path/to/aisolate_client-0.3.
|
|
19
|
+
pip install path/to/aisolate_client-0.3.2-py3-none-any.whl
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
@@ -19,9 +19,12 @@ src/aisolate/common/__init__.py
|
|
|
19
19
|
src/aisolate/common/codec.py
|
|
20
20
|
src/aisolate/common/errors.py
|
|
21
21
|
src/aisolate/common/models.py
|
|
22
|
+
src/aisolate/common/output_window.py
|
|
22
23
|
src/aisolate/common/tests/__init__.py
|
|
24
|
+
src/aisolate/common/tests/fake_vm.py
|
|
23
25
|
src/aisolate/common/tests/fixtures.py
|
|
24
26
|
src/aisolate/common/tests/test_codec_channel.py
|
|
27
|
+
src/aisolate/common/tests/test_output_window.py
|
|
25
28
|
src/aisolate/common/tests/utils.py
|
|
26
29
|
src/aisolate_client.egg-info/PKG-INFO
|
|
27
30
|
src/aisolate_client.egg-info/SOURCES.txt
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import importlib
|
|
3
|
-
from contextlib import AsyncExitStack
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from types import SimpleNamespace
|
|
6
|
-
|
|
7
|
-
import pytest_asyncio
|
|
8
|
-
from asgi_lifespan import LifespanManager
|
|
9
|
-
from aisolate.common.tests.utils import WsAliasTestClient
|
|
10
|
-
|
|
11
|
-
# Import fixtures from common fixtures module
|
|
12
|
-
from aisolate.common.tests.fixtures import *
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class _DummyProcess(SimpleNamespace):
|
|
16
|
-
returncode = None
|
|
17
|
-
pid = 12345
|
|
18
|
-
|
|
19
|
-
async def wait(self):
|
|
20
|
-
return 0
|
|
21
|
-
|
|
22
|
-
def kill(self):
|
|
23
|
-
self.returncode = -9
|
|
24
|
-
|
|
25
|
-
def terminate(self):
|
|
26
|
-
return 0
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class FakeDriver:
|
|
30
|
-
"""In-process VM 'driver': boots the sandbox ASGI app instead of a VM.
|
|
31
|
-
|
|
32
|
-
Injected through the ``app.state.vm_driver_override`` seam
|
|
33
|
-
(vmm-driver.plan.md WI-5): the gateway lifespan constructs the real
|
|
34
|
-
FirecrackerDriver only when no override is present, so this fake is
|
|
35
|
-
the app's one driver for the whole test — including lifespan-shutdown
|
|
36
|
-
teardown of the handles it created.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self):
|
|
40
|
-
# One stack per sandbox to avoid WebSocket reuse issues; closed
|
|
41
|
-
# by the injecting fixture after the gateway client has exited.
|
|
42
|
-
self.stacks = []
|
|
43
|
-
|
|
44
|
-
def process_match(self, cmdline: str) -> bool:
|
|
45
|
-
# Nothing the fake creates is a real VM process: the dummy pid
|
|
46
|
-
# (12345) must never be signalled, sampled for PSS, or reaped.
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
async def create(self, sbx):
|
|
50
|
-
from aisolate.gateway.app.models.sandbox import (
|
|
51
|
-
SandboxConnection,
|
|
52
|
-
SocatProcess,
|
|
53
|
-
VMHandle,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
sandbox_app = importlib.import_module("aisolate.sandbox.app.main").app
|
|
57
|
-
|
|
58
|
-
stack = AsyncExitStack()
|
|
59
|
-
self.stacks.append(stack)
|
|
60
|
-
|
|
61
|
-
# bring up the ASGI sandbox app so WS paths exist
|
|
62
|
-
await stack.enter_async_context(LifespanManager(sandbox_app))
|
|
63
|
-
# get a test client that supports HTTP + WS
|
|
64
|
-
async_client = await stack.enter_async_context(WsAliasTestClient(sandbox_app))
|
|
65
|
-
ws = await stack.enter_async_context(async_client.websocket_connect("/execute"))
|
|
66
|
-
|
|
67
|
-
conn = SandboxConnection(
|
|
68
|
-
session=async_client,
|
|
69
|
-
ws=ws,
|
|
70
|
-
base_url="http://sandbox.local",
|
|
71
|
-
)
|
|
72
|
-
socat = SocatProcess(
|
|
73
|
-
process=_DummyProcess(),
|
|
74
|
-
socket_path=Path("/tmp/dummy.sock"),
|
|
75
|
-
vsock_port=1080,
|
|
76
|
-
protocol="TCP",
|
|
77
|
-
remote_host="127.0.0.1",
|
|
78
|
-
remote_port=1080,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
return VMHandle(
|
|
82
|
-
sandbox=sbx,
|
|
83
|
-
vm_proc=_DummyProcess(),
|
|
84
|
-
conn=conn,
|
|
85
|
-
socat=socat,
|
|
86
|
-
log_forwarder_task=asyncio.create_task(asyncio.sleep(0)), # dummy task
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@pytest_asyncio.fixture(autouse=True)
|
|
91
|
-
async def _inject_fake_vm_driver(kernel_gateway_proc):
|
|
92
|
-
"""Set the driver override BEFORE gateway_test_client enters the app
|
|
93
|
-
lifespan (autouse fixtures at the same scope run first), clear it and
|
|
94
|
-
close the fake sandbox stacks after the client has torn down."""
|
|
95
|
-
gateway_app = importlib.import_module("aisolate.gateway.app.main").app
|
|
96
|
-
driver = FakeDriver()
|
|
97
|
-
gateway_app.state.vm_driver_override = driver
|
|
98
|
-
yield
|
|
99
|
-
del gateway_app.state.vm_driver_override
|
|
100
|
-
for stack in driver.stacks:
|
|
101
|
-
await stack.aclose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/test_codec_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
{aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|