nontainer 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.
Files changed (72) hide show
  1. nontainer-0.1.0/.github/workflows/publish.yml +33 -0
  2. nontainer-0.1.0/.github/workflows/test.yml +43 -0
  3. nontainer-0.1.0/.gitignore +8 -0
  4. nontainer-0.1.0/CHANGELOG.md +223 -0
  5. nontainer-0.1.0/LICENSE +21 -0
  6. nontainer-0.1.0/PKG-INFO +198 -0
  7. nontainer-0.1.0/README.md +150 -0
  8. nontainer-0.1.0/docs/api.md +473 -0
  9. nontainer-0.1.0/docs/apps.md +351 -0
  10. nontainer-0.1.0/docs/design.md +107 -0
  11. nontainer-0.1.0/docs/quick-start.md +206 -0
  12. nontainer-0.1.0/examples/_model.py +18 -0
  13. nontainer-0.1.0/examples/analyst.py +70 -0
  14. nontainer-0.1.0/examples/webapp.py +176 -0
  15. nontainer-0.1.0/nontainer/__init__.py +67 -0
  16. nontainer-0.1.0/nontainer/adapters/__init__.py +17 -0
  17. nontainer-0.1.0/nontainer/adapters/a2ui.py +438 -0
  18. nontainer-0.1.0/nontainer/adapters/agno.py +300 -0
  19. nontainer-0.1.0/nontainer/adapters/mcp.py +357 -0
  20. nontainer-0.1.0/nontainer/adapters/render.py +726 -0
  21. nontainer-0.1.0/nontainer/apps/__init__.py +69 -0
  22. nontainer-0.1.0/nontainer/apps/browser.py +172 -0
  23. nontainer-0.1.0/nontainer/apps/contract.py +198 -0
  24. nontainer-0.1.0/nontainer/apps/curl.py +178 -0
  25. nontainer-0.1.0/nontainer/apps/dispatch.py +450 -0
  26. nontainer-0.1.0/nontainer/apps/serve.py +159 -0
  27. nontainer-0.1.0/nontainer/apps/testapp.py +484 -0
  28. nontainer-0.1.0/nontainer/cache.py +124 -0
  29. nontainer-0.1.0/nontainer/editing.py +227 -0
  30. nontainer-0.1.0/nontainer/errors.py +25 -0
  31. nontainer-0.1.0/nontainer/hints.py +44 -0
  32. nontainer-0.1.0/nontainer/presets.py +323 -0
  33. nontainer-0.1.0/nontainer/protocol.py +198 -0
  34. nontainer-0.1.0/nontainer/providers/__init__.py +17 -0
  35. nontainer-0.1.0/nontainer/providers/agentfs.py +499 -0
  36. nontainer-0.1.0/nontainer/providers/dir.py +170 -0
  37. nontainer-0.1.0/nontainer/providers/kvgit.py +206 -0
  38. nontainer-0.1.0/nontainer/py.typed +0 -0
  39. nontainer-0.1.0/nontainer/skills.py +201 -0
  40. nontainer-0.1.0/nontainer/workspace.py +1265 -0
  41. nontainer-0.1.0/pyproject.toml +80 -0
  42. nontainer-0.1.0/tests/conftest.py +13 -0
  43. nontainer-0.1.0/tests/test_a2ui.py +490 -0
  44. nontainer-0.1.0/tests/test_agent_patterns.py +115 -0
  45. nontainer-0.1.0/tests/test_agentfs_provider.py +165 -0
  46. nontainer-0.1.0/tests/test_agno_adapter.py +204 -0
  47. nontainer-0.1.0/tests/test_apps_browser.py +79 -0
  48. nontainer-0.1.0/tests/test_apps_dispatch.py +590 -0
  49. nontainer-0.1.0/tests/test_apps_serving.py +244 -0
  50. nontainer-0.1.0/tests/test_apps_surface.py +129 -0
  51. nontainer-0.1.0/tests/test_apps_testapp.py +378 -0
  52. nontainer-0.1.0/tests/test_async_facades.py +48 -0
  53. nontainer-0.1.0/tests/test_concurrency.py +242 -0
  54. nontainer-0.1.0/tests/test_dir_provider.py +73 -0
  55. nontainer-0.1.0/tests/test_factory.py +32 -0
  56. nontainer-0.1.0/tests/test_file_tools.py +72 -0
  57. nontainer-0.1.0/tests/test_kvgit_provider.py +199 -0
  58. nontainer-0.1.0/tests/test_mcp_adapter.py +209 -0
  59. nontainer-0.1.0/tests/test_mounts_and_cache.py +91 -0
  60. nontainer-0.1.0/tests/test_presets.py +230 -0
  61. nontainer-0.1.0/tests/test_primer.py +85 -0
  62. nontainer-0.1.0/tests/test_process_isolation.py +175 -0
  63. nontainer-0.1.0/tests/test_protocol.py +19 -0
  64. nontainer-0.1.0/tests/test_put_get.py +58 -0
  65. nontainer-0.1.0/tests/test_render.py +125 -0
  66. nontainer-0.1.0/tests/test_result_checkpoints.py +85 -0
  67. nontainer-0.1.0/tests/test_run_python.py +209 -0
  68. nontainer-0.1.0/tests/test_skills.py +128 -0
  69. nontainer-0.1.0/tests/test_terminal.py +158 -0
  70. nontainer-0.1.0/tests/test_terminal_python_sys.py +86 -0
  71. nontainer-0.1.0/tests/test_ui_artifacts.py +452 -0
  72. nontainer-0.1.0/uv.lock +2569 -0
