opencode-landstrip 0.19.7 → 0.19.9

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.
Files changed (4) hide show
  1. package/README.md +6 -12
  2. package/index.ts +31 -27
  3. package/package.json +2 -2
  4. package/shared.ts +27 -42
package/README.md CHANGED
@@ -19,23 +19,17 @@ Approval never bypasses hard denials. Interactive requests without a live
19
19
  presenter remain denied. Only AI `bash` receives OS isolation: OpenCode's plugin
20
20
  API cannot replace direct user shell commands.
21
21
 
22
- Policy precedence (persistent approvals update the project/global files):
23
-
24
- 1. Bundled [`sandbox.json`](sandbox.json).
25
- 2. `~/.config/opencode/sandbox.json`.
26
- 3. `.opencode/sandbox.json`.
27
- 4. Plugin options.
22
+ Policy precedence: bundled [`sandbox.json`](sandbox.json) → global →
23
+ `.opencode/sandbox.json` → plugin options. The global file is
24
+ `$XDG_CONFIG_HOME/opencode/sandbox.json` (`~/.config` when unset, empty, or relative).
25
+ Persistent approvals update the chosen project/global file.
28
26
 
29
27
  Arrays combine; later scalar values win. Defaults allow project writes, deny
30
28
  sensitive-file writes, and block network access. Enabled sandboxing fails closed
31
29
  on unusable binaries/platforms; `enabled: false` explicitly permits unsandboxed Bash.
32
30
 
33
- - `filesystem.denyReadAlways` cannot be overridden by equal/nested read grants
34
- and blocks promptable reads on those paths. Non-empty `denyReadAlways` is
35
- unsupported on Windows.
36
- - `filesystem.denyWriteAlways` cannot be overridden by equal/nested write grants
37
- and blocks promptable writes on those paths. Non-empty `denyWriteAlways` is
38
- unsupported on Windows.
31
+ `filesystem.denyReadAlways` and `filesystem.denyWriteAlways` block approvals and
32
+ equal/nested grants. Non-empty hard-denial lists are unsupported on Windows.
39
33
 
40
34
  See [landstrip(1)](../landstrip/man/man1/landstrip.1) for native CLI/policy semantics.
41
35
  Runtime seccomp traps/query approval are Linux-only. Native macOS `--trap` reports
package/index.ts CHANGED
@@ -5,10 +5,9 @@ import type { Plugin } from '@opencode/plugin';
5
5
 
6
6
  import { randomBytes } from 'node:crypto';
7
7
  import { spawnSync } from 'node:child_process';
8
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { rmSync } from 'node:fs';
9
9
  import { type AddressInfo, connect as connectNet, createServer } from 'node:net';
10
- import { tmpdir } from 'node:os';
11
- import { basename, join } from 'node:path';
10
+ import { basename } from 'node:path';
12
11
 
