nullspace-sdk 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.
- nullspace_sdk-0.1.0/.gitignore +61 -0
- nullspace_sdk-0.1.0/CHANGELOG.md +72 -0
- nullspace_sdk-0.1.0/PKG-INFO +416 -0
- nullspace_sdk-0.1.0/README.md +376 -0
- nullspace_sdk-0.1.0/pyproject.toml +80 -0
- nullspace_sdk-0.1.0/src/nullspace/__init__.py +354 -0
- nullspace_sdk-0.1.0/src/nullspace/_cli_schema.py +230 -0
- nullspace_sdk-0.1.0/src/nullspace/_config.py +90 -0
- nullspace_sdk-0.1.0/src/nullspace/_exit_codes.py +108 -0
- nullspace_sdk-0.1.0/src/nullspace/_fields.py +16 -0
- nullspace_sdk-0.1.0/src/nullspace/_filesystem_core.py +4553 -0
- nullspace_sdk-0.1.0/src/nullspace/_mcp_server.py +215 -0
- nullspace_sdk-0.1.0/src/nullspace/cli.py +6339 -0
- nullspace_sdk-0.1.0/src/nullspace/client.py +1184 -0
- nullspace_sdk-0.1.0/src/nullspace/code_interpreter.py +583 -0
- nullspace_sdk-0.1.0/src/nullspace/commands.py +393 -0
- nullspace_sdk-0.1.0/src/nullspace/desktop.py +572 -0
- nullspace_sdk-0.1.0/src/nullspace/errors.py +300 -0
- nullspace_sdk-0.1.0/src/nullspace/files.py +568 -0
- nullspace_sdk-0.1.0/src/nullspace/git.py +1034 -0
- nullspace_sdk-0.1.0/src/nullspace/lifecycle.py +1048 -0
- nullspace_sdk-0.1.0/src/nullspace/monitor.py +268 -0
- nullspace_sdk-0.1.0/src/nullspace/process.py +37 -0
- nullspace_sdk-0.1.0/src/nullspace/pty.py +483 -0
- nullspace_sdk-0.1.0/src/nullspace/sandbox.py +1479 -0
- nullspace_sdk-0.1.0/src/nullspace/snapshot.py +109 -0
- nullspace_sdk-0.1.0/src/nullspace/template.py +3702 -0
- nullspace_sdk-0.1.0/src/nullspace/types.py +1936 -0
- nullspace_sdk-0.1.0/src/nullspace/volume.py +419 -0
- nullspace_sdk-0.1.0/src/nullspace/volume_files.py +936 -0
- nullspace_sdk-0.1.0/tests/__init__.py +0 -0
- nullspace_sdk-0.1.0/tests/conftest.py +116 -0
- nullspace_sdk-0.1.0/tests/integration_helpers.py +1176 -0
- nullspace_sdk-0.1.0/tests/test_chart_extractor.py +46 -0
- nullspace_sdk-0.1.0/tests/test_cli.py +5422 -0
- nullspace_sdk-0.1.0/tests/test_cli_docs.py +143 -0
- nullspace_sdk-0.1.0/tests/test_cli_dx.py +79 -0
- nullspace_sdk-0.1.0/tests/test_client.py +245 -0
- nullspace_sdk-0.1.0/tests/test_code_interpreter.py +405 -0
- nullspace_sdk-0.1.0/tests/test_code_interpreter_sdk_sync_async.py +552 -0
- nullspace_sdk-0.1.0/tests/test_commands.py +711 -0
- nullspace_sdk-0.1.0/tests/test_connect_bucket.py +426 -0
- nullspace_sdk-0.1.0/tests/test_connect_bucket_integration.py +792 -0
- nullspace_sdk-0.1.0/tests/test_desktop.py +597 -0
- nullspace_sdk-0.1.0/tests/test_desktop_firecracker_integration.py +828 -0
- nullspace_sdk-0.1.0/tests/test_files.py +2716 -0
- nullspace_sdk-0.1.0/tests/test_firecracker_code_interpreter_integration.py +600 -0
- nullspace_sdk-0.1.0/tests/test_firecracker_code_interpreter_sdk_sync_async.py +481 -0
- nullspace_sdk-0.1.0/tests/test_firecracker_filesystem_parity.py +814 -0
- nullspace_sdk-0.1.0/tests/test_firecracker_sdk_user_journeys.py +2053 -0
- nullspace_sdk-0.1.0/tests/test_firecracker_uploads_integration.py +245 -0
- nullspace_sdk-0.1.0/tests/test_git.py +870 -0
- nullspace_sdk-0.1.0/tests/test_integration.py +1226 -0
- nullspace_sdk-0.1.0/tests/test_integration_helpers.py +116 -0
- nullspace_sdk-0.1.0/tests/test_jupyter_server.py +98 -0
- nullspace_sdk-0.1.0/tests/test_lifecycle.py +998 -0
- nullspace_sdk-0.1.0/tests/test_lifecycle_integration.py +1240 -0
- nullspace_sdk-0.1.0/tests/test_mcp_server.py +65 -0
- nullspace_sdk-0.1.0/tests/test_monitor.py +226 -0
- nullspace_sdk-0.1.0/tests/test_path_contract_docs.py +43 -0
- nullspace_sdk-0.1.0/tests/test_pty.py +476 -0
- nullspace_sdk-0.1.0/tests/test_pty_integration.py +97 -0
- nullspace_sdk-0.1.0/tests/test_quickstart_integration.py +52 -0
- nullspace_sdk-0.1.0/tests/test_sandbox.py +2549 -0
- nullspace_sdk-0.1.0/tests/test_sandbox_integration.py +1734 -0
- nullspace_sdk-0.1.0/tests/test_sdk_cli_beta_surface_matrix.py +191 -0
- nullspace_sdk-0.1.0/tests/test_snapshot.py +123 -0
- nullspace_sdk-0.1.0/tests/test_template.py +3054 -0
- nullspace_sdk-0.1.0/tests/test_template_builder_surface.py +649 -0
- nullspace_sdk-0.1.0/tests/test_template_docs.py +573 -0
- nullspace_sdk-0.1.0/tests/test_volume.py +1227 -0
- nullspace_sdk-0.1.0/uv.lock +1045 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
/target/
|
|
3
|
+
/artifacts/
|
|
4
|
+
/data/
|
|
5
|
+
/.local/
|
|
6
|
+
|
|
7
|
+
# Repo-local runtime state now lives under `.local/` by default. Keep the old
|
|
8
|
+
# root-level paths ignored as a safety net for stray local leftovers.
|
|
9
|
+
|
|
10
|
+
# Rootfs images
|
|
11
|
+
*.ext4
|
|
12
|
+
*.img
|
|
13
|
+
|
|
14
|
+
# Firecracker binaries
|
|
15
|
+
firecracker
|
|
16
|
+
vmlinux
|
|
17
|
+
!crates/vmm/src/firecracker/
|
|
18
|
+
!crates/vmm/src/firecracker/**
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
|
|
31
|
+
# Python
|
|
32
|
+
__pycache__/
|
|
33
|
+
*.pyc
|
|
34
|
+
.venv/
|
|
35
|
+
*.egg-info/
|
|
36
|
+
dist/
|
|
37
|
+
|
|
38
|
+
# Node / TypeScript
|
|
39
|
+
node_modules/
|
|
40
|
+
apps/console/dist/
|
|
41
|
+
apps/console/*.tsbuildinfo
|
|
42
|
+
apps/console/tailwind.config.js
|
|
43
|
+
apps/console/tailwind.config.d.ts
|
|
44
|
+
apps/console/vite.config.js
|
|
45
|
+
apps/console/vite.config.d.ts
|
|
46
|
+
sdks/typescript/dist/
|
|
47
|
+
apps/console/playwright-report/
|
|
48
|
+
apps/console/test-results/
|
|
49
|
+
|
|
50
|
+
# Environment
|
|
51
|
+
.env
|
|
52
|
+
.env.*
|
|
53
|
+
!.env.example
|
|
54
|
+
nullspace-metal-key.pem
|
|
55
|
+
|
|
56
|
+
# discord-bridge cli state
|
|
57
|
+
.state/
|
|
58
|
+
|
|
59
|
+
# Supabase local state
|
|
60
|
+
infra/supabase/.branches/
|
|
61
|
+
infra/supabase/.temp/
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `nullspace-sdk` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/) starting at `0.1.0`. The
|
|
6
|
+
import package is `nullspace`; the CLI command is `nullspace`.
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-04-30
|
|
9
|
+
|
|
10
|
+
First private-beta SDK package. Native-only surface; no E2B compatibility shim.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Sandbox`, `AsyncSandbox`, `SandboxPaginator`, `AsyncSandboxPaginator` —
|
|
15
|
+
on-demand Firecracker microVMs (`create`, `commands.run`, `files.*`,
|
|
16
|
+
`get_url`, `kill`, `pause`, `resume`, `fork`).
|
|
17
|
+
- `Snapshot` / `AsyncSnapshot` — snapshot and resume.
|
|
18
|
+
- `Volume` / `AsyncVolume` plus `volume.files` direct file management
|
|
19
|
+
(Firecracker-only beta surface).
|
|
20
|
+
- `Template` / `TemplateBuild` plus readiness probes
|
|
21
|
+
(`CommandReadinessProbe`, `HttpReadinessProbe`, `PortReadinessProbe`,
|
|
22
|
+
`TimeoutReadinessProbe`) and helpers (`wait_for_file`, `wait_for_port`,
|
|
23
|
+
`wait_for_process`, `wait_for_timeout`, `wait_for_url`).
|
|
24
|
+
- `CodeInterpreter` / `AsyncCodeInterpreter` — Jupyter-style execution.
|
|
25
|
+
- `Monitor` / `AsyncMonitor` — sandbox metrics and process events.
|
|
26
|
+
- `Lifecycle` / `AsyncLifecycle` plus webhook delivery types — lifecycle
|
|
27
|
+
event subscriptions.
|
|
28
|
+
- `nullspace.__version__` — derived from `importlib.metadata`.
|
|
29
|
+
- `nullspace` CLI under the `[cli]` extra: `auth`, `sandbox`, `volume`,
|
|
30
|
+
`template`, `lifecycle` subcommands.
|
|
31
|
+
- Resumable file and directory uploads with progress callbacks; tar-based
|
|
32
|
+
directory uploads honor `.nullspaceignore`.
|
|
33
|
+
- Public exception types: `AuthError`, `BuildError`, `FileUploadError`,
|
|
34
|
+
`UploadChecksumError`, `UploadConflictError`, `UploadExpiredError`,
|
|
35
|
+
`UploadLimitError`, `UploadPathError`, `UploadStateError`,
|
|
36
|
+
`SandboxError`, `TimeoutError`, `NotFoundError`, `NotImplementedError`,
|
|
37
|
+
`GitError`, `BatchWriteError`.
|
|
38
|
+
|
|
39
|
+
### Distribution
|
|
40
|
+
|
|
41
|
+
- Distribution name: `nullspace-sdk`. Wheel is `py3-none-any`. Supported Python
|
|
42
|
+
versions: 3.11, 3.12, 3.13 on Linux and macOS.
|
|
43
|
+
- Optional extras: `[cli]` adds `click` and `rich`; `[dev]` adds the test
|
|
44
|
+
toolchain.
|
|
45
|
+
- Built as a standard wheel. Operators may publish the same artifact through
|
|
46
|
+
PyPI trusted publishing or provide an immutable private-beta wheel/tag
|
|
47
|
+
through the handout install spec.
|
|
48
|
+
|
|
49
|
+
### Known beta limitations
|
|
50
|
+
|
|
51
|
+
- **Volumes are a Firecracker-only SDK surface.** Shared mounts use
|
|
52
|
+
close-to-open visibility, atomic rename, and `flock` plus traditional
|
|
53
|
+
`fcntl` record locks.
|
|
54
|
+
- **Template build and logging are Firecracker-only.** Docker is not part
|
|
55
|
+
of the supported long-term contract for this surface.
|
|
56
|
+
- **Path contract.** `/workspace` remains the default mutable work tree;
|
|
57
|
+
filesystem APIs accept other absolute sandbox-scoped paths
|
|
58
|
+
(`/tmp/...`, `/data/...`, `/srv/app/...`); user-controlled `cwd` and
|
|
59
|
+
mount-path inputs reject reserved runtime paths under
|
|
60
|
+
`/workspace/.nullspace`.
|
|
61
|
+
- **No session caps.** Persistence is GA; there are no 24-hour limits,
|
|
62
|
+
unlike alternatives in this category.
|
|
63
|
+
- **Breaking changes are allowed during the `0.x` series** and will be
|
|
64
|
+
recorded here.
|
|
65
|
+
|
|
66
|
+
### Documentation
|
|
67
|
+
|
|
68
|
+
- README at `sdks/python/README.md`.
|
|
69
|
+
- Hosted private-beta docs: `https://docs.13-215-85-171.sslip.io`.
|
|
70
|
+
- Sprint planning ticket:
|
|
71
|
+
`docs/engineering/sprints/sprint_6_derek/09_python_sdk_pypi_release.md`.
|
|
72
|
+
- Operator runbook: `docs/engineering/runbooks/python-sdk-release.md`.
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nullspace-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cloud sandboxes for AI agents
|
|
5
|
+
Project-URL: Homepage, https://docs.13-215-85-171.sslip.io
|
|
6
|
+
Project-URL: Repository, https://github.com/catamaran-research/nullspace
|
|
7
|
+
Project-URL: Documentation, https://docs.13-215-85-171.sslip.io/sdk-reference
|
|
8
|
+
Project-URL: Issues, https://github.com/catamaran-research/nullspace/issues
|
|
9
|
+
Author-email: Nullspace <team@nullspace.dev>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: agents,ai,cloud,firecracker,microvm,sandbox
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pathspec>=0.12
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: websockets>=13.0
|
|
26
|
+
Provides-Extra: cli
|
|
27
|
+
Requires-Dist: click>=8.0; extra == 'cli'
|
|
28
|
+
Requires-Dist: rich>=13.0; extra == 'cli'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: click>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mcp[cli]<2,>=1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: rich>=13.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: zstandard>=0.23; extra == 'dev'
|
|
37
|
+
Provides-Extra: mcp
|
|
38
|
+
Requires-Dist: mcp[cli]<2,>=1; extra == 'mcp'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Nullspace
|
|
42
|
+
|
|
43
|
+
Open-source cloud sandboxes for AI agents. Create isolated Linux environments on demand, run commands, read/write files, and expose ports — all from a few lines of Python.
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/nullspace-sdk/)
|
|
46
|
+
[](https://github.com/catamaran-research/nullspace/blob/main/LICENSE)
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv pip install "${NULLSPACE_SDK_INSTALL_SPEC:-nullspace-sdk}"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For private beta, `NULLSPACE_SDK_INSTALL_SPEC` may point at a pinned PyPI
|
|
55
|
+
version, a hosted wheel URL, or a private repo tag supplied in the handout.
|
|
56
|
+
|
|
57
|
+
For CLI usage:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv pip install "nullspace-sdk[cli]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or install from source:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv pip install -e . # from this directory
|
|
67
|
+
uv pip install -e ./sdks/python # from repo root
|
|
68
|
+
uv pip install -e ".[cli]" # from this directory, with the CLI
|
|
69
|
+
uv pip install -e "./sdks/python[cli]" # from repo root, with the CLI
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Docs:
|
|
73
|
+
|
|
74
|
+
- [Hosted endpoints](../../docs/site/reference/hosted-endpoints.mdx)
|
|
75
|
+
- [Python SDK Overview](../../docs/site/guides/python-sdk/overview.mdx)
|
|
76
|
+
- [Python CLI Guide](../../docs/site/guides/python-sdk/cli.mdx)
|
|
77
|
+
|
|
78
|
+
## Quickstart
|
|
79
|
+
|
|
80
|
+
### Synchronous
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from nullspace import Sandbox
|
|
84
|
+
|
|
85
|
+
with Sandbox.create() as sandbox:
|
|
86
|
+
# Run a command
|
|
87
|
+
result = sandbox.commands.run("echo 'Hello from Nullspace!'", shell=True)
|
|
88
|
+
print(result.stdout)
|
|
89
|
+
|
|
90
|
+
# Work with files
|
|
91
|
+
sandbox.files.write("/hello.txt", "world")
|
|
92
|
+
print(sandbox.files.read("/hello.txt"))
|
|
93
|
+
|
|
94
|
+
# Expose a port
|
|
95
|
+
server = sandbox.commands.run(
|
|
96
|
+
"python3 -m http.server 8080",
|
|
97
|
+
background=True,
|
|
98
|
+
shell=True,
|
|
99
|
+
)
|
|
100
|
+
print(sandbox.get_url(8080))
|
|
101
|
+
server.kill()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Asynchronous
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import asyncio
|
|
108
|
+
from nullspace import AsyncSandbox
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def main() -> None:
|
|
112
|
+
async with await AsyncSandbox.create() as sandbox:
|
|
113
|
+
result = await sandbox.commands.run("echo 'Hello from Nullspace!'", shell=True)
|
|
114
|
+
print(result.stdout)
|
|
115
|
+
|
|
116
|
+
await sandbox.files.write("/hello.txt", "world")
|
|
117
|
+
print(await sandbox.files.read("/hello.txt"))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
asyncio.run(main())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `shell=True` for normal authored command strings. Use `args` when you want
|
|
124
|
+
exact argument boundaries without shell parsing. The SDK does not infer shell
|
|
125
|
+
mode from a plain string, and `shell=True` cannot be combined with `args`.
|
|
126
|
+
|
|
127
|
+
Path contract note:
|
|
128
|
+
|
|
129
|
+
- No breaking change: `/workspace` remains the default mutable work tree for
|
|
130
|
+
agent and repo-style flows.
|
|
131
|
+
- General filesystem APIs also accept valid sandbox-scoped absolute paths such
|
|
132
|
+
as `/tmp/...`, `/data/...`, and `/srv/app/...`.
|
|
133
|
+
- Endpoint-specific path rules remain explicit: user-controlled `cwd` and
|
|
134
|
+
mount-path inputs still reject reserved runtime paths under
|
|
135
|
+
`/workspace/.nullspace`, and `/context/...` only exists when a source-mount
|
|
136
|
+
capable surface provides it.
|
|
137
|
+
|
|
138
|
+
## File Uploads
|
|
139
|
+
|
|
140
|
+
Use `write()` for in-memory strings and bytes. Use `upload_file()` or `upload()`
|
|
141
|
+
when the source is a local file path or readable binary stream. Use
|
|
142
|
+
`upload_dir()` or `upload()` when the source is a local directory path.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from nullspace import Sandbox
|
|
146
|
+
|
|
147
|
+
with Sandbox.create() as sandbox:
|
|
148
|
+
result = sandbox.files.upload_file("./dist/app.tar.gz")
|
|
149
|
+
print(result.transport, result.target_path, result.bytes_uploaded)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Without an explicit destination, `result.target_path` is returned as the
|
|
153
|
+
resolved absolute sandbox path. With the default path contract, that is
|
|
154
|
+
typically `/workspace/app.tar.gz`.
|
|
155
|
+
|
|
156
|
+
The SDK picks direct upload for smaller known-length files and switches to
|
|
157
|
+
resumable upload for larger files automatically. You can force resumable mode
|
|
158
|
+
and wire a progress callback:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from nullspace import FileUploadError, Sandbox
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def on_progress(event) -> None:
|
|
165
|
+
print(event.phase, event.bytes_completed, event.bytes_total, event.transport)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
with Sandbox.create() as sandbox:
|
|
169
|
+
try:
|
|
170
|
+
sandbox.files.upload_file(
|
|
171
|
+
"./dist/model.bin",
|
|
172
|
+
"/data/model.bin",
|
|
173
|
+
resumable=True,
|
|
174
|
+
progress=on_progress,
|
|
175
|
+
)
|
|
176
|
+
except FileUploadError as exc:
|
|
177
|
+
if exc.upload_id:
|
|
178
|
+
resumed = sandbox.files.resume_upload(
|
|
179
|
+
exc.upload_id,
|
|
180
|
+
"./dist/model.bin",
|
|
181
|
+
progress=on_progress,
|
|
182
|
+
)
|
|
183
|
+
print(resumed.upload_id, resumed.bytes_uploaded)
|
|
184
|
+
else:
|
|
185
|
+
raise
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Local directory paths use resumable tar upload. `upload()` dispatches directory
|
|
189
|
+
paths to `upload_dir()` with the default `merge` conflict policy:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from nullspace import Sandbox
|
|
193
|
+
|
|
194
|
+
with Sandbox.create() as sandbox:
|
|
195
|
+
result = sandbox.files.upload_dir(
|
|
196
|
+
"./src",
|
|
197
|
+
"/data/src",
|
|
198
|
+
ignore_patterns=["*.pyc", "!pkg/__init__.py"],
|
|
199
|
+
)
|
|
200
|
+
print(result.kind, result.file_count, result.target_path)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Directory uploads honor `.nullspaceignore` from the source root, append any
|
|
204
|
+
explicit `ignore_patterns` after it, preserve symlinks, and do not implicitly
|
|
205
|
+
exclude `.git` or other dot-directories.
|
|
206
|
+
|
|
207
|
+
If a resumable directory upload fails mid-transfer, pass the same source
|
|
208
|
+
directory back to `resume_upload()`:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from nullspace import FileUploadError, Sandbox
|
|
212
|
+
|
|
213
|
+
with Sandbox.create() as sandbox:
|
|
214
|
+
try:
|
|
215
|
+
sandbox.files.upload_dir("./src", "/data/src")
|
|
216
|
+
except FileUploadError as exc:
|
|
217
|
+
if exc.upload_id:
|
|
218
|
+
resumed = sandbox.files.resume_upload(exc.upload_id, "./src")
|
|
219
|
+
print(resumed.upload_id, resumed.bytes_uploaded)
|
|
220
|
+
else:
|
|
221
|
+
raise
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## CLI Uploads
|
|
225
|
+
|
|
226
|
+
The bundled CLI now exposes the same upload surface under
|
|
227
|
+
`nullspace sandbox upload`:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
nullspace sandbox upload sb_123 ./dist/app.tar.gz /data/app.tar.gz
|
|
231
|
+
nullspace sandbox upload sb_123 ./src /data/src --exclude '*.pyc'
|
|
232
|
+
nullspace sandbox upload sb_123 - /tmp/stdin.bin
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Resumable failures print a concrete next command when the source can be replayed:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
nullspace sandbox upload sb_123 ./big.iso --resume up_123
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use `--dry-run` to preview local file or directory uploads, and add `--json`
|
|
242
|
+
for machine-readable output.
|
|
243
|
+
|
|
244
|
+
## Authentication
|
|
245
|
+
|
|
246
|
+
Set your API key as an environment variable:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
export NULLSPACE_API_KEY=ns_live_...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For the bundled CLI, you can also save an API key with:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
nullspace auth login
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
That writes `~/.nullspace/config.json` for backward compatibility. The SDK
|
|
259
|
+
accepts explicit `api_key=` / `base_url=` arguments; without those, the SDK and
|
|
260
|
+
CLI read environment variables, project `.env`,
|
|
261
|
+
`~/.config/nullspace/config.json`, then legacy `~/.nullspace/config.json`.
|
|
262
|
+
The legacy config key `api_url` is accepted as an alias for `base_url`.
|
|
263
|
+
|
|
264
|
+
Or pass it directly:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from nullspace import Sandbox
|
|
268
|
+
|
|
269
|
+
sandbox = Sandbox.create(api_key="ns_live_...")
|
|
270
|
+
sandbox.kill()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Auto-Resume
|
|
274
|
+
|
|
275
|
+
Use `auto_resume=True` with paused timeout behavior when a sandbox should wake
|
|
276
|
+
on its public URL after hibernating:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from nullspace import Sandbox
|
|
280
|
+
|
|
281
|
+
sandbox = Sandbox.create(on_timeout="pause", auto_resume=True)
|
|
282
|
+
url = sandbox.get_url(8080)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
After the sandbox pauses, inbound HTTP or websocket traffic to its public URL
|
|
286
|
+
wakes it and the original request is forwarded once the resumed execution is
|
|
287
|
+
ready. `Sandbox.connect(id)` is still an explicit reconnect operation: it
|
|
288
|
+
resumes a paused sandbox ID by snapshot route and returns the new running
|
|
289
|
+
execution. `Sandbox.get_info_by_id(id)` is read-only and does not wake paused
|
|
290
|
+
sandboxes.
|
|
291
|
+
|
|
292
|
+
## Features
|
|
293
|
+
|
|
294
|
+
- **On-demand sandboxes** — spin up isolated Linux environments in milliseconds
|
|
295
|
+
- **Command execution** — run shell commands and capture stdout/stderr
|
|
296
|
+
- **File I/O** — read, write, list, and search files inside the sandbox
|
|
297
|
+
- **Persistent volumes** — tenant-scoped shared volume objects, direct `volume.files` management in Python and CLI, by-name lookup, and canonical `volumes=[...]` sandbox mounts
|
|
298
|
+
- **Port exposure** — expose sandbox ports via public URLs
|
|
299
|
+
- **PTY sessions** — interactive terminal sessions over WebSocket
|
|
300
|
+
- **PTY REST session IDs** — canonical session identity is numeric PID
|
|
301
|
+
- **Snapshot & resume** — hibernate sandboxes and resume them later
|
|
302
|
+
- **Fork** — clone a running sandbox (unique to Nullspace)
|
|
303
|
+
|
|
304
|
+
## Current Limits
|
|
305
|
+
|
|
306
|
+
- **Volumes** are currently a Firecracker-only SDK surface. The Python SDK and
|
|
307
|
+
bundled CLI expose direct volume file management plus create-time mounts.
|
|
308
|
+
- Shared mounts use close-to-open visibility, atomic rename, and `flock` plus
|
|
309
|
+
traditional `fcntl` record locks; snapshot resume and fork remount them with
|
|
310
|
+
fresh internal leases before the new sandbox becomes ready. This remounts
|
|
311
|
+
external shared storage; it does not make Firecracker VM memory or mutable
|
|
312
|
+
rootfs snapshots portable across incompatible runtime hosts.
|
|
313
|
+
- The canonical mount shape is `volumes=[...]` with `ref`, `mount_path`,
|
|
314
|
+
optional `subpath`, and `read_only`.
|
|
315
|
+
|
|
316
|
+
## Persistent Volumes
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from nullspace import Sandbox, Volume
|
|
320
|
+
|
|
321
|
+
shared = Volume.create("team-data")
|
|
322
|
+
same_shared = Volume.from_name("team-data")
|
|
323
|
+
|
|
324
|
+
with Sandbox.create(volumes=[shared.mount("/workspace/shared")]) as sandbox:
|
|
325
|
+
sandbox.files.write("/workspace/shared/hello.txt", "persistent state")
|
|
326
|
+
print([attachment.mount_path for attachment in sandbox.volumes])
|
|
327
|
+
print(same_shared.id)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Direct volume file management uses the same persistent data without starting a
|
|
331
|
+
sandbox first:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from pathlib import Path
|
|
335
|
+
import tempfile
|
|
336
|
+
|
|
337
|
+
from nullspace import Volume
|
|
338
|
+
|
|
339
|
+
shared = Volume.from_name("team-data", create_if_missing=True)
|
|
340
|
+
shared.files.make_dir("/datasets")
|
|
341
|
+
shared.files.write("/datasets/hello.txt", "hello from direct volume access\n")
|
|
342
|
+
|
|
343
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
344
|
+
local_file = Path(tmpdir) / "artifact.txt"
|
|
345
|
+
local_file.write_text("uploaded from local disk\n", encoding="utf-8")
|
|
346
|
+
shared.files.upload_file(local_file, "/datasets/artifact.txt")
|
|
347
|
+
shared.files.download_file("/datasets/artifact.txt", Path(tmpdir) / "artifact-copy.txt")
|
|
348
|
+
shared.files.download_dir("/datasets", Path(tmpdir) / "datasets-copy")
|
|
349
|
+
|
|
350
|
+
print(shared.files.read("/datasets/hello.txt").strip())
|
|
351
|
+
print(shared.files.download_url("/datasets/artifact.txt"))
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
nullspace volume ls-files team-data /
|
|
356
|
+
nullspace volume upload team-data ./dist/model.bin /models/model.bin
|
|
357
|
+
nullspace volume download team-data /models/model.bin ./model.bin
|
|
358
|
+
nullspace volume download team-data /datasets/frontend ./frontend-copy
|
|
359
|
+
nullspace volume download team-data /datasets/frontend ./frontend-copy.tar --archive
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
See the full guides:
|
|
363
|
+
|
|
364
|
+
- [`docs/site/guides/python-sdk/volumes.mdx`](../../docs/site/guides/python-sdk/volumes.mdx)
|
|
365
|
+
|
|
366
|
+
## Templates
|
|
367
|
+
|
|
368
|
+
Template build and logging are Firecracker-only. Docker is not part of the supported long-term contract for this feature.
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from nullspace import Sandbox, Template, default_build_logger, wait_for_timeout
|
|
372
|
+
|
|
373
|
+
builder = (
|
|
374
|
+
Template()
|
|
375
|
+
.from_ubuntu_image("22.04")
|
|
376
|
+
.set_runtime_envs({"HELLO": "Hello from Nullspace!"})
|
|
377
|
+
.set_start_cmd(
|
|
378
|
+
"echo $HELLO > /tmp/boot.log",
|
|
379
|
+
readiness=wait_for_timeout(5_000),
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
build = Template.build(
|
|
384
|
+
builder,
|
|
385
|
+
name="hello-template",
|
|
386
|
+
on_log_entry=default_build_logger(),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Equivalent shorthand:
|
|
390
|
+
build = Template.build(builder, "hello-template:stable")
|
|
391
|
+
|
|
392
|
+
with Sandbox.create(template=build.name) as sandbox:
|
|
393
|
+
print(sandbox.files.read("/tmp/boot.log").strip())
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Use `Template.build_in_background(...)` plus `build.get_status(...)` for background builds, and `TemplateBuild.connect(...)` to reconnect to an existing build.
|
|
397
|
+
Ref-based management helpers include `Template.get_tags(...)`, `Template.assign_tags(...)`, and `Template.remove_tag(...)`.
|
|
398
|
+
|
|
399
|
+
For the full template guide and migration notes, see:
|
|
400
|
+
|
|
401
|
+
- [`docs/site/guides/python-sdk/templates.mdx`](../../docs/site/guides/python-sdk/templates.mdx)
|
|
402
|
+
- [`docs/site/guides/python-sdk/template-build-logging-migration.mdx`](../../docs/site/guides/python-sdk/template-build-logging-migration.mdx)
|
|
403
|
+
|
|
404
|
+
## Links
|
|
405
|
+
|
|
406
|
+
- [Hosted endpoints](../../docs/site/reference/hosted-endpoints.mdx)
|
|
407
|
+
- [GitHub](https://github.com/catamaran-research/nullspace)
|
|
408
|
+
- [Python SDK Overview](../../docs/site/guides/python-sdk/overview.mdx)
|
|
409
|
+
- [Python Template Guide](../../docs/site/guides/python-sdk/templates.mdx)
|
|
410
|
+
- [Python Template Build Migration Guide](../../docs/site/guides/python-sdk/template-build-logging-migration.mdx)
|
|
411
|
+
- [Documentation source](../../docs/site)
|
|
412
|
+
- [Changelog](./CHANGELOG.md)
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
Apache-2.0
|