opencode-sandbox 0.1.14 → 0.1.16
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.
- package/README.md +69 -26
- package/dist/index.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,19 +12,18 @@ Every `bash` tool invocation is wrapped with OS-level filesystem and network res
|
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
# opencode.json
|
|
15
|
+
```json
|
|
16
|
+
// opencode.json
|
|
18
17
|
{
|
|
19
18
|
"plugin": ["opencode-sandbox"]
|
|
20
19
|
}
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
The plugin is automatically installed from npm when
|
|
22
|
+
The plugin is automatically installed from npm when OpenCode starts.
|
|
24
23
|
|
|
25
|
-
### Linux
|
|
24
|
+
### Linux prerequisites
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
**1. Install bubblewrap:**
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
29
|
# Debian/Ubuntu
|
|
@@ -37,34 +36,76 @@ sudo dnf install bubblewrap
|
|
|
37
36
|
sudo pacman -S bubblewrap
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
**2. Ubuntu 24.04+ (AppArmor fix):**
|
|
40
|
+
|
|
41
|
+
Ubuntu 24.04 and later restrict unprivileged user namespaces via AppArmor, which prevents bubblewrap from working. You need to enable the `bwrap-userns-restrict` AppArmor profile:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install the AppArmor profiles package
|
|
45
|
+
sudo apt install apparmor-profiles
|
|
46
|
+
|
|
47
|
+
# Create the symlink to enable the profile
|
|
48
|
+
sudo ln -s /etc/apparmor.d/bwrap-userns-restrict /etc/apparmor.d/force-complain/bwrap-userns-restrict
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
# Load the profile
|
|
51
|
+
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You can verify bwrap works:
|
|
43
55
|
|
|
44
56
|
```bash
|
|
45
|
-
|
|
57
|
+
bwrap --ro-bind / / --dev /dev --proc /proc -- echo "sandbox works"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without this fix, bwrap will fail with `loopback: Failed RTM_NEWADDR: Operation not permitted` or `setting up uid map: Permission denied`.
|
|
61
|
+
|
|
62
|
+
## What it does
|
|
63
|
+
|
|
64
|
+
When the agent runs a bash command, the sandbox enforces three layers of protection:
|
|
65
|
+
|
|
66
|
+
### Filesystem write protection
|
|
67
|
+
|
|
68
|
+
Commands can only write to the project directory and `/tmp`. Writing anywhere else returns "Read-only file system":
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
$ touch ~/some-file
|
|
72
|
+
touch: cannot touch '/home/user/some-file': Read-only file system
|
|
73
|
+
|
|
74
|
+
$ echo "data" > /etc/config
|
|
75
|
+
/usr/bin/bash: line 1: /etc/config: Read-only file system
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Sensitive file read protection
|
|
79
|
+
|
|
80
|
+
Access to credential directories is blocked:
|
|
81
|
+
|
|
46
82
|
```
|
|
83
|
+
$ cat ~/.ssh/id_rsa
|
|
84
|
+
cat: /home/user/.ssh/id_rsa: Permission denied
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Network allowlist
|
|
47
88
|
|
|
48
|
-
|
|
89
|
+
Only approved domains are reachable. All other traffic is blocked via a local proxy:
|
|
49
90
|
|
|
50
91
|
```
|
|
51
|
-
|
|
92
|
+
$ curl https://evil.com
|
|
52
93
|
Connection blocked by network allowlist
|
|
94
|
+
|
|
95
|
+
$ curl https://registry.npmjs.org
|
|
96
|
+
(works — npmjs.org is in the default allowlist)
|
|
53
97
|
```
|
|
54
98
|
|
|
55
99
|
### Default restrictions
|
|
56
100
|
|
|
57
101
|
**Filesystem (deny-read)**:
|
|
58
|
-
- `~/.ssh`
|
|
59
|
-
- `~/.
|
|
60
|
-
- `~/.
|
|
61
|
-
- `~/.config/gcloud`
|
|
62
|
-
- `~/.npmrc`
|
|
63
|
-
- `~/.env`
|
|
102
|
+
- `~/.ssh`, `~/.gnupg`
|
|
103
|
+
- `~/.aws/credentials`, `~/.config/gcloud`
|
|
104
|
+
- `~/.npmrc`, `~/.env`
|
|
64
105
|
|
|
65
106
|
**Filesystem (allow-write)**:
|
|
66
107
|
- Project directory
|
|
67
|
-
- Git worktree
|
|
108
|
+
- Git worktree (validated — unsafe paths like `/` are rejected)
|
|
68
109
|
- `/tmp`
|
|
69
110
|
|
|
70
111
|
**Network (allow-only)**:
|
|
@@ -82,7 +123,8 @@ Everything else is **blocked by default**.
|
|
|
82
123
|
|
|
83
124
|
### Option 1: Config file
|
|
84
125
|
|
|
85
|
-
```json
|
|
126
|
+
```json
|
|
127
|
+
// .opencode/sandbox.json
|
|
86
128
|
{
|
|
87
129
|
"filesystem": {
|
|
88
130
|
"denyRead": ["~/.ssh", "~/.aws/credentials"],
|
|
@@ -128,12 +170,14 @@ Or in `.opencode/sandbox.json`:
|
|
|
128
170
|
The plugin uses two OpenCode hooks:
|
|
129
171
|
|
|
130
172
|
1. **`tool.execute.before`** — Intercepts bash commands and wraps them with `SandboxManager.wrapWithSandbox()` before execution
|
|
131
|
-
2. **`tool.execute.after`** —
|
|
173
|
+
2. **`tool.execute.after`** — Restores the original command in the UI (hides the bwrap wrapper)
|
|
132
174
|
|
|
133
175
|
```
|
|
134
|
-
Agent → bash tool → [plugin wraps command] → sandboxed execution → [plugin
|
|
176
|
+
Agent → bash tool → [plugin wraps command] → sandboxed execution → [plugin restores UI] → Agent
|
|
135
177
|
```
|
|
136
178
|
|
|
179
|
+
The AI model interprets sandbox errors (like "Read-only file system" or "Connection blocked") directly from command output — no additional annotation layer needed.
|
|
180
|
+
|
|
137
181
|
### Fail-open design
|
|
138
182
|
|
|
139
183
|
If anything goes wrong (sandbox init fails, wrapping fails, platform unsupported), commands run normally without sandbox. The plugin never breaks your workflow.
|
|
@@ -149,9 +193,8 @@ bun install
|
|
|
149
193
|
# Run tests
|
|
150
194
|
bun test
|
|
151
195
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
export { SandboxPlugin } from "/path/to/opencode-sandbox/src/index"
|
|
196
|
+
# Build
|
|
197
|
+
bun run build
|
|
155
198
|
```
|
|
156
199
|
|
|
157
200
|
## Architecture
|
|
@@ -159,10 +202,10 @@ export { SandboxPlugin } from "/path/to/opencode-sandbox/src/index"
|
|
|
159
202
|
```
|
|
160
203
|
src/
|
|
161
204
|
├── index.ts # Plugin entry — exports SandboxPlugin, hooks into tool.execute.before/after
|
|
162
|
-
└── config.ts # Config loading (env var, .opencode/sandbox.json) + defaults +
|
|
205
|
+
└── config.ts # Config loading (env var, .opencode/sandbox.json) + defaults + path validation
|
|
163
206
|
|
|
164
207
|
test/
|
|
165
|
-
├── config.test.ts # Unit tests for config resolution
|
|
208
|
+
├── config.test.ts # Unit tests for config resolution and path safety
|
|
166
209
|
└── plugin.test.ts # Integration tests for plugin hooks
|
|
167
210
|
```
|
|
168
211
|
|
package/dist/index.js
CHANGED
|
@@ -32,12 +32,31 @@ var DEFAULT_ALLOWED_DOMAINS = [
|
|
|
32
32
|
"generativelanguage.googleapis.com",
|
|
33
33
|
"*.googleapis.com"
|
|
34
34
|
];
|
|
35
|
+
var UNSAFE_WRITE_PATHS = new Set(["/", "/home", "/usr", "/etc", "/var", "/opt"]);
|
|
36
|
+
function isSafeWritePath(p) {
|
|
37
|
+
const normalized = path.resolve(p);
|
|
38
|
+
if (UNSAFE_WRITE_PATHS.has(normalized)) {
|
|
39
|
+
console.warn(`[opencode-sandbox] Rejecting unsafe write path: ${normalized}`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
35
44
|
function resolveConfig(projectDir, worktree, user) {
|
|
36
45
|
const homeDir = os.homedir();
|
|
46
|
+
const candidatePaths = [projectDir, worktree, os.tmpdir()].filter(Boolean);
|
|
47
|
+
const safePaths = candidatePaths.filter((p) => isSafeWritePath(p));
|
|
48
|
+
const seen = new Set;
|
|
49
|
+
const writePaths = user?.filesystem?.allowWrite ?? safePaths.filter((p) => {
|
|
50
|
+
const resolved = path.resolve(p);
|
|
51
|
+
if (seen.has(resolved))
|
|
52
|
+
return false;
|
|
53
|
+
seen.add(resolved);
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
37
56
|
return {
|
|
38
57
|
filesystem: {
|
|
39
58
|
denyRead: user?.filesystem?.denyRead ?? DEFAULT_DENY_READ_DIRS.map((p) => path.join(homeDir, p)),
|
|
40
|
-
allowWrite:
|
|
59
|
+
allowWrite: writePaths,
|
|
41
60
|
denyWrite: user?.filesystem?.denyWrite ?? []
|
|
42
61
|
},
|
|
43
62
|
network: {
|
package/package.json
CHANGED