sandfs 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.
- sandfs-0.1.0/LICENSE +21 -0
- sandfs-0.1.0/PKG-INFO +194 -0
- sandfs-0.1.0/README.md +174 -0
- sandfs-0.1.0/docs/blue.md +57 -0
- sandfs-0.1.0/docs/publishing.md +32 -0
- sandfs-0.1.0/docs/vision.md +29 -0
- sandfs-0.1.0/pyproject.toml +35 -0
- sandfs-0.1.0/sandfs/__init__.py +30 -0
- sandfs-0.1.0/sandfs/adapters.py +58 -0
- sandfs-0.1.0/sandfs/exceptions.py +32 -0
- sandfs-0.1.0/sandfs/hooks.py +20 -0
- sandfs-0.1.0/sandfs/integrations.py +33 -0
- sandfs-0.1.0/sandfs/nodes.py +166 -0
- sandfs-0.1.0/sandfs/policies.py +55 -0
- sandfs-0.1.0/sandfs/providers.py +82 -0
- sandfs-0.1.0/sandfs/pyexec.py +82 -0
- sandfs-0.1.0/sandfs/shell.py +395 -0
- sandfs-0.1.0/sandfs/vfs.py +599 -0
- sandfs-0.1.0/tests/conftest.py +6 -0
- sandfs-0.1.0/tests/test_adapters.py +47 -0
- sandfs-0.1.0/tests/test_hooks.py +21 -0
- sandfs-0.1.0/tests/test_integration_adapters.py +18 -0
- sandfs-0.1.0/tests/test_persistence.py +32 -0
- sandfs-0.1.0/tests/test_policies.py +37 -0
- sandfs-0.1.0/tests/test_shell.py +67 -0
- sandfs-0.1.0/tests/test_snapshots.py +30 -0
- sandfs-0.1.0/tests/test_vfs.py +46 -0
- sandfs-0.1.0/tests/test_visibility.py +21 -0
sandfs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 George Lydakis
|
|
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.
|
sandfs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandfs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Composable virtual filesystem sandbox with a UNIX-like command surface for agents
|
|
5
|
+
Project-URL: Homepage, https://example.com/sandfs
|
|
6
|
+
Project-URL: Repository, https://example.com/sandfs.git
|
|
7
|
+
Author: sandfs contributors
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,in-memory,sandbox,virtual filesystem
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: System :: Filesystems
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sandfs
|
|
22
|
+
|
|
23
|
+
`sandfs` is an experimental Python package that implements an entirely virtual filesystem (VFS) that can be embedded inside AI agent tooling. Each sandbox keeps its own private directory tree, supports dynamic nodes that are hydrated on demand, and exposes a small UNIX-like command surface (`cd`, `ls`, `cat`, `grep`, `rg`, etc.) so agents can reuse familiar workflows.
|
|
24
|
+
|
|
25
|
+
> **Status**: design prototype. APIs are subject to change.
|
|
26
|
+
|
|
27
|
+
## Why?
|
|
28
|
+
|
|
29
|
+
Agent builders frequently need a scratch filesystem for planning, iterating on generated code, and testing hypotheses. Shipping a simple `exec` tool that proxies every bash command to the host disk is unsafe. `sandfs` keeps those operations inside a controlled, in-memory sandbox while still feeling like a mini Linux environment.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- In-memory directories and files that never touch the host disk unless exported.
|
|
34
|
+
- Dynamic nodes backed by callables (e.g., query a DB, fetch from an API, or generate text on the fly).
|
|
35
|
+
- Pure-Python shell/executor that understands a handful of GNU-style commands and can be extended with your own.
|
|
36
|
+
- Optional Python execution helper that evaluates snippets against the sandbox state only.
|
|
37
|
+
- Bridge to real GNU utilities via `host -p <path> <command>` which materializes the subtree and runs the host binary against it.
|
|
38
|
+
- Node policies (read-only, append-only, visibility labels) plus shell views so different agents see only the nodes they are allowed to.
|
|
39
|
+
- Write hooks and optimistic versions so hosts can flush files to external stores with conflict detection.
|
|
40
|
+
- Storage adapters and snapshots so you can mount external state and roll forward/back within a turn.
|
|
41
|
+
- Serialization helpers to snapshot or hydrate sandboxes (planned).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/lydakis/sandfs.git
|
|
47
|
+
# Once a PyPI release is available: pip install sandfs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from sandfs import VirtualFileSystem, SandboxShell
|
|
54
|
+
|
|
55
|
+
vfs = VirtualFileSystem()
|
|
56
|
+
vfs.write_file("/notes/todo.txt", "- build VFS\n- add rg support\n")
|
|
57
|
+
|
|
58
|
+
shell = SandboxShell(vfs)
|
|
59
|
+
print(shell.exec("ls /notes").stdout)
|
|
60
|
+
print(shell.exec("rg 'VFS' /notes").stdout)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Agent shell usage
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from sandfs import ProvidedNode, SandboxShell, VirtualFileSystem
|
|
67
|
+
|
|
68
|
+
vfs = VirtualFileSystem()
|
|
69
|
+
vfs.write_file("/workspace/app.py", "print('hello')\n")
|
|
70
|
+
|
|
71
|
+
def logs_provider(ctx):
|
|
72
|
+
return {"latest.log": ProvidedNode.file(content="generated on demand")}
|
|
73
|
+
|
|
74
|
+
vfs.mount_directory("/workspace/logs", logs_provider)
|
|
75
|
+
|
|
76
|
+
shell = SandboxShell(vfs)
|
|
77
|
+
shell.exec("cd /workspace")
|
|
78
|
+
print(shell.exec("ls").stdout)
|
|
79
|
+
print(shell.exec("tree").stdout)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Running host GNU tools
|
|
83
|
+
|
|
84
|
+
`SandboxShell` exposes `host` to materialize a subtree onto the host disk and invoke any installed command:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
host -p /workspace grep -n TODO app.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The example above exports `/workspace` into a temporary directory, runs the system `grep` inside it, then discards the files.
|
|
91
|
+
|
|
92
|
+
### Policies & views
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
vfs.set_policy(
|
|
96
|
+
"/blue/identity/persona.md",
|
|
97
|
+
NodePolicy(writable=False, classification="private", principals={"alice", "bob"}),
|
|
98
|
+
)
|
|
99
|
+
shell = SandboxShell(vfs, view=VisibilityView(classifications={"public"}, principals={"alice"}))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Agent shell mode
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
shell = SandboxShell(
|
|
106
|
+
vfs,
|
|
107
|
+
allowed_commands={"ls", "cat", "rg"},
|
|
108
|
+
max_output_bytes=8_192,
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Commands outside `allowed_commands` now return an error immediately, and outputs larger than `max_output_bytes` are rejected so partner-facing agents cannot dump oversized payloads.
|
|
113
|
+
|
|
114
|
+
### Storage adapters
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from sandfs import MemoryStorageAdapter, VirtualFileSystem
|
|
118
|
+
|
|
119
|
+
adapter = MemoryStorageAdapter(initial={"a.txt": "seed"})
|
|
120
|
+
vfs = VirtualFileSystem()
|
|
121
|
+
vfs.mount_storage("/data", adapter)
|
|
122
|
+
|
|
123
|
+
print(vfs.read_file("/data/a.txt"))
|
|
124
|
+
vfs.write_file("/data/a.txt", "update")
|
|
125
|
+
vfs.sync_storage("/data") # refresh from adapter if it changed externally
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`mount_storage` keeps the virtual tree synchronized with the adapter, while `sync_storage` refreshes the VFS from the latest adapter contents.
|
|
129
|
+
|
|
130
|
+
### Snapshots
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
snap = vfs.snapshot()
|
|
134
|
+
# run risky operations ...
|
|
135
|
+
vfs.restore(snap)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Snapshots capture the entire tree (including storage-backed nodes) so you can checkpoint before an agent action and roll back on failure.
|
|
139
|
+
|
|
140
|
+
## Repository layout
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
.
|
|
144
|
+
├── docs/ # design/vision notes
|
|
145
|
+
├── sandfs/ # library sources
|
|
146
|
+
└── tests/ # runtime smoke tests
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Local development
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
python -m venv .venv
|
|
153
|
+
source .venv/bin/activate
|
|
154
|
+
uv pip install -e .[dev]
|
|
155
|
+
uv run pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Persistence hooks
|
|
159
|
+
|
|
160
|
+
Register a write hook to flush files into your own store and use optimistic versions to avoid clobbering concurrent updates:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from sandfs import VirtualFileSystem
|
|
164
|
+
from sandfs.hooks import WriteEvent
|
|
165
|
+
|
|
166
|
+
vfs = VirtualFileSystem()
|
|
167
|
+
|
|
168
|
+
def flush(event: WriteEvent) -> None:
|
|
169
|
+
save_to_db(event.path, event.content, event.version)
|
|
170
|
+
|
|
171
|
+
vfs.register_write_hook("/blue/work", flush)
|
|
172
|
+
vfs.write_file("/blue/work/note.md", "draft")
|
|
173
|
+
vfs.write_file("/blue/work/note.md", "final", expected_version=1)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Integration hooks
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from sandfs.integrations import InboxRecorder
|
|
180
|
+
|
|
181
|
+
recorder = InboxRecorder()
|
|
182
|
+
recorder.attach(vfs, "/blue/inbox")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`PathEvent` (used under the hood) carries the path, event type (`create`, `update`, `delete`), and latest content so you can fan writes into staging queues or audit logs.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
190
|
+
|
|
191
|
+
## Further reading
|
|
192
|
+
|
|
193
|
+
- [Using sandfs with Blue](docs/blue.md)
|
|
194
|
+
- [Publishing sandfs](docs/publishing.md)
|
sandfs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# sandfs
|
|
2
|
+
|
|
3
|
+
`sandfs` is an experimental Python package that implements an entirely virtual filesystem (VFS) that can be embedded inside AI agent tooling. Each sandbox keeps its own private directory tree, supports dynamic nodes that are hydrated on demand, and exposes a small UNIX-like command surface (`cd`, `ls`, `cat`, `grep`, `rg`, etc.) so agents can reuse familiar workflows.
|
|
4
|
+
|
|
5
|
+
> **Status**: design prototype. APIs are subject to change.
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
Agent builders frequently need a scratch filesystem for planning, iterating on generated code, and testing hypotheses. Shipping a simple `exec` tool that proxies every bash command to the host disk is unsafe. `sandfs` keeps those operations inside a controlled, in-memory sandbox while still feeling like a mini Linux environment.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- In-memory directories and files that never touch the host disk unless exported.
|
|
14
|
+
- Dynamic nodes backed by callables (e.g., query a DB, fetch from an API, or generate text on the fly).
|
|
15
|
+
- Pure-Python shell/executor that understands a handful of GNU-style commands and can be extended with your own.
|
|
16
|
+
- Optional Python execution helper that evaluates snippets against the sandbox state only.
|
|
17
|
+
- Bridge to real GNU utilities via `host -p <path> <command>` which materializes the subtree and runs the host binary against it.
|
|
18
|
+
- Node policies (read-only, append-only, visibility labels) plus shell views so different agents see only the nodes they are allowed to.
|
|
19
|
+
- Write hooks and optimistic versions so hosts can flush files to external stores with conflict detection.
|
|
20
|
+
- Storage adapters and snapshots so you can mount external state and roll forward/back within a turn.
|
|
21
|
+
- Serialization helpers to snapshot or hydrate sandboxes (planned).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install git+https://github.com/lydakis/sandfs.git
|
|
27
|
+
# Once a PyPI release is available: pip install sandfs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from sandfs import VirtualFileSystem, SandboxShell
|
|
34
|
+
|
|
35
|
+
vfs = VirtualFileSystem()
|
|
36
|
+
vfs.write_file("/notes/todo.txt", "- build VFS\n- add rg support\n")
|
|
37
|
+
|
|
38
|
+
shell = SandboxShell(vfs)
|
|
39
|
+
print(shell.exec("ls /notes").stdout)
|
|
40
|
+
print(shell.exec("rg 'VFS' /notes").stdout)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Agent shell usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from sandfs import ProvidedNode, SandboxShell, VirtualFileSystem
|
|
47
|
+
|
|
48
|
+
vfs = VirtualFileSystem()
|
|
49
|
+
vfs.write_file("/workspace/app.py", "print('hello')\n")
|
|
50
|
+
|
|
51
|
+
def logs_provider(ctx):
|
|
52
|
+
return {"latest.log": ProvidedNode.file(content="generated on demand")}
|
|
53
|
+
|
|
54
|
+
vfs.mount_directory("/workspace/logs", logs_provider)
|
|
55
|
+
|
|
56
|
+
shell = SandboxShell(vfs)
|
|
57
|
+
shell.exec("cd /workspace")
|
|
58
|
+
print(shell.exec("ls").stdout)
|
|
59
|
+
print(shell.exec("tree").stdout)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Running host GNU tools
|
|
63
|
+
|
|
64
|
+
`SandboxShell` exposes `host` to materialize a subtree onto the host disk and invoke any installed command:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
host -p /workspace grep -n TODO app.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The example above exports `/workspace` into a temporary directory, runs the system `grep` inside it, then discards the files.
|
|
71
|
+
|
|
72
|
+
### Policies & views
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
vfs.set_policy(
|
|
76
|
+
"/blue/identity/persona.md",
|
|
77
|
+
NodePolicy(writable=False, classification="private", principals={"alice", "bob"}),
|
|
78
|
+
)
|
|
79
|
+
shell = SandboxShell(vfs, view=VisibilityView(classifications={"public"}, principals={"alice"}))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Agent shell mode
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
shell = SandboxShell(
|
|
86
|
+
vfs,
|
|
87
|
+
allowed_commands={"ls", "cat", "rg"},
|
|
88
|
+
max_output_bytes=8_192,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Commands outside `allowed_commands` now return an error immediately, and outputs larger than `max_output_bytes` are rejected so partner-facing agents cannot dump oversized payloads.
|
|
93
|
+
|
|
94
|
+
### Storage adapters
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from sandfs import MemoryStorageAdapter, VirtualFileSystem
|
|
98
|
+
|
|
99
|
+
adapter = MemoryStorageAdapter(initial={"a.txt": "seed"})
|
|
100
|
+
vfs = VirtualFileSystem()
|
|
101
|
+
vfs.mount_storage("/data", adapter)
|
|
102
|
+
|
|
103
|
+
print(vfs.read_file("/data/a.txt"))
|
|
104
|
+
vfs.write_file("/data/a.txt", "update")
|
|
105
|
+
vfs.sync_storage("/data") # refresh from adapter if it changed externally
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`mount_storage` keeps the virtual tree synchronized with the adapter, while `sync_storage` refreshes the VFS from the latest adapter contents.
|
|
109
|
+
|
|
110
|
+
### Snapshots
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
snap = vfs.snapshot()
|
|
114
|
+
# run risky operations ...
|
|
115
|
+
vfs.restore(snap)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Snapshots capture the entire tree (including storage-backed nodes) so you can checkpoint before an agent action and roll back on failure.
|
|
119
|
+
|
|
120
|
+
## Repository layout
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
.
|
|
124
|
+
├── docs/ # design/vision notes
|
|
125
|
+
├── sandfs/ # library sources
|
|
126
|
+
└── tests/ # runtime smoke tests
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Local development
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
python -m venv .venv
|
|
133
|
+
source .venv/bin/activate
|
|
134
|
+
uv pip install -e .[dev]
|
|
135
|
+
uv run pytest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Persistence hooks
|
|
139
|
+
|
|
140
|
+
Register a write hook to flush files into your own store and use optimistic versions to avoid clobbering concurrent updates:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from sandfs import VirtualFileSystem
|
|
144
|
+
from sandfs.hooks import WriteEvent
|
|
145
|
+
|
|
146
|
+
vfs = VirtualFileSystem()
|
|
147
|
+
|
|
148
|
+
def flush(event: WriteEvent) -> None:
|
|
149
|
+
save_to_db(event.path, event.content, event.version)
|
|
150
|
+
|
|
151
|
+
vfs.register_write_hook("/blue/work", flush)
|
|
152
|
+
vfs.write_file("/blue/work/note.md", "draft")
|
|
153
|
+
vfs.write_file("/blue/work/note.md", "final", expected_version=1)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Integration hooks
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from sandfs.integrations import InboxRecorder
|
|
160
|
+
|
|
161
|
+
recorder = InboxRecorder()
|
|
162
|
+
recorder.attach(vfs, "/blue/inbox")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`PathEvent` (used under the hood) carries the path, event type (`create`, `update`, `delete`), and latest content so you can fan writes into staging queues or audit logs.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
170
|
+
|
|
171
|
+
## Further reading
|
|
172
|
+
|
|
173
|
+
- [Using sandfs with Blue](docs/blue.md)
|
|
174
|
+
- [Publishing sandfs](docs/publishing.md)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Using sandfs with Blue
|
|
2
|
+
|
|
3
|
+
This document shows how a Blue runtime can mount `/blue`, enforce view-specific policies, attach inbox/log recorders, and wrap each turn in snapshots.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git+https://github.com/lydakis/sandfs.git
|
|
9
|
+
# once published: pip install sandfs
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Bootstrap a sandbox per chat
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from sandfs import MemoryStorageAdapter, NodePolicy, SandboxShell, VirtualFileSystem, VisibilityView
|
|
16
|
+
from sandfs.integrations import InboxRecorder
|
|
17
|
+
|
|
18
|
+
# 1. create the sandbox
|
|
19
|
+
vfs = VirtualFileSystem()
|
|
20
|
+
|
|
21
|
+
# 2. mount identity + workspaces with policies
|
|
22
|
+
vfs.mount_storage("/blue/identity", MemoryStorageAdapter(initial=identity_blob))
|
|
23
|
+
vfs.set_policy(
|
|
24
|
+
"/blue/identity/persona.md",
|
|
25
|
+
NodePolicy(writable=False, classification="private", principals={chat.owner_id}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 3. attach inbox/log recorders
|
|
29
|
+
inbox = InboxRecorder()
|
|
30
|
+
inbox.attach(vfs, "/blue/inbox")
|
|
31
|
+
|
|
32
|
+
# 4. configure the shell for the chat participants
|
|
33
|
+
view = VisibilityView(
|
|
34
|
+
classifications={"public", "partner"},
|
|
35
|
+
principals=set(chat.participant_ids),
|
|
36
|
+
)
|
|
37
|
+
shell = SandboxShell(
|
|
38
|
+
vfs,
|
|
39
|
+
view=view,
|
|
40
|
+
allowed_commands={"ls", "cat", "rg", "python"},
|
|
41
|
+
max_output_bytes=8_000,
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Per-turn workflow
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
snapshot = vfs.snapshot()
|
|
49
|
+
try:
|
|
50
|
+
result = shell.exec(agent_command)
|
|
51
|
+
process_inbox(inbox.events) # fan into Blue staging queues
|
|
52
|
+
finally:
|
|
53
|
+
vfs.restore(snapshot)
|
|
54
|
+
inbox.events.clear()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This keeps `/blue` isolated per turn while still letting the runtime flush writes into its storage layer.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Publishing sandfs
|
|
2
|
+
|
|
3
|
+
1. Ensure `pyproject.toml` has the correct `version` and changelog/README are up to date.
|
|
4
|
+
2. Build the package:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
uv build
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This produces distributions under `dist/`.
|
|
11
|
+
|
|
12
|
+
3. Publish to PyPI:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv publish --username <pypi-user>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`uv publish` will prompt for credentials or can read `PYPI_TOKEN`. Remove `--dry-run`; uv currently publishes directly.
|
|
19
|
+
|
|
20
|
+
If you prefer Twine:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m build
|
|
24
|
+
python -m twine upload dist/*
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Tag the release:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git tag -a v0.1.0 -m "sandfs v0.1.0"
|
|
31
|
+
git push origin v0.1.0
|
|
32
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# sandfs Vision
|
|
2
|
+
|
|
3
|
+
`sandfs` aims to provide an extensible, self-contained virtual filesystem (VFS) that can be embedded in agentic workflows. The VFS is designed to be backed by in-memory state, dynamically generated nodes, or live data sources that are exposed via provider callables. Agent developers can expose a single `exec` tool that proxies commands into the sandbox, enabling AIs to manipulate a private filesystem with familiar UNIX idioms.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
1. **Virtual-first** – The filesystem lives in memory and never touches the host disk unless explicitly exported.
|
|
8
|
+
2. **Dynamic data** – Nodes can be backed by callables (or adapters around DB queries, APIs, etc.) that materialize file contents or directory listings on demand.
|
|
9
|
+
3. **Command friendly** – The sandbox ships with a tiny command runner that understands a subset of GNU-style commands (`cd`, `ls`, `cat`, `grep`, `rg`, `tree`, etc.). New commands can be registered.
|
|
10
|
+
4. **Execution context isolation** – Python or shell snippets can be executed against the VFS without escaping it. All side-effects remain local to the sandbox state object.
|
|
11
|
+
5. **Composable layers** – Multiple providers can be mounted at different paths, enabling hybrid topologies (e.g., static project template + live DB dumps + scratch space).
|
|
12
|
+
|
|
13
|
+
## Proposed Architecture
|
|
14
|
+
|
|
15
|
+
- `sandfs.nodes` keeps lightweight dataclasses for directories, files, and symbolic links. Nodes expose a uniform `read()` / `write()` / `iterdir()` API and support lazy population.
|
|
16
|
+
- `sandfs.providers` defines the provider protocol (callables receiving a `NodeContext` and returning bytes/str) plus adapters for common scenarios (e.g., `StaticText`, `CallableProvider`).
|
|
17
|
+
- `sandfs.vfs.VirtualFileSystem` manages the tree, the current working directory, path resolution, mounting/unmounting providers, and basic file operations.
|
|
18
|
+
- `sandfs.shell.SandboxShell` hosts a registry of command handlers, parses user commands via `shlex`, and executes them against the VFS, returning a `CommandResult` object.
|
|
19
|
+
- `sandfs.pyexec.PythonExecutor` offers a helper to run Python code with a curated globals dict that includes the VFS helpers but forbids direct disk I/O by default.
|
|
20
|
+
|
|
21
|
+
## Roadmap Snapshot
|
|
22
|
+
|
|
23
|
+
1. Build the foundational VFS and directory/file nodes with provider support.
|
|
24
|
+
2. Layer on top a shell/executor that mimics common filesystem commands.
|
|
25
|
+
3. Add serialization/import/export helpers so sandboxes can be persisted or hydrated from templates.
|
|
26
|
+
4. Expand command coverage (pipelines, redirection, e.g., `head`, `tail`, `sed`) and integrate with long-lived agent sessions.
|
|
27
|
+
5. Offer host integration points, such as bridging selected directories to the real FS through adapters guarded by policies.
|
|
28
|
+
|
|
29
|
+
This document captures the initial architectural intent and can be revised as the package evolves.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.20"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sandfs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Composable virtual filesystem sandbox with a UNIX-like command surface for agents"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "sandfs contributors" }
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
keywords = ["virtual filesystem", "agents", "sandbox", "in-memory"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Topic :: System :: Filesystems",
|
|
21
|
+
"Topic :: Software Development :: Libraries"
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=7.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://example.com/sandfs"
|
|
32
|
+
Repository = "https://example.com/sandfs.git"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.metadata]
|
|
35
|
+
allow-direct-references = true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""sandfs package: virtual filesystem sandbox with a mini shell."""
|
|
2
|
+
|
|
3
|
+
from .vfs import VirtualFileSystem
|
|
4
|
+
from .shell import SandboxShell, CommandResult
|
|
5
|
+
from .pyexec import PythonExecutor, PythonExecutionResult
|
|
6
|
+
from .providers import ContentProvider, DirectoryProvider, NodeContext, ProvidedNode
|
|
7
|
+
from .policies import NodePolicy, VisibilityView
|
|
8
|
+
from .hooks import WriteEvent, WriteHook
|
|
9
|
+
from .integrations import PathEvent, PathHook
|
|
10
|
+
from .adapters import MemoryStorageAdapter, StorageAdapter
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"VirtualFileSystem",
|
|
14
|
+
"SandboxShell",
|
|
15
|
+
"CommandResult",
|
|
16
|
+
"PythonExecutor",
|
|
17
|
+
"PythonExecutionResult",
|
|
18
|
+
"ContentProvider",
|
|
19
|
+
"DirectoryProvider",
|
|
20
|
+
"NodeContext",
|
|
21
|
+
"ProvidedNode",
|
|
22
|
+
"NodePolicy",
|
|
23
|
+
"VisibilityView",
|
|
24
|
+
"WriteEvent",
|
|
25
|
+
"WriteHook",
|
|
26
|
+
"PathEvent",
|
|
27
|
+
"PathHook",
|
|
28
|
+
"StorageAdapter",
|
|
29
|
+
"MemoryStorageAdapter",
|
|
30
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Storage adapter interfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class StorageEntry:
|
|
11
|
+
content: str
|
|
12
|
+
version: int = 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StorageAdapter:
|
|
16
|
+
def read(self, path: str) -> StorageEntry:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def write(self, path: str, content: str, *, version: int) -> StorageEntry:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
def list(self) -> Dict[str, StorageEntry]:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
def delete(self, path: str) -> None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class MemoryStorageAdapter(StorageAdapter):
|
|
31
|
+
initial: Mapping[str, str] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
self._entries: Dict[str, StorageEntry] = {
|
|
35
|
+
path: StorageEntry(content=text, version=0)
|
|
36
|
+
for path, text in self.initial.items()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def read(self, path: str) -> StorageEntry:
|
|
40
|
+
entry = self._entries.get(path)
|
|
41
|
+
if entry is None:
|
|
42
|
+
raise FileNotFoundError(path)
|
|
43
|
+
return StorageEntry(content=entry.content, version=entry.version)
|
|
44
|
+
|
|
45
|
+
def write(self, path: str, content: str, *, version: int) -> StorageEntry:
|
|
46
|
+
entry = self._entries.get(path)
|
|
47
|
+
if entry and entry.version != version:
|
|
48
|
+
raise ValueError("version mismatch")
|
|
49
|
+
next_version = version + 1
|
|
50
|
+
entry = StorageEntry(content=content, version=next_version)
|
|
51
|
+
self._entries[path] = entry
|
|
52
|
+
return StorageEntry(content=entry.content, version=entry.version)
|
|
53
|
+
|
|
54
|
+
def list(self) -> Dict[str, StorageEntry]:
|
|
55
|
+
return {path: StorageEntry(content=entry.content, version=entry.version) for path, entry in self._entries.items()}
|
|
56
|
+
|
|
57
|
+
def delete(self, path: str) -> None:
|
|
58
|
+
self._entries.pop(path, None)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Custom exceptions for sandfs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SandboxError(RuntimeError):
|
|
7
|
+
"""Base error for the sandbox."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeNotFound(SandboxError):
|
|
11
|
+
"""Raised when a node cannot be resolved."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NodeExists(SandboxError):
|
|
15
|
+
"""Raised when attempting to create a node that already exists."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidOperation(SandboxError):
|
|
19
|
+
"""Raised when an operation cannot be completed."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProviderError(SandboxError):
|
|
23
|
+
"""Raised when a provider callable fails."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"SandboxError",
|
|
28
|
+
"NodeNotFound",
|
|
29
|
+
"NodeExists",
|
|
30
|
+
"InvalidOperation",
|
|
31
|
+
"ProviderError",
|
|
32
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Hook dataclasses and type hints for persistence adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class WriteEvent:
|
|
11
|
+
path: str
|
|
12
|
+
content: str
|
|
13
|
+
version: int
|
|
14
|
+
append: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
WriteHook = Callable[[WriteEvent], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["WriteEvent", "WriteHook"]
|