13
12
  import {
14
13
  createProxyCredentials,
@@ -38,7 +37,7 @@ import {
38
37
  normalizeOptions,
39
38
  parseLandstripTraps,
40
39
  readDiscoveryPort,
41
- serializeLandstripPolicy,
40
+ writeLandstripPolicyFile,
42
41
  trapSessionHelloLine,
43
42
  } from './shared.js';
44
43
 
@@ -210,21 +209,15 @@ function writePolicyFile(
210
209
  baseDirectory: string,
211
210
  proxyPort: number | null,
212
211
  ): { dir: string; path: string } {
213
- const dir = mkdtempSync(join(tmpdir(), 'opencode-landstrip-'));
214
- const path = join(dir, 'policy.json');
215
- writeFileSync(
216
- path,
217
- serializeLandstripPolicy(
218
- buildLandstripPolicy({
219
- network: config.network,
220
- filesystem: config.filesystem,
221
- baseDirectory,
222
- httpProxyPort: proxyPort,
223
- }),
224
- ),
212
+ return writeLandstripPolicyFile(
213
+ buildLandstripPolicy({
214
+ network: config.network,
215
+ filesystem: config.filesystem,
216
+ baseDirectory,
217
+ httpProxyPort: proxyPort,
218
+ }),
219
+ 'opencode-landstrip-',
225
220
  );
226
-
227
- return { dir, path };
228
221
  }
229
222
 
230
223
  function proxyEnv(
@@ -491,7 +484,7 @@ function evaluateToolPermissions(
491
484
  );
492
485
  }
493
486
 
494
- if (tool === 'apply_patch' && typeof args.patchText === 'string') {
487
+ if ((tool === 'patch' || tool === 'apply_patch') && typeof args.patchText === 'string') {
495
488
  return extractPatchPaths(args.patchText).map((path) =>
496
489
  evaluateWritePermission(path, config, baseDirectory, effectiveAllowWrite),
497
490
  );
@@ -873,14 +866,25 @@ const plugin: Plugin.Plugin = {
873
866
  const state = activeBash.get(event.id);
874
867
  if (!state) return;
875
868
 
876
- const outputText =
877
- event.status === 'completed'
878
- ? typeof event.result.output === 'string'
879
- ? event.result.output
880
- : typeof event.result.content === 'string'
881
- ? event.result.content
882
- : ''
883
- : '';
869
+ let outputText = '';
870
+ if (event.status === 'completed') {
871
+ const { output, content } = event.result;
872
+ if (typeof output === 'string') {
873
+ outputText = output;
874
+ } else if (isRecord(output) && typeof output.output === 'string') {
875
+ outputText = output.output;
876
+ } else if (typeof content === 'string') {
877
+ outputText = content;
878
+ } else if (Array.isArray(content)) {
879
+ outputText = content
880
+ .flatMap((part) =>
881
+ isRecord(part) && part.type === 'text' && typeof part.text === 'string'
882
+ ? [part.text]
883
+ : [],
884
+ )
885
+ .join('\n');
886
+ }
887
+ }
884
888
  const serverTrapOutput = state.trapLines.join('\n');
885
889
  const combinedOutput = serverTrapOutput ? outputText + '\n' + serverTrapOutput : outputText;
886
890
  const traps = parseLandstripTraps(combinedOutput);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.19.7",
3
+ "version": "0.19.9",
4
4
  "description": "OS-level AI command sandboxing for OpenCode with Landstrip",
5
5
  "keywords": [
6
6
  "landlock",
@@ -48,7 +48,7 @@
48
48
  "ci:test": "npm test"
49
49
  },
50
50
  "dependencies": {
51
- "@landstrip/landstrip-api": "^0.19.7",
51
+ "@landstrip/landstrip-api": "^0.19.9",
52
52
  "@opentui/core": ">=0.5.12",
53
53
  "@opentui/solid": ">=0.5.12",
54
54
  "solid-js": "1.9.12"
package/shared.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  deepMergeSandboxConfig,
7
7
  extractDomainsFromCommand,
8
8
  isRecord,
9
+ mergeArray,
9
10
  parseSandboxConfig,
10
11
  } from '@landstrip/landstrip-api/shared';
11
12
  import type { SandboxConfig, SandboxConfigOverrides } from '@landstrip/landstrip-api/shared';
@@ -40,6 +41,7 @@ export {
40
41
  sessionAllows,
41
42
  sessionScopeFor,
42
43
  serializeLandstripPolicy,
44
+ writeLandstripPolicyFile,
43
45
  deepMergeSandboxConfig,
44
46
  mergeArray,
45
47
  parseSandboxConfig,
@@ -74,20 +76,10 @@ import { createHash } from 'node:crypto';
74
76
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
75
77
  import { homedir, tmpdir } from 'node:os';
76
78
  import { fileURLToPath } from 'node:url';
77
- import { dirname, join } from 'node:path';
79
+ import { dirname, isAbsolute, join } from 'node:path';
78
80
 
79
81
  const packageDir = dirname(fileURLToPath(import.meta.url));
80
82
 
81
- const LANDSTRIP_PACKAGE_NAMES = new Set([
82
- '@landstrip/landstrip-api',
83
- '@landstrip/landstrip-darwin-arm64',
84
- '@landstrip/landstrip-darwin-x64',
85
- '@landstrip/landstrip-linux-x64',
86
- '@landstrip/landstrip-linux-arm64',
87
- '@landstrip/landstrip-win32-x64',
88
- '@landstrip/landstrip-win32-arm64',
89
- ]);
90
-
91
83
  export function normalizeConfig(value: unknown): SandboxConfigOverrides {
92
84
  return parseSandboxConfig(value);
93
85
  }
@@ -104,8 +96,11 @@ export function deepMerge(base: SandboxConfig, overrides: SandboxConfigOverrides
104
96
  }
105
97
 
106
98
  export function getConfigPaths(baseDirectory: string): { globalPath: string; projectPath: string } {
99
+ const configHome = process.env.XDG_CONFIG_HOME;
100
+ const configDirectory =
101
+ configHome && isAbsolute(configHome) ? configHome : join(homedir(), '.config');
107
102
  return {
108
- globalPath: join(homedir(), '.config', 'opencode', 'sandbox.json'),
103
+ globalPath: join(configDirectory, 'opencode', 'sandbox.json'),
109
104
  projectPath: join(baseDirectory, '.opencode', 'sandbox.json'),
110
105
  };
111
106
  }
@@ -136,6 +131,7 @@ export function loadConfig(
136
131
  }
137
132
 
138
133
  const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
134
+ templateConfig.filesystem.denyWrite.push(globalPath);
139
135
  const globalOverrides = readConfigFile(globalPath);
140
136
  const baseConfig = deepMerge(templateConfig, globalOverrides);
141
137
 
@@ -143,12 +139,24 @@ export function loadConfig(
143
139
  }
144
140
 
145
141
  export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
146
- const current = readConfigFile(configPath);
142
+ const next: Record<string, unknown> = { ...readConfigFile(configPath) };
143
+ for (const [key, value] of Object.entries(normalizeConfig(update))) {
144
+ if (!isRecord(value)) {
145
+ next[key] = value;
146
+ continue;
147
+ }
147
148
 
148
- const templateConfig: SandboxConfig = JSON.parse(
149
- readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
150
- );
151
- const next = deepMerge(deepMerge(templateConfig, current), update);
149
+ const previous = next[key];
150
+ const section: Record<string, unknown> = isRecord(previous) ? { ...previous } : {};
151
+ for (const [field, fieldValue] of Object.entries(value)) {
152
+ if (fieldValue === undefined) continue;
153
+ const existing = section[field];
154
+ section[field] = Array.isArray(fieldValue)
155
+ ? mergeArray(Array.isArray(existing) ? existing : [], fieldValue)
156
+ : fieldValue;
157
+ }
158
+ next[key] = section;
159
+ }
152
160
 
153
161
  mkdirSync(dirname(configPath), { recursive: true });
154
162
  writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');
@@ -162,31 +170,8 @@ export function landstripBinaryPath(): string {
162
170
  if (_landstripBinaryPathError !== undefined) throw _landstripBinaryPathError;
163
171
 
164
172
  try {
165
- const filePath = realpathSync.native(binaryPath());
166
- let probe = dirname(filePath);
167
-
168
- while (true) {
169
- const manifestPath = join(probe, 'package.json');
170
- if (existsSync(manifestPath)) {
171
- try {
172
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as unknown;
173
- if (isRecord(manifest) && LANDSTRIP_PACKAGE_NAMES.has(String(manifest.name))) {
174
- _landstripBinaryPath = filePath;
175
- return filePath;
176
- }
177
- } catch {
178
- // malformed package.json — continue walking to parent
179
- }
180
- }
181
-
182
- const parent = dirname(probe);
183
- if (parent === probe) break;
184
- probe = parent;
185
- }
186
-
187
- throw new Error(
188
- `Refusing to use landstrip binary outside official @landstrip/landstrip-api packages: ${filePath}`,
189
- );
173
+ _landstripBinaryPath = binaryPath();
174
+ return _landstripBinaryPath;
190
175
  } catch (error) {
191
176
  _landstripBinaryPathError = error;
192
177
  throw error;