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.
- nontainer-0.1.0/.github/workflows/publish.yml +33 -0
- nontainer-0.1.0/.github/workflows/test.yml +43 -0
- nontainer-0.1.0/.gitignore +8 -0
- nontainer-0.1.0/CHANGELOG.md +223 -0
- nontainer-0.1.0/LICENSE +21 -0
- nontainer-0.1.0/PKG-INFO +198 -0
- nontainer-0.1.0/README.md +150 -0
- nontainer-0.1.0/docs/api.md +473 -0
- nontainer-0.1.0/docs/apps.md +351 -0
- nontainer-0.1.0/docs/design.md +107 -0
- nontainer-0.1.0/docs/quick-start.md +206 -0
- nontainer-0.1.0/examples/_model.py +18 -0
- nontainer-0.1.0/examples/analyst.py +70 -0
- nontainer-0.1.0/examples/webapp.py +176 -0
- nontainer-0.1.0/nontainer/__init__.py +67 -0
- nontainer-0.1.0/nontainer/adapters/__init__.py +17 -0
- nontainer-0.1.0/nontainer/adapters/a2ui.py +438 -0
- nontainer-0.1.0/nontainer/adapters/agno.py +300 -0
- nontainer-0.1.0/nontainer/adapters/mcp.py +357 -0
- nontainer-0.1.0/nontainer/adapters/render.py +726 -0
- nontainer-0.1.0/nontainer/apps/__init__.py +69 -0
- nontainer-0.1.0/nontainer/apps/browser.py +172 -0
- nontainer-0.1.0/nontainer/apps/contract.py +198 -0
- nontainer-0.1.0/nontainer/apps/curl.py +178 -0
- nontainer-0.1.0/nontainer/apps/dispatch.py +450 -0
- nontainer-0.1.0/nontainer/apps/serve.py +159 -0
- nontainer-0.1.0/nontainer/apps/testapp.py +484 -0
- nontainer-0.1.0/nontainer/cache.py +124 -0
- nontainer-0.1.0/nontainer/editing.py +227 -0
- nontainer-0.1.0/nontainer/errors.py +25 -0
- nontainer-0.1.0/nontainer/hints.py +44 -0
- nontainer-0.1.0/nontainer/presets.py +323 -0
- nontainer-0.1.0/nontainer/protocol.py +198 -0
- nontainer-0.1.0/nontainer/providers/__init__.py +17 -0
- nontainer-0.1.0/nontainer/providers/agentfs.py +499 -0
- nontainer-0.1.0/nontainer/providers/dir.py +170 -0
- nontainer-0.1.0/nontainer/providers/kvgit.py +206 -0
- nontainer-0.1.0/nontainer/py.typed +0 -0
- nontainer-0.1.0/nontainer/skills.py +201 -0
- nontainer-0.1.0/nontainer/workspace.py +1265 -0
- nontainer-0.1.0/pyproject.toml +80 -0
- nontainer-0.1.0/tests/conftest.py +13 -0
- nontainer-0.1.0/tests/test_a2ui.py +490 -0
- nontainer-0.1.0/tests/test_agent_patterns.py +115 -0
- nontainer-0.1.0/tests/test_agentfs_provider.py +165 -0
- nontainer-0.1.0/tests/test_agno_adapter.py +204 -0
- nontainer-0.1.0/tests/test_apps_browser.py +79 -0
- nontainer-0.1.0/tests/test_apps_dispatch.py +590 -0
- nontainer-0.1.0/tests/test_apps_serving.py +244 -0
- nontainer-0.1.0/tests/test_apps_surface.py +129 -0
- nontainer-0.1.0/tests/test_apps_testapp.py +378 -0
- nontainer-0.1.0/tests/test_async_facades.py +48 -0
- nontainer-0.1.0/tests/test_concurrency.py +242 -0
- nontainer-0.1.0/tests/test_dir_provider.py +73 -0
- nontainer-0.1.0/tests/test_factory.py +32 -0
- nontainer-0.1.0/tests/test_file_tools.py +72 -0
- nontainer-0.1.0/tests/test_kvgit_provider.py +199 -0
- nontainer-0.1.0/tests/test_mcp_adapter.py +209 -0
- nontainer-0.1.0/tests/test_mounts_and_cache.py +91 -0
- nontainer-0.1.0/tests/test_presets.py +230 -0
- nontainer-0.1.0/tests/test_primer.py +85 -0
- nontainer-0.1.0/tests/test_process_isolation.py +175 -0
- nontainer-0.1.0/tests/test_protocol.py +19 -0
- nontainer-0.1.0/tests/test_put_get.py +58 -0
- nontainer-0.1.0/tests/test_render.py +125 -0
- nontainer-0.1.0/tests/test_result_checkpoints.py +85 -0
- nontainer-0.1.0/tests/test_run_python.py +209 -0
- nontainer-0.1.0/tests/test_skills.py +128 -0
- nontainer-0.1.0/tests/test_terminal.py +158 -0
- nontainer-0.1.0/tests/test_terminal_python_sys.py +86 -0
- nontainer-0.1.0/tests/test_ui_artifacts.py +452 -0
- 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,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()`).
|
nontainer-0.1.0/LICENSE
ADDED
|
@@ -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.
|
nontainer-0.1.0/PKG-INFO
ADDED
|
@@ -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
|