@@ -0,0 +1,33 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [released]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+
18
+ - name: Install build dependencies
19
+ run: |
20
+ python -m pip install --upgrade pip
21
+ pip install build twine
22
+
23
+ - name: Build package
24
+ run: python -m build
25
+
26
+ - name: Check package
27
+ run: twine check dist/*
28
+
29
+ - name: Upload to PyPI
30
+ env:
31
+ TWINE_USERNAME: __token__
32
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
33
+ run: twine upload dist/*
@@ -0,0 +1,43 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest]
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ runs-on: ${{ matrix.os }}
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Install dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install -e ".[dev,apps,agno,mcp]"
29
+
30
+ - name: Install Playwright browser
31
+ run: python -m playwright install --with-deps chromium
32
+
33
+ - name: Run tests
34
+ run: |
35
+ pytest tests/ -v
36
+
37
+ - name: Run linter
38
+ run: |
39
+ ruff check nontainer/
40
+
41
+ - name: Check formatting
42
+ run: |
43
+ ruff format --check nontainer/
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,223 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-07-15
9
+
10
+ ### Added
11
+ - **`AppsConfig.script_hosts` + `apps_primer`: the script allowlist is
12
+ one declaration.** The hosts browser scripts may load from used to
13
+ live in four hand-synced places — test_app's interception, the served
14
+ CSP, the agent-facing notes, curl's error message — kept honest only
15
+ by a test. All four now derive from `AppsConfig.script_hosts`
16
+ (default unchanged: `DEFAULT_SCRIPT_HOSTS`), so an embedder adding a
17
+ private registry host (e.g. a self-hosted esm.sh over an internal
18
+ npm registry) changes one tuple and the walls, the verifier, and the
19
+ agent's instructions stay in agreement. `apps_primer` appends
20
+ embedder guidance to the apps notes — the place to teach a private
21
+ component lib's known-good import block. `build_router(csp=...)`
22
+ now defaults to deriving from the config (`build_csp`); pass a string
23
+ to override or `""` to disable. Removed: `test_app`'s per-call
24
+ `cdn_allowlist` parameter (set it on the config instead). Agents predictably
25
+ write into `/ui` themselves (`fig.write_json('/ui/x.json')`,
26
+ savefig) instead of assigning objects to `ui = {...}` — and those
27
+ files displayed nowhere. `run_python` now diffs the `/ui` listing
28
+ around the call and appends files the code created to the
29
+ `[ui artifacts: ...]` note (deduped against materialized values),
30
+ extending the existing path-pointer near-miss forgiveness.
31
+ - **The walls label their doors.** Three predictable agent collisions
32
+ now redirect instead of dead-ending:
33
+ a 404 on `/api/<name>.py` says endpoints are module names without
34
+ the extension (and suggests the real path when it exists) — agents
35
+ reliably mirror the filename into `fetch()` and then debug the
36
+ backend; blocked imports of `subprocess`/`requests`/`urllib.request`/
37
+ `httpx`/`socket` get a `[hint: ...]` in both run_python observations
38
+ and api.log pointing at the terminal's curl; and `urllib.parse` is
39
+ granted in the STDLIB preset (pure string functions only — `quote`,
40
+ `urlencode`, `parse_qs`, `urlparse`, ... — the network side of
41
+ urllib stays out). The apps primer also states the no-`.py`-in-URL
42
+ rule explicitly.
43
+ - **The 8MB `ui` artifact cap explains itself.** An oversize value used
44
+ to silently degrade to a truncated `repr` `.txt` — a 280k-point
45
+ plotly map showed up as a wall of text with no hint why. Now the
46
+ tool result carries a `[ui note: ...]` diagnosis (size vs cap, and
47
+ for plotly the actual usual culprit: per-point customdata/hover
48
+ strings — coordinates are cheap, WebGL traces render 100k+ points
49
+ fine) so the agent self-corrects, and the `.txt` artifact shows the
50
+ same message to the human where the figure would have been.
51
+ `materialize_ui` now returns `(artifacts, problems)`. The tool
52
+ description also teaches the cap + lean-spec guidance up front.
53
+ - **`python3` terminal alias.** The reserved `python` bridge now also
54
+ answers to `python3` — the reflex spelling agents type first. Both
55
+ names are reserved against user command injection.
56
+ - **`warnings` in the STDLIB preset.** `warn`, `filterwarnings`,
57
+ `simplefilter`, and `catch_warnings` are granted — agents reach for
58
+ `warnings.filterwarnings("ignore")` the moment pandas/sklearn start
59
+ emitting deprecation noise, and the module was imported by the
60
+ presets but never granted.
61
+ - **Artifact channels: binary in, images and files out.** Three
62
+ pieces close the "artifacts are stranded in the workspace" gap:
63
+ a `view_image` tool in both adapters (the agent views a saved
64
+ plot/chart — returned as real image content for vision models;
65
+ png/jpeg/gif/webp, 10MB cap); MCP **resources** exposing every
66
+ workspace file as `workspace://{path}` (text as text, binary as
67
+ blob) with a `workspace://-/tree` index — the client-side window
68
+ for extracting what the agent produced; and a `--mount
69
+ POINT=DIR[:rw]` flag on the MCP CLI (read-only by default) — the
70
+ inbound channel for seeding real host files without base64 games.
71
+ `file_write` results additionally carry a ground-truth
72
+ `ResourceLink` to the written file (the link exists because the
73
+ write succeeded), and the MCP tool descriptions coach the agent to
74
+ mention `workspace://` URIs when it produces artifacts.
75
+ - **Safe stdlib by default** — `PythonConfig(stdlib=True)` grants a
76
+ curated stdlib set (see `nontainer.presets.STDLIB`), so a plain
77
+ workspace's Python can `import math`/`json`/`csv`/... out of the box.
78
+ - **Module-grant presets** — `nontainer.presets.dataframes()` (numpy +
79
+ pandas) and `plotting()` (matplotlib Agg-pinned + font cache warmed;
80
+ plotly optional). `ModuleGrant` gains `include`/`exclude`/`recursive`/
81
+ `name`; `PythonConfig.modules` flattens preset lists one level.
82
+ - **Results pin their commit** — `TerminalResult`/`PythonResult`/
83
+ `EditOutcome` carry `checkpoint` (the commit the call produced, or
84
+ `None`); `write_file`/`put` return a `WriteOutcome`; `ws.head` /
85
+ `ws.dirty` pin the state a read-only call observed.
86
+ - **Async host facades** — `ws.aterminal` / `ws.arun_python` run the
87
+ sync execution in a thread so event-loop hosts (FastAPI, etc.) stay
88
+ responsive; the agent surface is unchanged.
89
+ - **Shared browser for `test_app`** — one Chromium across all calls
90
+ (async Playwright on a dedicated loop-thread), a context per
91
+ concurrent test bounded by a semaphore (`configure_browser`), plus
92
+ `arun_test_app` and `shutdown_browser`. Memory scales with
93
+ concurrency, not sessions.
94
+ - **`py.typed`** — the package now ships its PEP 561 marker.
95
+ - **Tool primers** — `WorkspaceTools`/`build_server` accept
96
+ `terminal_primer` / `python_primer`: embedder guidance appended to the
97
+ respective tool's description (e.g. "`db` is a SQLite store — use it,
98
+ not `cache`, for shared state"). Strict 1-to-1 with the exposed tools;
99
+ a `python_primer` in terminal-only mode lands in the terminal tool's
100
+ `python` section (with a warning).
101
+
102
+ - **Faithful `sys` in terminal `python`** — piped input reaches the code
103
+ as `sys.stdin` (`cat data | python script.py`), and `sys.argv` /
104
+ `input()` work, via sandtrap's synthetic safe `sys`. No `import`
105
+ quoting workarounds; dangerous `sys` internals stay unreachable.
106
+
107
+ ### Added
108
+ - **Workspace extension surface: `exec_python` / `build_sandbox` /
109
+ `lock`.** A small, documented contract for embedders composing
110
+ execution features on top of the workspace: `exec_python(code, *,
111
+ inputs, sandbox, cache, stdin, argv)` is the raw execution path (no
112
+ checkpoint, no lock; `cache=` overrides the agent-visible cache —
113
+ the old private `_UNSET` sentinel is gone); `build_sandbox(*,
114
+ timeout, tick_limit, extra_classes, filesystem)` mints per-purpose
115
+ sandboxes sharing the frozen config, memoizing the built `Policy`
116
+ per parameter set so a fresh sandbox per request is cheap; `lock`
117
+ exposes the single-writer RLock for host/extension work that must
118
+ serialize with tool calls. The apps extra now talks exclusively to
119
+ this surface (no private attribute access — enforced by a test), so
120
+ it runs unchanged on any `WorkspaceProvider`; frozen serving's
121
+ per-request policy rebuild (a latency + DoS-amplification papercut
122
+ on the anonymous path) is fixed by the memo; mutable (authoring)
123
+ dispatch now serializes under the workspace's own lock, so test_app
124
+ route callbacks and screenshot writes can't race ordinary tool
125
+ calls.
126
+
127
+ ### Added
128
+ - **`--apps` flag on the MCP CLI.** `python -m nontainer.adapters.mcp
129
+ --apps` enables the apps loop without writing an embed script: the
130
+ `curl` terminal builtin plus a `test_app` tool whose screenshots
131
+ return as MCP image content. Previously test_app over MCP required
132
+ calling `build_server(ws, apps=...)` from Python.
133
+
134
+ ### Changed
135
+ - **Workspace enforces its single-writer invariant internally.**
136
+ Mutating public calls (`terminal`, `run_python`, `write_file`,
137
+ `edit_file`, `put`, `checkpoint`, `restore`, `rollback`, `discard`,
138
+ `fork`, `close`) hold an internal `RLock`, so a harness that threads
139
+ parallel tool calls onto one session serializes safely — each call
140
+ atomic + checkpointed — instead of corrupting staged state. Custom
141
+ harnesses no longer need to supply their own lock (the adapters keep
142
+ theirs as a fence for adapter-level work). Read-only accessors stay
143
+ lock-free; host-side escape hatches (`ws.fs` writes, `ws.cache`
144
+ mutation) bypass the lock and remain the caller's concurrency
145
+ problem. RLock so a `host_object` that calls back into the public
146
+ API serializes instead of deadlocking.
147
+ - **stderr capture is per-execution, not a process-global redirect.**
148
+ `run_python` stderr now comes from sandtrap 0.2.4's ContextVar-routed
149
+ capture (`ExecResult.stderr`): concurrent executions — other sessions
150
+ in the same process, frozen app serving — no longer cross-contaminate
151
+ stderr or risk leaving `sys.stderr` pointing at a dead buffer. The
152
+ internal `capture_stderr` escape hatch is gone; served (frozen) app
153
+ handlers get stderr capture back. Sandboxed `sys.stderr` writes in
154
+ the terminal `python` builtin now surface as stderr instead of
155
+ leaking into pipeline stdout.
156
+ - **Live app serving is now frozen (read-only) snapshots.** `build_router`
157
+ serves a Workspace pinned to a published commit: handlers read the VFS
158
+ and call `host_objects` but can't mutate it (write → 500). This makes
159
+ serving **concurrent** (fresh read-only sandbox per request, no
160
+ per-session lock, no staged buffer, no checkpointing) and lossless to
161
+ evict. Mutable app state belongs in an external store via
162
+ `host_objects`. Removed: per-session serialization, quiesce
163
+ checkpointing, `queue_depth`/`quiesce_seconds`. Added: `max_snapshots`,
164
+ `on_log` (handler logs route off the read-only VFS; default: the
165
+ `nontainer.apps` logger). `AppRuntime(..., frozen=True, log_sink=...)`.
166
+ The router is **stateless** — `resolve → dispatch`, no snapshot cache,
167
+ no residency/lifecycle (cache inside `resolve` if it's expensive; the
168
+ router doesn't close its result). Rate limiting is an edge concern;
169
+ `rate_limit_per_min`/`max_snapshots`/`queue_depth` are gone.
170
+
171
+ ### Fixed
172
+ - **`test_app` accepts a stringified actions list.** Models routinely
173
+ send the nested list as a JSON string; the pydantic layer agno wraps
174
+ entrypoints in rejected it on the annotation before the existing
175
+ `coerce_actions` tolerance could run. The annotation is loosened so
176
+ coercion gets its chance.
177
+ - **Agent-set response headers are matched case-insensitively.**
178
+ `normalize()` lowercases `Response.headers` keys on the way to the
179
+ wire, so the idiomatic `"Content-Type": "text/csv"` overrides the
180
+ inferred content type instead of being silently ignored, and an
181
+ agent-set `Content-Security-Policy` makes the served router defer
182
+ its default instead of emitting a duplicate header (browsers apply
183
+ the intersection). `WireResponse.headers` keys are now canonical
184
+ lowercase.
185
+ - **`Request.require()` coerces symmetrically across sources.** JSON
186
+ has one number type, so `require("x", float)` accepts JSON `5` and
187
+ `require("n", int)` accepts `2.0` (non-integral floats still 400);
188
+ bools are never numbers (JSON `true` no longer passes an `int`
189
+ check); JSON strings coerce like query params; and query-param bools
190
+ parse `true/1/false/0` instead of Python's `bool("false") is True`.
191
+ - **Screenshot cap no longer aborts the test.** A `test_app` action
192
+ hitting `max_screenshots` is a noted soft skip (`ok`, with a
193
+ "skipped: screenshot cap reached" note) instead of a hard failure
194
+ that discarded every later action — asserts after the cap now run
195
+ and count.
196
+ - **Handler-log failures warn instead of going silently blind.**
197
+ `_log` still never breaks dispatch, but a broken/full fs (or a
198
+ raising `on_log` sink) now emits one `RuntimeWarning` per runtime —
199
+ previously every handler diagnostic vanished while the agent's
200
+ documented repair loop ("tail `/app/logs/api.log`") debugged blind.
201
+ - **`test_app` false-PASS window closed (as far as heuristics can).**
202
+ `read` now settles before observing, so a fetch that *starts* after
203
+ the previous action's settle returned (debounce, `setTimeout`) is
204
+ waited for instead of read as stale DOM. And a settle that exits via
205
+ its cap (`settle_cap`, default 5s — now a `test_app` parameter)
206
+ attaches a stale-risk note to the action's result instead of
207
+ silently passing, pointing the agent at `{"assert": ...}` — the
208
+ retrying form no heuristic can replace, since nothing can wait for a
209
+ fetch that hasn't started yet.
210
+ - **Browser shutdown no longer stalls interpreter exit.** The shared
211
+ test_app browser's atexit teardown deadlines dropped from 10s+5s to
212
+ 3s+2s — a healthy Chromium closes in milliseconds, and a wedged one
213
+ isn't worth holding process exit for. `configure_browser` now
214
+ documents its process-global, first-caller-wins contract.
215
+ - **App static serving path traversal** — `.`/`..` segments can no
216
+ longer escape `/app/`, and backend source under `/app/api/` is never
217
+ served as a static file.
218
+
219
+ ### Changed
220
+ - Requires **sandtrap ≥ 0.2.4** (per-execution stderr capture;
221
+ recursive-registration filter propagation, dotted patterns,
222
+ synthetic `sys`/stdin) and **monkeyfs ≥ 0.1.5**
223
+ (`VirtualFS.invalidate()`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Ashenfelter
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.
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: nontainer
3
+ Version: 0.1.0
4
+ Summary: A fake little computer for your agent: versioned filesystem, shell, and sandboxed Python.
5
+ Project-URL: Homepage, https://github.com/ashenfad/nontainer
6
+ Project-URL: Documentation, https://github.com/ashenfad/nontainer#readme
7
+ Project-URL: Bug Tracker, https://github.com/ashenfad/nontainer/issues
8
+ Project-URL: Source, https://github.com/ashenfad/nontainer
9
+ Author: ashenfad
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,sandbox,terminal,versioning,workspace
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: kvgit[disk]>=0.3.1
25
+ Requires-Dist: monkeyfs>=0.1.5
26
+ Requires-Dist: reprobate>=0.1.1
27
+ Requires-Dist: sandtrap>=0.2.8
28
+ Requires-Dist: termish>=0.1.8
29
+ Provides-Extra: agentfs
30
+ Requires-Dist: agentfs-sdk>=0.6; extra == 'agentfs'
31
+ Provides-Extra: agno
32
+ Requires-Dist: agno>=1.0; extra == 'agno'
33
+ Provides-Extra: apps
34
+ Requires-Dist: anyio>=4; extra == 'apps'
35
+ Requires-Dist: playwright>=1.40; extra == 'apps'
36
+ Requires-Dist: starlette>=0.37; extra == 'apps'
37
+ Provides-Extra: dev
38
+ Requires-Dist: httpx2; extra == 'dev'
39
+ Requires-Dist: matplotlib; extra == 'dev'
40
+ Requires-Dist: numpy; extra == 'dev'
41
+ Requires-Dist: pandas; extra == 'dev'
42
+ Requires-Dist: pytest; extra == 'dev'
43
+ Requires-Dist: pytest-asyncio; extra == 'dev'
44
+ Requires-Dist: ruff; extra == 'dev'
45
+ Provides-Extra: mcp
46
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # nontainer 📦
50
+
51
+ A fake little computer for your agent: versioned filesystem, shell, and
52
+ sandboxed Python -- as tools for any Python-based agent harness. No Docker,
53
+ no cloud sandbox, no infra. `pip install nontainer`.
54
+
55
+ > **Status: pre-alpha.** Usable and tested end to end; the API will still
56
+ > move before 1.0.
57
+
58
+ ## The pitch
59
+
60
+ You hand your agent a `terminal` and a `run_python` tool. Unlike a
61
+ stateless sandbox call, these are **stateful and bound to a session**: the
62
+ shell's `cd` sticks, files one call writes the next call reads, and a
63
+ `cache` dict persists for the whole conversation. It's a little computer the
64
+ agent keeps *using* -- not a fresh box each call.
65
+
66
+ And because that computer is a **versioned workspace**, you get the
67
+ operations durable state makes possible: checkpoint every call, fork a
68
+ session in O(1), roll back to any commit, audit the history. All in-process,
69
+ `pip`-installable, running wherever Python runs.
70
+
71
+ | | |
72
+ |---|---|
73
+ | **Terminal tool** | ~33 shell builtins (grep, sed, jq, tar, ...) over the virtual filesystem via [termish](https://github.com/ashenfad/termish). |
74
+ | **Python tool** | Policy-gated sandboxed execution via [sandtrap](https://github.com/ashenfad/sandtrap); safe stdlib on by default, `open()`/`os`/`pathlib` routed to the workspace via [monkeyfs](https://github.com/ashenfad/monkeyfs). |
75
+ | **In-process** | Agent code can call *your* whitelisted host objects -- the live model, the db pool -- under policy. No cloud sandbox can. |
76
+ | **Pluggable substrate** | [kvgit](https://github.com/ashenfad/kvgit) (versioned), [AgentFS](https://github.com/tursodatabase/agentfs), or a plain directory -- same tools. |
77
+ | **Thin adapters** | [agno](https://github.com/agno-agi/agno) toolkit and an MCP server over one core. |
78
+
79
+ > **What the sandbox is (and isn't).** In-process, the Python sandbox
80
+ > ([sandtrap](https://github.com/ashenfad/sandtrap)) is a **walled garden
81
+ > for cooperative LLM-generated code** — it gates what agent code can
82
+ > reach (modules, host objects, the filesystem) to an allowlist you
83
+ > control (safe stdlib on by default, everything else opt-in), not a
84
+ > hardened boundary against code *trying* to escape. That's the right
85
+ > posture for your own agent's code. When you need a real boundary
86
+ > (untrusted code, or serving to anonymous clients), escalate with
87
+ > `isolation="process"` / `"kernel"`. Full framing in the
88
+ > [design notes](docs/design.md).
89
+
90
+ ## The API in one glance
91
+
92
+ ```python
93
+ from nontainer import workspace
94
+
95
+ ws = workspace("user-42") # versioned; a kvgit branch per session
96
+
97
+ ws.terminal("mkdir -p data && echo 'a,b\n1,2' > data/in.csv")
98
+ r = ws.run_python("""
99
+ import csv
100
+ rows = list(csv.reader(open('data/in.csv'))) # sees the shell's file
101
+ cache['n_rows'] = len(rows) # persists across the session
102
+ print(rows)
103
+ """)
104
+
105
+ r.checkpoint # commit id this call produced; ws.restore(it) undoes it
106
+ fork = ws.fork("what-if") # O(1) branch; the original is untouched
107
+ ws.rollback(steps=1) # or time-travel by steps
108
+ ```
109
+
110
+ Adapters are one import away:
111
+
112
+ ```python
113
+ from nontainer.adapters.agno import WorkspaceTools # agno Toolkit
114
+ # or: python -m nontainer.adapters.mcp --session s1 # MCP server (stdio)
115
+ ```
116
+
117
+ ## Substrates
118
+
119
+ `WorkspaceProvider` is the pluggable seam -- one filesystem-and-KV protocol,
120
+ **capability flags** instead of pretended equivalence:
121
+
122
+ | Provider | versioned | `cheap_fork` | `sql_audit` |
123
+ |---|---|---|---|
124
+ | kvgit (default) | ✅ | ✅ O(1) | ❌ |
125
+ | plain dir | ❌ | ❌ | ❌ |
126
+ | AgentFS (spike) | ❌ | ❌ | ✅ |
127
+
128
+ kvgit for fork/undo/audit, `dir` when agent code needs real files (C
129
+ extensions, subprocesses), AgentFS for the one-file-artifact + SQL story --
130
+ or bring your own provider. Full guidance in the [API reference](docs/api.md).
131
+
132
+ ## App handlers (the `[apps]` extra)
133
+
134
+ Agents author full-stack apps: a Preact/HTM frontend plus **request
135
+ handlers** -- serverless semantics, not resident servers. A file's path is
136
+ its route (`/app/api/scores.py` → `/api/scores`), its exported `get`/`post`
137
+ are the verbs. The agent builds and verifies entirely in-loop: a `curl`
138
+ builtin hits the dispatcher from the terminal, and `test_app` runs the app
139
+ headlessly through Playwright with the workspace as the origin -- no server,
140
+ no Node. To share it, publish a **frozen snapshot**: `build_router` serves
141
+ the app read-only and concurrently at `/apps/{token}/...`; mutable app state
142
+ lives in an external store injected via `host_objects`, not the (frozen)
143
+ workspace.
144
+
145
+ Full design -- handler contract, execution model, test_app DSL,
146
+ serving/threat model: [docs/apps.md](docs/apps.md).
147
+
148
+ ## Related work
149
+
150
+ - **Cloud sandboxes** (E2B, Daytona, Modal, Fly Sprites): real isolation,
151
+ real infra. They have persistence; none have history, forking, or
152
+ in-process host-object access.
153
+ - **[mcp-run-python](https://github.com/pydantic/mcp-run-python)** (Pydantic):
154
+ the incumbent local run-python (Pyodide-in-Deno). Stateless per call, no
155
+ workspace, needs Deno.
156
+ - **[AgentFS](https://turso.tech/blog/agentfs)** (Turso): SQLite-backed
157
+ agent FS + KV + SQL-queryable audit, snapshots by file copy. It comes at
158
+ the problem from storage where nontainer comes from execution -- and
159
+ nontainer runs on it as one of its backends.
160
+ - **[Val Town](https://www.val.town/)**: agents-deploying-endpoints as a
161
+ polished cloud product (TS). The handler design here is the self-hosted,
162
+ session-scoped, Python, versioned take on the same instinct.
163
+
164
+ ## Part of the agex stack
165
+
166
+ nontainer composes [kvgit](https://github.com/ashenfad/kvgit),
167
+ [monkeyfs](https://github.com/ashenfad/monkeyfs),
168
+ [termish](https://github.com/ashenfad/termish), and
169
+ [sandtrap](https://github.com/ashenfad/sandtrap) -- each independently
170
+ useful, each zero/minimal-dep. [agex](https://github.com/ashenfad/agex) is
171
+ the full agent framework over the same substrate; nontainer is the
172
+ environment layer alone, offered to someone else's loop.
173
+
174
+ ## Documentation
175
+
176
+ - [Quick Start](docs/quick-start.md) -- first workspace, sandbox config,
177
+ backends, adapters, the apps loop; runnable examples
178
+ - [API Reference](docs/api.md) -- every class, method, and flag
179
+ - [Design notes](docs/design.md) -- why it's shaped this way (execution
180
+ model, commit granularity, tool exposure) and what's still ahead
181
+ - [Apps design](docs/apps.md) -- handler contract, execution model,
182
+ test_app, serving/threat model
183
+ - [Examples](examples/) -- live agno agents: a data analyst
184
+ (`analyst.py`) and a build-and-verify web app (`webapp.py`)
185
+
186
+ ## Install
187
+
188
+ ```bash
189
+ pip install nontainer # workspace + terminal + run_python
190
+ pip install nontainer[agno] # + agno Toolkit adapter
191
+ pip install nontainer[mcp] # + MCP server (python -m nontainer.adapters.mcp)
192
+ pip install nontainer[apps] # + handlers/curl, Playwright test_app, serving router
193
+ pip install nontainer[agentfs] # + AgentFS substrate (agentfs-sdk)
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT