inis 0.1.0__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.
inis-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: inis
3
+ Version: 0.1.0
4
+ Summary: Python SDK for inis.run agent sandboxes
5
+ Author-email: "inis.run" <ops@inis.run>
6
+ License: MIT
7
+ Project-URL: Homepage, https://inis.run
8
+ Project-URL: Documentation, https://docs.inis.run
9
+ Project-URL: Repository, https://github.com/inis-run/sdk/tree/main/python
10
+ Project-URL: Issues, https://github.com/inis-run/sdk/issues
11
+ Keywords: inis,sandbox,agents,llm
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: httpx>=0.27
15
+ Requires-Dist: websockets>=13
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
19
+ Requires-Dist: pyyaml>=6; extra == "dev"
20
+
21
+ # inis.run Python SDK
22
+
23
+ Run untrusted or AI-generated code in its own isolated VM.
24
+
25
+ [![PyPI](https://img.shields.io/pypi/v/inis.svg)](https://pypi.org/project/inis/)
26
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/inis-run/sdk/blob/main/LICENSE)
27
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/inis/)
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install inis
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from inis import Client
39
+
40
+ client = Client() # reads INIS_API_KEY (and INIS_BASE_URL) from the environment
41
+
42
+ with client.session() as session:
43
+ session.exec(["pip", "install", "httpx", "-q"])
44
+ session.files.write("/workspace/out.txt", "hello")
45
+ print(session.files.read("/workspace/out.txt"))
46
+
47
+ preview = session.expose(8765)
48
+ print(preview.preview_url)
49
+
50
+ # One-shot execution (throwaway session, no cleanup needed)
51
+ result = client.execute(language="python", code="print(42)")
52
+ print(result.stdout)
53
+
54
+ # Re-attach to a session created elsewhere, by ID
55
+ session = client.sessions.attach("ses_123")
56
+ ```
57
+
58
+ `client.session()` returns a context manager that destroys the session on
59
+ exit. Use `client.sessions.create(...)` directly if you want to manage its
60
+ lifetime yourself, and `client.sessions.attach(id)` to reconnect to a
61
+ session you don't own — that call never destroys it.
62
+
63
+ ## Timeouts
64
+
65
+ `Client(timeout=...)` and `Session(timeout=...)` are **seconds** (httpx
66
+ convention). The TypeScript SDK's `timeoutMs` is milliseconds (JS
67
+ convention) — a deliberate per-language idiom, not drift.
68
+
69
+ ## Links
70
+
71
+ - Docs: [docs.inis.run](https://docs.inis.run)
72
+ - Quickstart: [docs.inis.run/docs/quickstart](https://docs.inis.run/docs/quickstart)
73
+ - Source: [github.com/inis-run/sdk/tree/main/python](https://github.com/inis-run/sdk/tree/main/python)
74
+ - Homepage: [inis.run](https://inis.run)
75
+
76
+ Built and run in the EU. Your code and data never leave Europe.
77
+
78
+ ## License
79
+
80
+ MIT — see [LICENSE](https://github.com/inis-run/sdk/blob/main/LICENSE).
inis-0.1.0/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # inis.run Python SDK
2
+
3
+ Run untrusted or AI-generated code in its own isolated VM.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/inis.svg)](https://pypi.org/project/inis/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/inis-run/sdk/blob/main/LICENSE)
7
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/inis/)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install inis
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from inis import Client
19
+
20
+ client = Client() # reads INIS_API_KEY (and INIS_BASE_URL) from the environment
21
+
22
+ with client.session() as session:
23
+ session.exec(["pip", "install", "httpx", "-q"])
24
+ session.files.write("/workspace/out.txt", "hello")
25
+ print(session.files.read("/workspace/out.txt"))
26
+
27
+ preview = session.expose(8765)
28
+ print(preview.preview_url)
29
+
30
+ # One-shot execution (throwaway session, no cleanup needed)
31
+ result = client.execute(language="python", code="print(42)")
32
+ print(result.stdout)
33
+
34
+ # Re-attach to a session created elsewhere, by ID
35
+ session = client.sessions.attach("ses_123")
36
+ ```
37
+
38
+ `client.session()` returns a context manager that destroys the session on
39
+ exit. Use `client.sessions.create(...)` directly if you want to manage its
40
+ lifetime yourself, and `client.sessions.attach(id)` to reconnect to a
41
+ session you don't own — that call never destroys it.
42
+
43
+ ## Timeouts
44
+
45
+ `Client(timeout=...)` and `Session(timeout=...)` are **seconds** (httpx
46
+ convention). The TypeScript SDK's `timeoutMs` is milliseconds (JS
47
+ convention) — a deliberate per-language idiom, not drift.
48
+
49
+ ## Links
50
+
51
+ - Docs: [docs.inis.run](https://docs.inis.run)
52
+ - Quickstart: [docs.inis.run/docs/quickstart](https://docs.inis.run/docs/quickstart)
53
+ - Source: [github.com/inis-run/sdk/tree/main/python](https://github.com/inis-run/sdk/tree/main/python)
54
+ - Homepage: [inis.run](https://inis.run)
55
+
56
+ Built and run in the EU. Your code and data never leave Europe.
57
+
58
+ ## License
59
+
60
+ MIT — see [LICENSE](https://github.com/inis-run/sdk/blob/main/LICENSE).
@@ -0,0 +1,63 @@
1
+ """inis.run Python SDK — thin wrapper over the session HTTP API."""
2
+
3
+ from inis.async_client import (
4
+ AsyncClient,
5
+ AsyncInisClient,
6
+ AsyncSession,
7
+ )
8
+ from inis.client import (
9
+ ArtifactFile,
10
+ ArtifactInfo,
11
+ BatchExecResult,
12
+ Capacity,
13
+ CapacityLimits,
14
+ CheckpointInfo,
15
+ Client,
16
+ ConnectorInfo,
17
+ ExposeResult,
18
+ ExecResult,
19
+ ExecStreamEvent,
20
+ ForkResult,
21
+ InisClient,
22
+ InisError,
23
+ ProcessInfo,
24
+ ProcessLogEvent,
25
+ ProcessLogs,
26
+ RegistryCredentialInfo,
27
+ Session,
28
+ SessionInfo,
29
+ TemplateInfo,
30
+ )
31
+ from inis.interpreter_common import InterpreterResult
32
+ from inis.pty import AsyncPTY, PTY, PTYExitInfo
33
+
34
+ __all__ = [
35
+ "ArtifactFile",
36
+ "ArtifactInfo",
37
+ "AsyncClient",
38
+ "AsyncInisClient",
39
+ "AsyncPTY",
40
+ "AsyncSession",
41
+ "BatchExecResult",
42
+ "Capacity",
43
+ "CapacityLimits",
44
+ "CheckpointInfo",
45
+ "Client",
46
+ "ConnectorInfo",
47
+ "ExposeResult",
48
+ "ExecResult",
49
+ "ExecStreamEvent",
50
+ "ForkResult",
51
+ "InisClient",
52
+ "InisError",
53
+ "InterpreterResult",
54
+ "ProcessInfo",
55
+ "ProcessLogEvent",
56
+ "ProcessLogs",
57
+ "PTY",
58
+ "PTYExitInfo",
59
+ "RegistryCredentialInfo",
60
+ "Session",
61
+ "SessionInfo",
62
+ "TemplateInfo",
63
+ ]
@@ -0,0 +1,279 @@
1
+ # inis.run code-interpreter kernel.
2
+ #
3
+ # Ships inside the inis SDK packages (Python + TypeScript — see
4
+ # sdk/typescript/src/kernel_script.ts, generated from this file by
5
+ # scripts/gen-kernel-ts.py, which must be re-run after any edit here) and is
6
+ # uploaded into a session's filesystem on the first run_code() call, then
7
+ # started as a long-lived background process via start_process(). It has no
8
+ # dependency on the inis SDK itself and no guest-agent changes are needed:
9
+ # stdlib only for the core loop. matplotlib/pandas are used opportunistically
10
+ # if already importable in the target environment (e.g. the official
11
+ # "data-science" template) to produce richer results, but their absence
12
+ # never breaks plain code execution.
13
+ #
14
+ # Protocol: polls a requests directory for atomically renamed *.json request
15
+ # files ({"id", "code"}) and executes each against a persistent globals()
16
+ # dict shared across calls (the "context"), then atomically writes a *.json
17
+ # result envelope ({"id", "results": [...]}) to a results directory. The SDK
18
+ # writes request files via write-then-rename (write a temp file, then a
19
+ # separate exec() renames it into place) so this loop never observes a
20
+ # partially-written request; it does the same on the way out (write a .tmp
21
+ # file, then os.rename) so the SDK's shell-side poll loop never observes a
22
+ # partially-written result.
23
+ #
24
+ # Each result item has a "type" of "text", "image", "table", "json", or
25
+ # "error". restart_context() in the SDK kills this process outright (the
26
+ # simplest way to guarantee a fully fresh context) rather than messaging it,
27
+ # so this script has no reset protocol of its own.
28
+ import ast
29
+ import contextlib
30
+ import io
31
+ import json
32
+ import os
33
+ import sys
34
+ import time
35
+ import traceback
36
+
37
+ ROOT = sys.argv[1] if len(sys.argv) > 1 else "/workspace/.inis_kernel"
38
+ REQ_DIR = os.path.join(ROOT, "requests")
39
+ RES_DIR = os.path.join(ROOT, "results")
40
+ ART_DIR = os.path.join(ROOT, "artifacts")
41
+
42
+ # These bound how much of a rich result (a DataFrame's rows, or
43
+ # a value's repr()) the kernel puts in one result envelope, distinct from
44
+ # the transport's own limits. The envelope is read back over the same
45
+ # buffered-exec channel as any other run_code() stdout, so it inherits that
46
+ # path's cap (protocol.MaxStdoutBytes, 4 MiB — see internal/protocol/
47
+ # protocol.go) as the real ceiling; these were previously 200/20000 with no
48
+ # stated relationship to that budget. Raised 5x — still comfortably inside
49
+ # the 4 MiB budget for realistic tables/reprs, while an agent asking for the
50
+ # last value no longer needs a second call just to see past row 200.
51
+ # truncated/row_count are still reported so a caller that needs more than
52
+ # this can tell and can re-query with an explicit .head()/slice instead of
53
+ # silently getting a partial view.
54
+ MAX_TABLE_ROWS = 1000
55
+ MAX_REPR_LEN = 100000
56
+
57
+ _globals = {"__name__": "__main__"}
58
+
59
+
60
+ def _ensure_dirs():
61
+ for d in (REQ_DIR, RES_DIR, ART_DIR):
62
+ os.makedirs(d, exist_ok=True)
63
+
64
+
65
+ def _try_matplotlib():
66
+ try:
67
+ import matplotlib
68
+
69
+ matplotlib.use("Agg")
70
+ import matplotlib.pyplot as plt
71
+
72
+ return plt
73
+ except Exception:
74
+ return None
75
+
76
+
77
+ def _try_pandas():
78
+ try:
79
+ import pandas as pd
80
+
81
+ return pd
82
+ except Exception:
83
+ return None
84
+
85
+
86
+ def _json_safe(value):
87
+ try:
88
+ json.dumps(value)
89
+ return value
90
+ except Exception:
91
+ return repr(value)
92
+
93
+
94
+ def _table_result(df):
95
+ try:
96
+ truncated = len(df) > MAX_TABLE_ROWS
97
+ view = df.head(MAX_TABLE_ROWS)
98
+ rows = json.loads(view.to_json(orient="records", date_format="iso"))
99
+ return {
100
+ "type": "table",
101
+ "columns": [str(c) for c in view.columns],
102
+ "rows": rows,
103
+ "row_count": int(len(df)),
104
+ "truncated": truncated,
105
+ }
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def capture_figures(req_id, plt):
111
+ """Save every open matplotlib figure with at least one axes to ART_DIR
112
+ as a PNG, close it, and return one "image" result dict per figure."""
113
+ results = []
114
+ if plt is None:
115
+ return results
116
+ for i, num in enumerate(list(plt.get_fignums())):
117
+ fig = plt.figure(num)
118
+ try:
119
+ if not fig.axes:
120
+ continue
121
+ path = os.path.join(ART_DIR, "%s_%d.png" % (req_id, i))
122
+ fig.savefig(path, format="png", bbox_inches="tight")
123
+ results.append(
124
+ {
125
+ "type": "image",
126
+ "format": "png",
127
+ "path": path,
128
+ "size": os.path.getsize(path),
129
+ }
130
+ )
131
+ finally:
132
+ plt.close(fig)
133
+ return results
134
+
135
+
136
+ def run_cell(code, req_id, globals_dict=None):
137
+ """Execute one cell of code against globals_dict (defaults to the
138
+ persistent module-level context), returning a list of result dicts.
139
+ Pure function of (code, globals_dict) plus the optional-dependency
140
+ probes above — no filesystem polling, so it's directly unit-testable."""
141
+ g = _globals if globals_dict is None else globals_dict
142
+ results = []
143
+ stdout_buf, stderr_buf = io.StringIO(), io.StringIO()
144
+ plt = _try_matplotlib()
145
+ pd = _try_pandas()
146
+ last_value = None
147
+ has_last_value = False
148
+
149
+ try:
150
+ parsed = ast.parse(code, mode="exec")
151
+ last_expr = None
152
+ if parsed.body and isinstance(parsed.body[-1], ast.Expr):
153
+ last_expr = parsed.body.pop()
154
+ with contextlib.redirect_stdout(stdout_buf), contextlib.redirect_stderr(stderr_buf):
155
+ if parsed.body:
156
+ exec(compile(parsed, "<inis-run_code>", "exec"), g)
157
+ if last_expr is not None:
158
+ value = eval(
159
+ compile(ast.Expression(last_expr.value), "<inis-run_code>", "eval"),
160
+ g,
161
+ )
162
+ if value is not None:
163
+ last_value = value
164
+ has_last_value = True
165
+ except Exception as exc:
166
+ # Deliberately not BaseException: SystemExit/KeyboardInterrupt (e.g.
167
+ # user code calling exit()) should kill this process rather than be
168
+ # reported as a catchable "error" result — the same call-exit-to-
169
+ # restart-the-kernel convention Jupyter uses. main()'s caller lets
170
+ # SystemExit propagate; the next run_code() transparently restarts
171
+ # the (now-dead) kernel with a fresh context, same as any other
172
+ # kernel death.
173
+ tb = traceback.format_exc()
174
+ out, err = stdout_buf.getvalue(), stderr_buf.getvalue()
175
+ if out:
176
+ results.append({"type": "text", "stream": "stdout", "text": out})
177
+ if err:
178
+ results.append({"type": "text", "stream": "stderr", "text": err})
179
+ results.append(
180
+ {
181
+ "type": "error",
182
+ "ename": type(exc).__name__,
183
+ "evalue": str(exc),
184
+ "traceback": tb,
185
+ }
186
+ )
187
+ results.extend(capture_figures(req_id, plt))
188
+ return results
189
+
190
+ out, err = stdout_buf.getvalue(), stderr_buf.getvalue()
191
+ if out:
192
+ results.append({"type": "text", "stream": "stdout", "text": out})
193
+ if err:
194
+ results.append({"type": "text", "stream": "stderr", "text": err})
195
+
196
+ results.extend(capture_figures(req_id, plt))
197
+
198
+ if has_last_value:
199
+ handled = False
200
+ if pd is not None:
201
+ try:
202
+ if isinstance(last_value, pd.DataFrame):
203
+ t = _table_result(last_value)
204
+ if t is not None:
205
+ results.append(t)
206
+ handled = True
207
+ elif isinstance(last_value, pd.Series):
208
+ t = _table_result(last_value.to_frame())
209
+ if t is not None:
210
+ results.append(t)
211
+ handled = True
212
+ except Exception:
213
+ pass
214
+ if not handled:
215
+ if isinstance(last_value, (dict, list, tuple)):
216
+ results.append({"type": "json", "json": _json_safe(last_value)})
217
+ else:
218
+ r = repr(last_value)
219
+ if len(r) > MAX_REPR_LEN:
220
+ r = r[:MAX_REPR_LEN] + "... (truncated)"
221
+ results.append({"type": "text", "stream": "result", "text": r})
222
+
223
+ return results
224
+
225
+
226
+ def _write_result(req_id, results):
227
+ payload = {"id": req_id, "results": results}
228
+ tmp_path = os.path.join(RES_DIR, ".%s.tmp" % req_id)
229
+ final_path = os.path.join(RES_DIR, "%s.json" % req_id)
230
+ with open(tmp_path, "w") as f:
231
+ json.dump(payload, f)
232
+ os.rename(tmp_path, final_path)
233
+
234
+
235
+ def main():
236
+ _ensure_dirs()
237
+ seen = set()
238
+ while True:
239
+ try:
240
+ names = sorted(os.listdir(REQ_DIR))
241
+ except FileNotFoundError:
242
+ names = []
243
+ for name in names:
244
+ if not name.endswith(".json") or name.startswith(".") or name in seen:
245
+ continue
246
+ seen.add(name)
247
+ path = os.path.join(REQ_DIR, name)
248
+ try:
249
+ with open(path, "r") as f:
250
+ req = json.load(f)
251
+ except Exception:
252
+ continue
253
+ finally:
254
+ try:
255
+ os.remove(path)
256
+ except OSError:
257
+ pass
258
+
259
+ req_id = req.get("id") or name[:-5]
260
+ code = req.get("code", "")
261
+ try:
262
+ results = run_cell(code, req_id)
263
+ except SystemExit:
264
+ raise
265
+ except Exception:
266
+ results = [
267
+ {
268
+ "type": "error",
269
+ "ename": "KernelError",
270
+ "evalue": "kernel failed to execute request",
271
+ "traceback": traceback.format_exc(),
272
+ }
273
+ ]
274
+ _write_result(req_id, results)
275
+ time.sleep(0.05)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()