opencode-landstrip 0.19.7 → 0.19.8

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 (3) hide show
  1. package/README.md +6 -12
  2. package/package.json +2 -2
  3. package/shared.ts +24 -7
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.19.7",
3
+ "version": "0.19.8",
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.8",
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';
@@ -74,7 +75,7 @@ import { createHash } from 'node:crypto';
74
75
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
75
76
  import { homedir, tmpdir } from 'node:os';
76
77
  import { fileURLToPath } from 'node:url';
77
- import { dirname, join } from 'node:path';
78
+ import { dirname, isAbsolute, join } from 'node:path';
78
79
 
79
80
  const packageDir = dirname(fileURLToPath(import.meta.url));
80
81
 
@@ -104,8 +105,11 @@ export function deepMerge(base: SandboxConfig, overrides: SandboxConfigOverrides
104
105
  }
105
106
 
106
107
  export function getConfigPaths(baseDirectory: string): { globalPath: string; projectPath: string } {
108
+ const configHome = process.env.XDG_CONFIG_HOME;
109
+ const configDirectory =
110
+ configHome && isAbsolute(configHome) ? configHome : join(homedir(), '.config');
107
111
  return {
108
- globalPath: join(homedir(), '.config', 'opencode', 'sandbox.json'),
112
+ globalPath: join(configDirectory, 'opencode', 'sandbox.json'),
109
113
  projectPath: join(baseDirectory, '.opencode', 'sandbox.json'),
110
114
  };
111
115
  }
@@ -136,6 +140,7 @@ export function loadConfig(
136
140
  }
137
141
 
138
142
  const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
143
+ templateConfig.filesystem.denyWrite.push(globalPath);
139
144
  const globalOverrides = readConfigFile(globalPath);
140
145
  const baseConfig = deepMerge(templateConfig, globalOverrides);
141
146
 
@@ -143,12 +148,24 @@ export function loadConfig(
143
148
  }
144
149
 
145
150
  export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
146
- const current = readConfigFile(configPath);
151
+ const next: Record<string, unknown> = { ...readConfigFile(configPath) };
152
+ for (const [key, value] of Object.entries(normalizeConfig(update))) {
153
+ if (!isRecord(value)) {
154
+ next[key] = value;
155
+ continue;
156
+ }
147
157
 
148
- const templateConfig: SandboxConfig = JSON.parse(
149
- readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
150
- );
151
- const next = deepMerge(deepMerge(templateConfig, current), update);
158
+ const previous = next[key];
159
+ const section: Record<string, unknown> = isRecord(previous) ? { ...previous } : {};
160
+ for (const [field, fieldValue] of Object.entries(value)) {
161
+ if (fieldValue === undefined) continue;
162
+ const existing = section[field];
163
+ section[field] = Array.isArray(fieldValue)
164
+ ? mergeArray(Array.isArray(existing) ? existing : [], fieldValue)
165
+ : fieldValue;
166
+ }
167
+ next[key] = section;
168
+ }
152
169
 
153
170
  mkdirSync(dirname(configPath), { recursive: true });
154
171
  writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');