agentix-runtime-basic 0.1.1__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.
- agentix/bash/__init__.py +260 -0
- agentix/files/__init__.py +145 -0
- agentix_runtime_basic-0.1.1.dist-info/METADATA +69 -0
- agentix_runtime_basic-0.1.1.dist-info/RECORD +7 -0
- agentix_runtime_basic-0.1.1.dist-info/WHEEL +4 -0
- agentix_runtime_basic-0.1.1.dist-info/entry_points.txt +3 -0
- agentix_runtime_basic-0.1.1.dist-info/licenses/LICENSE +21 -0
agentix/bash/__init__.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Bash primitive — shell command execution as an Agentix namespace.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
from agentix import RuntimeClient
|
|
6
|
+
from agentix import bash
|
|
7
|
+
from agentix.bash import BashStdout, BashStderr, BashExit, BashError
|
|
8
|
+
|
|
9
|
+
async with RuntimeClient(sandbox.runtime_url) as c:
|
|
10
|
+
r = await c.remote(bash.run, command="ls -la", cwd="/workspace")
|
|
11
|
+
print(r.exit_code, r.stdout)
|
|
12
|
+
|
|
13
|
+
async for ev in c.remote(bash.run_stream, command="long-job.sh"):
|
|
14
|
+
match ev:
|
|
15
|
+
case BashStdout(data=chunk): print(chunk, end="")
|
|
16
|
+
case BashStderr(data=chunk): print(chunk, end="")
|
|
17
|
+
case BashExit(exit_code=code): print(f"\\nexit {code}")
|
|
18
|
+
case BashError(message=msg): print(f"\\nerror: {msg}")
|
|
19
|
+
|
|
20
|
+
The package IS the namespace — `run` and `run_stream` are top-level
|
|
21
|
+
async functions, dataclasses (`BashResult`, `BashStdout`, …) coexist
|
|
22
|
+
as types callers can import. The framework's discovery picks the async
|
|
23
|
+
functions; types and constants are just regular Python imports.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
from collections.abc import AsyncIterator
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from typing import Annotated, Literal
|
|
33
|
+
|
|
34
|
+
from pydantic import Field
|
|
35
|
+
|
|
36
|
+
# Env vars stripped before forking a user-space subprocess. The runtime
|
|
37
|
+
# is a Nix-built binary; os.environ is pre-loaded with Nix runtime paths
|
|
38
|
+
# (LD_LIBRARY_PATH pointing at Nix-store libs, NIX_*, PYTHONPATH,
|
|
39
|
+
# FONTCONFIG_*). Leaking those into a host-image subprocess causes glibc
|
|
40
|
+
# ABI mismatches and silent library override bugs.
|
|
41
|
+
_RUNTIME_ONLY_ENV = {
|
|
42
|
+
"LD_LIBRARY_PATH",
|
|
43
|
+
"LD_PRELOAD",
|
|
44
|
+
"PYTHONPATH",
|
|
45
|
+
"PYTHONHOME",
|
|
46
|
+
"LOCALE_ARCHIVE",
|
|
47
|
+
"FONTCONFIG_FILE",
|
|
48
|
+
"FONTCONFIG_PATH",
|
|
49
|
+
"SSL_CERT_FILE",
|
|
50
|
+
"NIX_SSL_CERT_FILE",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _clean_env(extra: dict[str, str] | None) -> dict[str, str]:
|
|
55
|
+
"""Build a subprocess env: scrubbed base + caller overrides."""
|
|
56
|
+
env = {
|
|
57
|
+
k: v
|
|
58
|
+
for k, v in os.environ.items()
|
|
59
|
+
if k not in _RUNTIME_ONLY_ENV and not k.startswith("NIX_")
|
|
60
|
+
}
|
|
61
|
+
if extra:
|
|
62
|
+
env.update(extra)
|
|
63
|
+
return env
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _read_capped(stream: asyncio.StreamReader, limit: int) -> str:
|
|
67
|
+
"""Drain a subprocess stream, retaining at most `limit` bytes.
|
|
68
|
+
|
|
69
|
+
The drain-to-EOF part matters: if one pipe stops being read after its cap
|
|
70
|
+
is reached, the child can block forever writing to that full pipe while the
|
|
71
|
+
parent waits on the process.
|
|
72
|
+
"""
|
|
73
|
+
chunks: list[bytes] = []
|
|
74
|
+
total = 0
|
|
75
|
+
truncated = False
|
|
76
|
+
while True:
|
|
77
|
+
chunk = await stream.read(8192)
|
|
78
|
+
if not chunk:
|
|
79
|
+
break
|
|
80
|
+
remaining = limit - total
|
|
81
|
+
if remaining <= 0:
|
|
82
|
+
truncated = True
|
|
83
|
+
continue
|
|
84
|
+
if len(chunk) >= remaining:
|
|
85
|
+
chunks.append(chunk[:remaining])
|
|
86
|
+
truncated = len(chunk) > remaining
|
|
87
|
+
total = limit
|
|
88
|
+
continue
|
|
89
|
+
chunks.append(chunk)
|
|
90
|
+
total += len(chunk)
|
|
91
|
+
if truncated:
|
|
92
|
+
chunks.append(b"\n[truncated at %d bytes]" % limit)
|
|
93
|
+
return b"".join(chunks).decode(errors="replace")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class BashResult:
|
|
98
|
+
"""Return value of `Bash.run` — full output captured before the call returns."""
|
|
99
|
+
|
|
100
|
+
exit_code: int
|
|
101
|
+
stdout: str
|
|
102
|
+
stderr: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Algebraic stream events — each variant is its own dataclass so callers
|
|
106
|
+
# can `match event: case BashStdout(...)` and pyright tracks the type.
|
|
107
|
+
# The `type` field is the wire discriminator; users pattern-match the
|
|
108
|
+
# class, not the field.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class BashStdout:
|
|
113
|
+
"""A chunk of subprocess stdout."""
|
|
114
|
+
|
|
115
|
+
data: str
|
|
116
|
+
type: Literal["stdout"] = "stdout"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class BashStderr:
|
|
121
|
+
"""A chunk of subprocess stderr."""
|
|
122
|
+
|
|
123
|
+
data: str
|
|
124
|
+
type: Literal["stderr"] = "stderr"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class BashExit:
|
|
129
|
+
"""The subprocess finished. `exit_code` is its return status."""
|
|
130
|
+
|
|
131
|
+
exit_code: int
|
|
132
|
+
type: Literal["exit"] = "exit"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class BashError:
|
|
137
|
+
"""Wire-side problem (e.g. timeout, fork failure). `message` explains."""
|
|
138
|
+
|
|
139
|
+
message: str
|
|
140
|
+
type: Literal["error"] = "error"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
BashEvent = Annotated[
|
|
144
|
+
BashStdout | BashStderr | BashExit | BashError,
|
|
145
|
+
Field(discriminator="type"),
|
|
146
|
+
]
|
|
147
|
+
"""One event from `Bash.run_stream`. Discriminated union of the four
|
|
148
|
+
variants above — JSON wire form carries a `type` tag, but in Python
|
|
149
|
+
the user pattern-matches the class directly."""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def run(
|
|
153
|
+
command: str,
|
|
154
|
+
cwd: str | None = None,
|
|
155
|
+
env: dict[str, str] | None = None,
|
|
156
|
+
timeout: float | None = None,
|
|
157
|
+
max_output: int = 10 * 1024 * 1024,
|
|
158
|
+
) -> BashResult:
|
|
159
|
+
"""Run a shell command in the sandbox and return its captured output."""
|
|
160
|
+
sub_env = _clean_env(env)
|
|
161
|
+
proc = await asyncio.create_subprocess_shell(
|
|
162
|
+
command,
|
|
163
|
+
stdout=asyncio.subprocess.PIPE,
|
|
164
|
+
stderr=asyncio.subprocess.PIPE,
|
|
165
|
+
cwd=cwd,
|
|
166
|
+
env=sub_env,
|
|
167
|
+
)
|
|
168
|
+
assert proc.stdout is not None and proc.stderr is not None
|
|
169
|
+
stdout_task = asyncio.create_task(_read_capped(proc.stdout, max_output))
|
|
170
|
+
stderr_task = asyncio.create_task(_read_capped(proc.stderr, max_output))
|
|
171
|
+
wait_task = asyncio.create_task(proc.wait())
|
|
172
|
+
try:
|
|
173
|
+
await asyncio.wait_for(
|
|
174
|
+
asyncio.gather(stdout_task, stderr_task, wait_task),
|
|
175
|
+
timeout=timeout,
|
|
176
|
+
)
|
|
177
|
+
except TimeoutError:
|
|
178
|
+
proc.kill()
|
|
179
|
+
await proc.wait()
|
|
180
|
+
for task in (stdout_task, stderr_task):
|
|
181
|
+
task.cancel()
|
|
182
|
+
await asyncio.gather(stdout_task, stderr_task, return_exceptions=True)
|
|
183
|
+
return BashResult(
|
|
184
|
+
exit_code=-1, stdout="", stderr=f"Command timed out after {timeout}s",
|
|
185
|
+
)
|
|
186
|
+
stdout = stdout_task.result()
|
|
187
|
+
stderr = stderr_task.result()
|
|
188
|
+
return BashResult(exit_code=proc.returncode or 0, stdout=stdout, stderr=stderr)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def run_stream(
|
|
192
|
+
command: str,
|
|
193
|
+
cwd: str | None = None,
|
|
194
|
+
env: dict[str, str] | None = None,
|
|
195
|
+
timeout: float | None = None,
|
|
196
|
+
) -> AsyncIterator[BashEvent]:
|
|
197
|
+
"""Run a shell command, yielding events as the subprocess emits them.
|
|
198
|
+
|
|
199
|
+
Terminates with a single `BashExit` event on normal completion or
|
|
200
|
+
a single `BashError` event on timeout / wire-level failure.
|
|
201
|
+
"""
|
|
202
|
+
sub_env = _clean_env(env)
|
|
203
|
+
proc = await asyncio.create_subprocess_shell(
|
|
204
|
+
command,
|
|
205
|
+
stdout=asyncio.subprocess.PIPE,
|
|
206
|
+
stderr=asyncio.subprocess.PIPE,
|
|
207
|
+
cwd=cwd,
|
|
208
|
+
env=sub_env,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def _pump(stream, tag, queue):
|
|
212
|
+
while True:
|
|
213
|
+
chunk = await stream.read(4096)
|
|
214
|
+
if not chunk:
|
|
215
|
+
break
|
|
216
|
+
await queue.put((tag, chunk))
|
|
217
|
+
await queue.put((tag, None))
|
|
218
|
+
|
|
219
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
220
|
+
tasks = [
|
|
221
|
+
asyncio.create_task(_pump(proc.stdout, "stdout", queue)),
|
|
222
|
+
asyncio.create_task(_pump(proc.stderr, "stderr", queue)),
|
|
223
|
+
]
|
|
224
|
+
open_streams = {"stdout", "stderr"}
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
deadline = None
|
|
228
|
+
if timeout is not None:
|
|
229
|
+
deadline = asyncio.get_event_loop().time() + timeout
|
|
230
|
+
while open_streams:
|
|
231
|
+
remaining = None
|
|
232
|
+
if deadline is not None:
|
|
233
|
+
remaining = max(deadline - asyncio.get_event_loop().time(), 0)
|
|
234
|
+
if remaining == 0:
|
|
235
|
+
proc.kill()
|
|
236
|
+
yield BashError(message=f"Command timed out after {timeout}s")
|
|
237
|
+
return
|
|
238
|
+
try:
|
|
239
|
+
tag, chunk = await asyncio.wait_for(queue.get(), timeout=remaining)
|
|
240
|
+
except TimeoutError:
|
|
241
|
+
proc.kill()
|
|
242
|
+
yield BashError(message=f"Command timed out after {timeout}s")
|
|
243
|
+
return
|
|
244
|
+
if chunk is None:
|
|
245
|
+
open_streams.discard(tag)
|
|
246
|
+
continue
|
|
247
|
+
text = chunk.decode(errors="replace")
|
|
248
|
+
if tag == "stdout":
|
|
249
|
+
yield BashStdout(data=text)
|
|
250
|
+
else:
|
|
251
|
+
yield BashStderr(data=text)
|
|
252
|
+
await proc.wait()
|
|
253
|
+
yield BashExit(exit_code=proc.returncode or 0)
|
|
254
|
+
finally:
|
|
255
|
+
for t in tasks:
|
|
256
|
+
t.cancel()
|
|
257
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
258
|
+
if proc.returncode is None:
|
|
259
|
+
proc.kill()
|
|
260
|
+
await proc.wait()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Files primitive — sandbox file upload / download as an Agentix namespace.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
from agentix import RuntimeClient
|
|
6
|
+
from agentix import files
|
|
7
|
+
|
|
8
|
+
async with RuntimeClient(sandbox.runtime_url) as c:
|
|
9
|
+
r = await c.remote(files.upload, path="/workspace/input.txt", content=b"hello")
|
|
10
|
+
print(r.size)
|
|
11
|
+
|
|
12
|
+
data = await c.remote(files.download, path="/workspace/output.txt")
|
|
13
|
+
|
|
14
|
+
Files are encoded as pydantic `bytes` (base64 in the JSON wire form).
|
|
15
|
+
Suitable for kB–MB sized files; very large blobs should ship via a
|
|
16
|
+
purpose-built binary `WirePattern` rather than the unary JSON path.
|
|
17
|
+
|
|
18
|
+
The package IS the namespace — `upload` and `download` are top-level
|
|
19
|
+
async functions, `UploadResult` is a regular dataclass callers can
|
|
20
|
+
import for type hints.
|
|
21
|
+
|
|
22
|
+
Writes/reads are confined to `$AGENTIX_UPLOAD_ROOT` (default
|
|
23
|
+
`/workspace`). Paths outside that root raise `PermissionError`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import errno
|
|
29
|
+
import os
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
UPLOAD_ROOT = Path(os.environ.get("AGENTIX_UPLOAD_ROOT", "/workspace")).resolve()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class UploadResult:
|
|
38
|
+
"""What `upload` returns — resolved sandbox-side path + bytes written."""
|
|
39
|
+
|
|
40
|
+
path: str
|
|
41
|
+
size: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_within(path: str) -> Path:
|
|
45
|
+
"""Return `path` lexically under `UPLOAD_ROOT`.
|
|
46
|
+
|
|
47
|
+
Actual open/read/write calls below walk from an already-open root
|
|
48
|
+
directory fd with O_NOFOLLOW, so symlink swaps cannot redirect the
|
|
49
|
+
final file operation outside the upload root.
|
|
50
|
+
"""
|
|
51
|
+
return UPLOAD_ROOT / Path(*_relative_parts(path))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _relative_parts(path: str) -> tuple[str, ...]:
|
|
55
|
+
raw = os.path.normpath(path)
|
|
56
|
+
if os.path.isabs(raw):
|
|
57
|
+
root = os.fspath(UPLOAD_ROOT)
|
|
58
|
+
try:
|
|
59
|
+
if os.path.commonpath([root, raw]) != root:
|
|
60
|
+
raise PermissionError(f"Path {raw} outside allowed root {UPLOAD_ROOT}")
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise PermissionError(f"Path {raw} outside allowed root {UPLOAD_ROOT}") from exc
|
|
63
|
+
raw = os.path.relpath(raw, root)
|
|
64
|
+
parts = tuple(p for p in Path(raw).parts if p not in ("", "."))
|
|
65
|
+
if not parts or any(p == ".." for p in parts):
|
|
66
|
+
raise PermissionError(f"Path {path!r} outside allowed root {UPLOAD_ROOT}")
|
|
67
|
+
return parts
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _open_parent(parts: tuple[str, ...], *, create: bool) -> tuple[int, str]:
|
|
71
|
+
"""Open the parent directory under UPLOAD_ROOT without following symlinks.
|
|
72
|
+
|
|
73
|
+
Returns `(parent_fd, filename)`. The caller owns `parent_fd`.
|
|
74
|
+
"""
|
|
75
|
+
root_fd = os.open(UPLOAD_ROOT, os.O_RDONLY | os.O_DIRECTORY)
|
|
76
|
+
fd = root_fd
|
|
77
|
+
try:
|
|
78
|
+
for part in parts[:-1]:
|
|
79
|
+
if create:
|
|
80
|
+
try:
|
|
81
|
+
os.mkdir(part, mode=0o777, dir_fd=fd)
|
|
82
|
+
except FileExistsError:
|
|
83
|
+
pass
|
|
84
|
+
try:
|
|
85
|
+
next_fd = os.open(
|
|
86
|
+
part,
|
|
87
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
|
88
|
+
dir_fd=fd,
|
|
89
|
+
)
|
|
90
|
+
except OSError as exc:
|
|
91
|
+
if exc.errno == errno.ELOOP:
|
|
92
|
+
raise PermissionError(
|
|
93
|
+
f"Refusing to follow symlink inside {UPLOAD_ROOT}"
|
|
94
|
+
) from exc
|
|
95
|
+
raise
|
|
96
|
+
os.close(fd)
|
|
97
|
+
fd = next_fd
|
|
98
|
+
return fd, parts[-1]
|
|
99
|
+
except Exception:
|
|
100
|
+
os.close(fd)
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _open_file(path: str, flags: int, *, create_parents: bool, mode: int = 0o666) -> tuple[int, Path]:
|
|
105
|
+
parts = _relative_parts(path)
|
|
106
|
+
parent_fd, name = _open_parent(parts, create=create_parents)
|
|
107
|
+
try:
|
|
108
|
+
try:
|
|
109
|
+
fd = os.open(name, flags | os.O_NOFOLLOW, mode, dir_fd=parent_fd)
|
|
110
|
+
except OSError as exc:
|
|
111
|
+
if exc.errno == errno.ELOOP:
|
|
112
|
+
raise PermissionError(
|
|
113
|
+
f"Refusing to follow symlink inside {UPLOAD_ROOT}"
|
|
114
|
+
) from exc
|
|
115
|
+
raise
|
|
116
|
+
return fd, _resolve_within(path)
|
|
117
|
+
finally:
|
|
118
|
+
os.close(parent_fd)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def upload(path: str, content: bytes) -> UploadResult:
|
|
122
|
+
"""Write `content` to `path` inside the sandbox.
|
|
123
|
+
|
|
124
|
+
Creates parent directories as needed. `path` must resolve under
|
|
125
|
+
the upload-root; otherwise raises `PermissionError`.
|
|
126
|
+
"""
|
|
127
|
+
fd, p = _open_file(
|
|
128
|
+
path,
|
|
129
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
130
|
+
create_parents=True,
|
|
131
|
+
)
|
|
132
|
+
with os.fdopen(fd, "wb") as f:
|
|
133
|
+
f.write(content)
|
|
134
|
+
return UploadResult(path=str(p), size=len(content))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def download(path: str) -> bytes:
|
|
138
|
+
"""Read the contents of `path` from inside the sandbox.
|
|
139
|
+
|
|
140
|
+
Raises `FileNotFoundError` / `IsADirectoryError` /
|
|
141
|
+
`PermissionError` for the corresponding filesystem conditions.
|
|
142
|
+
"""
|
|
143
|
+
fd, _ = _open_file(path, os.O_RDONLY, create_parents=False)
|
|
144
|
+
with os.fdopen(fd, "rb") as f:
|
|
145
|
+
return f.read()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentix-runtime-basic
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Shell + file I/O primitives for Agentix sandboxes
|
|
5
|
+
Project-URL: Homepage, https://github.com/Agentiix/Agentix-Runtime-Basic
|
|
6
|
+
Author: Agentiix
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: agentixx>=0.1.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# agentix-runtime-basic
|
|
19
|
+
|
|
20
|
+
Shell + file I/O primitives for [Agentix](https://github.com/Agentiix/Agentix)
|
|
21
|
+
sandboxes. One wheel, two namespaces:
|
|
22
|
+
|
|
23
|
+
| Namespace | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `agentix.bash` | execute shell commands, stream stdout/stderr |
|
|
26
|
+
| `agentix.files` | upload/download/list files inside the sandbox |
|
|
27
|
+
|
|
28
|
+
These two used to ship as separate `agentix-bash` and `agentix-files`
|
|
29
|
+
distributions. They consolidated here because every realistic sandbox
|
|
30
|
+
image needs both — splitting them was friction without isolation
|
|
31
|
+
benefit (neither has any non-stdlib runtime deps).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install agentix-runtime-basic
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from agentix import RuntimeClient
|
|
43
|
+
from agentix.bash import run as bash_run, run_stream as bash_stream
|
|
44
|
+
from agentix.files import upload, download
|
|
45
|
+
|
|
46
|
+
async with RuntimeClient(sandbox.runtime_url) as c:
|
|
47
|
+
await c.remote(upload, path="data.json", content=blob)
|
|
48
|
+
result = await c.remote(bash_run, command="cat data.json | jq .")
|
|
49
|
+
async for ev in c.remote(bash_stream, command="pytest -q"):
|
|
50
|
+
... # ev is a BashStdout / BashStderr / BashExit / BashError
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each namespace is "the package IS the namespace": top-level async
|
|
54
|
+
functions are the remote-callable surface, dataclasses/types like
|
|
55
|
+
`BashResult` and `UploadResult` are importable for return-type
|
|
56
|
+
annotations.
|
|
57
|
+
|
|
58
|
+
## Building a sandbox image
|
|
59
|
+
|
|
60
|
+
`runtime/Dockerfile` is the base image bundle builds extend from. Most
|
|
61
|
+
users invoke it indirectly:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
agentix build runtime-basic -o my-agent:0.1.0
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agentix/bash/__init__.py,sha256=n39E5fT_ObemG5zYUOH1y9xMK1hnWMIyM9y9cSq58c8,8176
|
|
2
|
+
agentix/files/__init__.py,sha256=54kyYKLE2-wI9Eu_fC9lOSNUmP1AetRjDwgjumdQLxI,4846
|
|
3
|
+
agentix_runtime_basic-0.1.1.dist-info/METADATA,sha256=_0MYs4i4tdo4M-Lsfi2iMN4zV0klJlf7n0-hz8SYDOc,2102
|
|
4
|
+
agentix_runtime_basic-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
agentix_runtime_basic-0.1.1.dist-info/entry_points.txt,sha256=yBKDuZJEX0Vi-e-CmwLijw5VmZn3puPBlx3gflhtzfg,62
|
|
6
|
+
agentix_runtime_basic-0.1.1.dist-info/licenses/LICENSE,sha256=mt35xkyIDkKZlEgxp-WYN0zaNDhtZtYtagSi6rMQmXg,1065
|
|
7
|
+
agentix_runtime_basic-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentiix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|