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.
- package/README.md +6 -12
- package/index.ts +31 -27
- package/package.json +2 -2
- 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 (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|
|
8
|
+
import { rmSync } from 'node:fs';
|
|
9
9
|
import { type AddressInfo, connect as connectNet, createServer } from 'node:net';
|
|
10
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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;
|