opencode-landstrip 0.16.12 → 0.16.14

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 CHANGED
@@ -1,9 +1,31 @@
1
1
  # opencode-landstrip
2
2
 
3
- Landlock-based sandboxing for [opencode](https://opencode.ai/) using
4
- [`landstrip`](https://github.com/landstrip/landstrip).
3
+ `opencode-landstrip` is a plugin for [OpenCode](https://opencode.ai/) providing
4
+ a sandbox defined with a policy compatible with Anthropic's JSON format. It uses
5
+ [`landstrip`](https://github.com/landstrip/landstrip) to implement the sandbox.
5
6
 
6
- ## Install
7
+ `opencode-landstrip` has a default policy [sandbox.json](./sandbox.json), and
8
+ allows the define either or both global or project specific policies.
9
+
10
+ ## Installing the plugin
11
+
12
+ ### Automatic install
13
+
14
+ Project specific install:
15
+
16
+ ```
17
+ opencode plugin install opencode-landstrip
18
+ ```
19
+
20
+ Global install:
21
+
22
+ ```
23
+ opencode plugin install opencode-landstrip --global
24
+ ```
25
+
26
+ ### Manual install
27
+
28
+ These changes are applied to OpenCode's configuration directories
7
29
 
8
30
  Add the plugin to `opencode.json`:
9
31
 
@@ -14,8 +36,7 @@ Add the plugin to `opencode.json`:
14
36
  }
15
37
  ```
16
38
 
17
- Add the TUI entrypoint to `tui.json` if you install or configure the plugin
18
- manually:
39
+ Add TUI entry point to `tui.json`:
19
40
 
20
41
  ```json
21
42
  {
@@ -24,59 +45,31 @@ manually:
24
45
  }
25
46
  ```
26
47
 
27
- `opencode plugin install opencode-landstrip` configures both entrypoints.
28
-
29
- This installs `opencode-landstrip` and its `@landstrip/landstrip` dependency, which
30
- includes platform-specific native binaries for Linux, macOS, and Windows.
31
-
32
- Requires OpenCode `1.17.7` or newer.
33
-
34
- On unsupported platforms the plugin loads but leaves sandboxing disabled.
35
-
36
- ## Configure
37
-
38
- Create `.opencode/sandbox.json` in a project or
39
- `~/.config/opencode/sandbox.json` globally. Project config takes precedence and
40
- array fields are merged with global/default values.
48
+ The plugin can be later on disabled as follows:
41
49
 
42
- See [`sandbox.json`](./sandbox.json) for a starter config.
50
+ ```json
51
+ {
52
+ "$schema": "https://opencode.ai/config.json",
53
+ "plugin": [["opencode-landstrip", { "enabled": false }]]
54
+ }
55
+ ```
43
56
 
44
57
  ## Behavior
45
58
 
46
- The plugin wraps opencode's AI `bash` tool in `landstrip`, routes proxy-aware
47
- network traffic through an allowlist proxy, and blocks read/write tool access
48
- outside configured filesystem allowlists. The default policy is strict: network
49
- access is off unless domains are allowed, reads are limited to the project,
50
- `~/.gitconfig`, and `/dev/null`, and writes are limited to the project and
51
- `/dev/null`.
52
-
53
- Run `/sandbox` in the TUI to inspect the active sandbox configuration. A compact
54
- status badge in the prompt area shows whether the sandbox is active and whether
55
- network is proxied or open.
56
-
57
- When OpenCode asks for a sandboxed permission, the TUI plugin plays the host's
58
- permission sound and desktop notification, then opens a single dialog with
59
- choices to allow once, allow for the session, persist for the project, persist
60
- globally, or reject. The dialog shows the exact path or domain being approved.
59
+ When OpenCode asks for a sandboxed permission, the plugin emits a host
60
+ notification. After that the plugin opens a dialog with the choices to allow
61
+ once, allow for the session, persist for the project, persist globally, or
62
+ reject. The dialog shows the exact path or domain being approved.
63
+
61
64
  Project approvals are written to `.opencode/sandbox.json`; global approvals are
62
- written to `~/.config/opencode/sandbox.json`.
65
+ written to `~/.config/opencode/sandbox.json`. When the global configuration is
66
+ initially written it acquires the copy of the default sandbox configuration.
63
67
 
64
68
  OpenCode's current plugin API allows wrapping AI `bash` tool calls, but does not
65
69
  allow a plugin to replace manually typed shell-mode commands with a landstrip
66
70
  wrapper. Those commands can still receive the proxy environment from OpenCode,
67
71
  but they are not process-sandboxed by this plugin.
68
72
 
69
- ## Disable
70
-
71
- Set `enabled` to `false` in `sandbox.json`, or pass plugin options:
72
-
73
- ```json
74
- {
75
- "$schema": "https://opencode.ai/config.json",
76
- "plugin": [["opencode-landstrip", { "enabled": false }]]
77
- }
78
- ```
79
-
80
73
  ## License
81
74
 
82
75
  `opencode-landstrip` is licensed under `MIT`. See [LICENSE](LICENSE) for more
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.16.12",
3
+ "version": "0.16.14",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",
package/sandbox.json CHANGED
@@ -12,6 +12,13 @@
12
12
  "denyRead": ["/Users", "/home"],
13
13
  "allowRead": [".", "~/.gitconfig", "~/.config/git/config", "/dev/null"],
14
14
  "allowWrite": [".", "/dev/null"],
15
- "denyWrite": ["**/.env", "**/.env.*", "**/*.pem", "**/*.key"]
15
+ "denyWrite": [
16
+ "**/.env",
17
+ "**/.env.*",
18
+ "**/*.pem",
19
+ "**/*.key",
20
+ ".opencode/sandbox.json",
21
+ "~/.config/opencode/sandbox.json"
22
+ ]
16
23
  }
17
24
  }
package/shared.ts CHANGED
@@ -6,6 +6,7 @@ import { binaryPath } from '@landstrip/landstrip';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
8
8
  import { homedir, tmpdir } from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
9
10
  import { dirname, join } from 'node:path';
10
11
 
11
12
  export interface SandboxFilesystemConfig {
@@ -36,23 +37,7 @@ export interface SandboxConfigOverrides {
36
37
  filesystem?: Partial<SandboxFilesystemConfig>;
37
38
  }
38
39
 
39
- export const DEFAULT_CONFIG: SandboxConfig = {
40
- enabled: true,
41
- network: {
42
- allowNetwork: false,
43
- allowLocalBinding: false,
44
- allowAllUnixSockets: false,
45
- allowUnixSockets: [],
46
- allowedDomains: [],
47
- deniedDomains: [],
48
- },
49
- filesystem: {
50
- denyRead: ['/Users', '/home'],
51
- allowRead: ['.', '~/.gitconfig', '~/.config/git/config', '/dev/null'],
52
- allowWrite: ['.', '/dev/null'],
53
- denyWrite: ['**/.env', '**/.env.*', '**/*.pem', '**/*.key'],
54
- },
55
- };
40
+ const packageDir = dirname(fileURLToPath(import.meta.url));
56
41
 
57
42
  const LANDSTRIP_PACKAGE_NAMES = new Set([
58
43
  '@landstrip/landstrip',
@@ -188,13 +173,18 @@ export function loadConfig(
188
173
  optionOverrides: SandboxConfigOverrides,
189
174
  ): SandboxConfig {
190
175
  const { globalPath, projectPath } = getConfigPaths(baseDirectory);
191
- return deepMerge(
192
- deepMerge(
193
- deepMerge(DEFAULT_CONFIG, readConfigFile(globalPath) ?? {}),
194
- readConfigFile(projectPath) ?? {},
195
- ),
196
- optionOverrides,
197
- );
176
+ const templatePath = join(packageDir, 'sandbox.json');
177
+
178
+ if (!existsSync(globalPath)) {
179
+ mkdirSync(dirname(globalPath), { recursive: true });
180
+ writeFileSync(globalPath, readFileSync(templatePath, 'utf-8'), 'utf-8');
181
+ }
182
+
183
+ const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
184
+ const globalOverrides = readConfigFile(globalPath) ?? {};
185
+ const baseConfig = deepMerge(templateConfig, globalOverrides);
186
+
187
+ return deepMerge(deepMerge(baseConfig, readConfigFile(projectPath) ?? {}), optionOverrides);
198
188
  }
199
189
 
200
190
  export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
@@ -203,7 +193,10 @@ export function writeConfigFile(configPath: string, update: SandboxConfigOverrid
203
193
  throw new Error(`Config file ${configPath} is corrupted; refusing to overwrite`);
204
194
  }
205
195
 
206
- const next = deepMerge(deepMerge(DEFAULT_CONFIG, current), update);
196
+ const templateConfig: SandboxConfig = JSON.parse(
197
+ readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
198
+ );
199
+ const next = deepMerge(deepMerge(templateConfig, current), update);
207
200
 
208
201
  mkdirSync(dirname(configPath), { recursive: true });
209
202
  writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');