opencode-sandbox 0.1.15 → 0.1.17
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 +73 -26
- package/dist/index.js +6 -6
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/isanchez31/opencode-sandbox-plugin/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/opencode-sandbox)
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
1
5
|
# opencode-sandbox
|
|
2
6
|
|
|
3
7
|
An [OpenCode](https://opencode.ai) plugin that sandboxes agent-executed commands using [`@anthropic-ai/sandbox-runtime`](https://github.com/anthropic-experimental/sandbox-runtime).
|
|
@@ -12,19 +16,18 @@ Every `bash` tool invocation is wrapped with OS-level filesystem and network res
|
|
|
12
16
|
|
|
13
17
|
## Install
|
|
14
18
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
# opencode.json
|
|
19
|
+
```json
|
|
20
|
+
// opencode.json
|
|
18
21
|
{
|
|
19
22
|
"plugin": ["opencode-sandbox"]
|
|
20
23
|
}
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
The plugin is automatically installed from npm when
|
|
26
|
+
The plugin is automatically installed from npm when OpenCode starts.
|
|
24
27
|
|
|
25
|
-
### Linux
|
|
28
|
+
### Linux prerequisites
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
**1. Install bubblewrap:**
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
33
|
# Debian/Ubuntu
|
|
@@ -37,34 +40,76 @@ sudo dnf install bubblewrap
|
|
|
37
40
|
sudo pacman -S bubblewrap
|
|
38
41
|
```
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
**2. Ubuntu 24.04+ (AppArmor fix):**
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
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:
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
|
-
|
|
48
|
+
# Install the AppArmor profiles package
|
|
49
|
+
sudo apt install apparmor-profiles
|
|
50
|
+
|
|
51
|
+
# Create the symlink to enable the profile
|
|
52
|
+
sudo ln -s /etc/apparmor.d/bwrap-userns-restrict /etc/apparmor.d/force-complain/bwrap-userns-restrict
|
|
53
|
+
|
|
54
|
+
# Load the profile
|
|
55
|
+
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
|
|
46
56
|
```
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
You can verify bwrap works:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bwrap --ro-bind / / --dev /dev --proc /proc -- echo "sandbox works"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Without this fix, bwrap will fail with `loopback: Failed RTM_NEWADDR: Operation not permitted` or `setting up uid map: Permission denied`.
|
|
65
|
+
|
|
66
|
+
## What it does
|
|
67
|
+
|
|
68
|
+
When the agent runs a bash command, the sandbox enforces three layers of protection:
|
|
69
|
+
|
|
70
|
+
### Filesystem write protection
|
|
71
|
+
|
|
72
|
+
Commands can only write to the project directory and `/tmp`. Writing anywhere else returns "Read-only file system":
|
|
49
73
|
|
|
50
74
|
```
|
|
51
|
-
|
|
75
|
+
$ touch ~/some-file
|
|
76
|
+
touch: cannot touch '/home/user/some-file': Read-only file system
|
|
77
|
+
|
|
78
|
+
$ echo "data" > /etc/config
|
|
79
|
+
/usr/bin/bash: line 1: /etc/config: Read-only file system
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Sensitive file read protection
|
|
83
|
+
|
|
84
|
+
Access to credential directories is blocked:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
$ cat ~/.ssh/id_rsa
|
|
88
|
+
cat: /home/user/.ssh/id_rsa: Permission denied
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Network allowlist
|
|
92
|
+
|
|
93
|
+
Only approved domains are reachable. All other traffic is blocked via a local proxy:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
$ curl https://evil.com
|
|
52
97
|
Connection blocked by network allowlist
|
|
98
|
+
|
|
99
|
+
$ curl https://registry.npmjs.org
|
|
100
|
+
(works — npmjs.org is in the default allowlist)
|
|
53
101
|
```
|
|
54
102
|
|
|
55
103
|
### Default restrictions
|
|
56
104
|
|
|
57
105
|
**Filesystem (deny-read)**:
|
|
58
|
-
- `~/.ssh`
|
|
59
|
-
- `~/.
|
|
60
|
-
- `~/.
|
|
61
|
-
- `~/.config/gcloud`
|
|
62
|
-
- `~/.npmrc`
|
|
63
|
-
- `~/.env`
|
|
106
|
+
- `~/.ssh`, `~/.gnupg`
|
|
107
|
+
- `~/.aws/credentials`, `~/.config/gcloud`
|
|
108
|
+
- `~/.npmrc`, `~/.env`
|
|
64
109
|
|
|
65
110
|
**Filesystem (allow-write)**:
|
|
66
111
|
- Project directory
|
|
67
|
-
- Git worktree
|
|
112
|
+
- Git worktree (validated — unsafe paths like `/` are rejected)
|
|
68
113
|
- `/tmp`
|
|
69
114
|
|
|
70
115
|
**Network (allow-only)**:
|
|
@@ -82,7 +127,8 @@ Everything else is **blocked by default**.
|
|
|
82
127
|
|
|
83
128
|
### Option 1: Config file
|
|
84
129
|
|
|
85
|
-
```json
|
|
130
|
+
```json
|
|
131
|
+
// .opencode/sandbox.json
|
|
86
132
|
{
|
|
87
133
|
"filesystem": {
|
|
88
134
|
"denyRead": ["~/.ssh", "~/.aws/credentials"],
|
|
@@ -128,12 +174,14 @@ Or in `.opencode/sandbox.json`:
|
|
|
128
174
|
The plugin uses two OpenCode hooks:
|
|
129
175
|
|
|
130
176
|
1. **`tool.execute.before`** — Intercepts bash commands and wraps them with `SandboxManager.wrapWithSandbox()` before execution
|
|
131
|
-
2. **`tool.execute.after`** —
|
|
177
|
+
2. **`tool.execute.after`** — Restores the original command in the UI (hides the bwrap wrapper)
|
|
132
178
|
|
|
133
179
|
```
|
|
134
|
-
Agent → bash tool → [plugin wraps command] → sandboxed execution → [plugin
|
|
180
|
+
Agent → bash tool → [plugin wraps command] → sandboxed execution → [plugin restores UI] → Agent
|
|
135
181
|
```
|
|
136
182
|
|
|
183
|
+
The AI model interprets sandbox errors (like "Read-only file system" or "Connection blocked") directly from command output — no additional annotation layer needed.
|
|
184
|
+
|
|
137
185
|
### Fail-open design
|
|
138
186
|
|
|
139
187
|
If anything goes wrong (sandbox init fails, wrapping fails, platform unsupported), commands run normally without sandbox. The plugin never breaks your workflow.
|
|
@@ -149,9 +197,8 @@ bun install
|
|
|
149
197
|
# Run tests
|
|
150
198
|
bun test
|
|
151
199
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
export { SandboxPlugin } from "/path/to/opencode-sandbox/src/index"
|
|
200
|
+
# Build
|
|
201
|
+
bun run build
|
|
155
202
|
```
|
|
156
203
|
|
|
157
204
|
## Architecture
|
|
@@ -159,10 +206,10 @@ export { SandboxPlugin } from "/path/to/opencode-sandbox/src/index"
|
|
|
159
206
|
```
|
|
160
207
|
src/
|
|
161
208
|
├── index.ts # Plugin entry — exports SandboxPlugin, hooks into tool.execute.before/after
|
|
162
|
-
└── config.ts # Config loading (env var, .opencode/sandbox.json) + defaults +
|
|
209
|
+
└── config.ts # Config loading (env var, .opencode/sandbox.json) + defaults + path validation
|
|
163
210
|
|
|
164
211
|
test/
|
|
165
|
-
├── config.test.ts # Unit tests for config resolution
|
|
212
|
+
├── config.test.ts # Unit tests for config resolution and path safety
|
|
166
213
|
└── plugin.test.ts # Integration tests for plugin hooks
|
|
167
214
|
```
|
|
168
215
|
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
3
3
|
|
|
4
4
|
// src/config.ts
|
|
5
|
-
import
|
|
6
|
-
import os from "os";
|
|
7
|
-
import
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
8
|
var DEFAULT_DENY_READ_DIRS = [
|
|
9
9
|
".ssh",
|
|
10
10
|
".gnupg",
|
|
@@ -69,7 +69,7 @@ function resolveConfig(projectDir, worktree, user) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
async function loadConfig(projectDir) {
|
|
72
|
-
const envConfig = process.env
|
|
72
|
+
const envConfig = process.env.OPENCODE_SANDBOX_CONFIG;
|
|
73
73
|
if (envConfig) {
|
|
74
74
|
try {
|
|
75
75
|
return JSON.parse(envConfig);
|
|
@@ -92,7 +92,7 @@ var SandboxPlugin = async ({ directory, worktree }) => {
|
|
|
92
92
|
console.warn("[opencode-sandbox] Not supported on Windows — sandbox disabled");
|
|
93
93
|
return {};
|
|
94
94
|
}
|
|
95
|
-
if (process.env
|
|
95
|
+
if (process.env.OPENCODE_DISABLE_SANDBOX === "1" || process.env.OPENCODE_DISABLE_SANDBOX === "true") {
|
|
96
96
|
return {};
|
|
97
97
|
}
|
|
98
98
|
const userConfig = await loadConfig(directory);
|
|
@@ -125,7 +125,7 @@ var SandboxPlugin = async ({ directory, worktree }) => {
|
|
|
125
125
|
console.warn("[opencode-sandbox] Failed to wrap command, running unsandboxed:", err instanceof Error ? err.message : err);
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
|
-
"tool.execute.after": async (input,
|
|
128
|
+
"tool.execute.after": async (input, _output) => {
|
|
129
129
|
if (input.tool !== "bash")
|
|
130
130
|
return;
|
|
131
131
|
if (lastOriginalCommand && input.args && typeof input.args.command === "string") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "OpenCode plugin that sandboxes agent commands using @anthropic-ai/sandbox-runtime (seatbelt on macOS, bubblewrap on Linux)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
"build": "bun build ./src/index.ts --outdir dist --target node --format esm --external @anthropic-ai/sandbox-runtime --external @opencode-ai/plugin && bun x tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
21
21
|
"dev": "bun run --watch src/index.ts",
|
|
22
22
|
"test": "bun test",
|
|
23
|
+
"test:coverage": "bun test --coverage",
|
|
24
|
+
"lint": "biome lint .",
|
|
25
|
+
"format": "biome format .",
|
|
26
|
+
"format:fix": "biome format --write .",
|
|
27
|
+
"check": "biome check .",
|
|
28
|
+
"check:fix": "biome check --write .",
|
|
23
29
|
"prepublishOnly": "bun run build"
|
|
24
30
|
},
|
|
25
31
|
"keywords": [
|
|
@@ -54,6 +60,7 @@
|
|
|
54
60
|
"@opencode-ai/plugin": ">=1.0.0"
|
|
55
61
|
},
|
|
56
62
|
"devDependencies": {
|
|
63
|
+
"@biomejs/biome": "^2.0.0",
|
|
57
64
|
"@opencode-ai/plugin": "^1.2.1",
|
|
58
65
|
"@opencode-ai/sdk": "^1.2.1",
|
|
59
66
|
"@types/bun": "^1.3.9",
|