opencode-landstrip 0.16.7 → 0.16.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 (2) hide show
  1. package/index.ts +25 -18
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -267,9 +267,10 @@ function extractBlockedPath(
267
267
  }
268
268
  }
269
269
 
270
- // If landstrip reported a trap but without a path, try to
271
- // extract the blocked path from the command itself
272
- if (landstripTraps.length > 0 && command) {
270
+ if (
271
+ landstripTraps.some((trap) => trap.kind === 'filesystem' || trap.kind === 'internal') &&
272
+ command
273
+ ) {
273
274
  for (const candidate of extractCandidatePaths(command)) {
274
275
  const resolved = canonicalizePath(candidate, baseDirectory);
275
276
  return resolved;
@@ -279,14 +280,6 @@ function extractBlockedPath(
279
280
  return null;
280
281
  }
281
282
 
282
- function extractBlockedWritePath(
283
- output: string,
284
- baseDirectory: string,
285
- command?: string,
286
- ): string | null {
287
- return extractBlockedPath(output, baseDirectory, command);
288
- }
289
-
290
283
  function evaluateReadPermission(
291
284
  path: string,
292
285
  config: SandboxConfig,
@@ -652,6 +645,15 @@ function socketQueryPort(baseDirectory: string): number | null {
652
645
  return readDiscoveryPort(baseDirectory);
653
646
  }
654
647
 
648
+ async function awaitQueryPort(baseDirectory: string): Promise<number | null> {
649
+ for (let attempt = 0; attempt < 5; attempt++) {
650
+ const port = socketQueryPort(baseDirectory);
651
+ if (port !== null) return port;
652
+ await new Promise((resolve) => setTimeout(resolve, 50));
653
+ }
654
+ return null;
655
+ }
656
+
655
657
  function buildWrappedCommand(
656
658
  policyPath: string,
657
659
  shell: string,
@@ -663,12 +665,17 @@ function buildWrappedCommand(
663
665
  if (trapPort === null) return plain;
664
666
 
665
667
  // Connect fd 3 to the TUI's query-response socket BEFORE landstrip applies the
666
- // sandbox, so a denied write can be approved live instead of forcing a re-run.
667
- // The `&&`/`||` guard falls back to the plain (no --trap-fd) invocation when the
668
- // redirect fails — a dead port, a non-bash outer shell without /dev/tcp, or an
669
- // outer `set -e`so a failed connect never hands the broker a dead fd 3.
668
+ // sandbox, so a denied filesystem access can be approved live instead of
669
+ // forcing a re-run.
670
+ //
671
+ // /dev/tcp is a bash/ksh built-init does not work in zsh, dash, or fish.
672
+ // We try the native redirect first (fast path when the host shell supports it)
673
+ // and fall back to an explicit bash invocation that always speaks /dev/tcp.
674
+ // If both fail (no bash, dead port, set -e in the outer shell) landstrip runs
675
+ // without --trap-fd so the toast-notify path still works.
670
676
  const trapped = [landstripBinaryPath(), '--trap-fd', '3', ...baseArgs].map(shellQuote).join(' ');
671
- return `{ exec 3<>/dev/tcp/127.0.0.1/${trapPort} ; } 2>/dev/null && ${trapped} || ${plain}`;
677
+ const bashTrap = `bash -c ${shellQuote(`exec 3<>/dev/tcp/127.0.0.1/${trapPort} && exec "$@"`)} bash ${trapped}`;
678
+ return `{ exec 3<>/dev/tcp/127.0.0.1/${trapPort} ; } 2>/dev/null && ${trapped} || ${bashTrap} 2>/dev/null || ${plain}`;
672
679
  }
673
680
 
674
681
  function isGeneratedWrappedCommand(command: string): boolean {
@@ -1062,7 +1069,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
1062
1069
  policy.path,
1063
1070
  configuredShell ?? process.env.SHELL ?? '/bin/sh',
1064
1071
  originalCommand,
1065
- socketQueryPort(directory),
1072
+ await awaitQueryPort(directory),
1066
1073
  );
1067
1074
 
1068
1075
  activeBash.set(callID, {
@@ -1239,7 +1246,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
1239
1246
  ?.catch?.(() => undefined);
1240
1247
  }
1241
1248
 
1242
- const blockedPath = extractBlockedWritePath(outputText, directory, state.originalCommand);
1249
+ const blockedPath = extractBlockedPath(outputText, directory, state.originalCommand);
1243
1250
  if (blockedPath) {
1244
1251
  await notifyOnce(
1245
1252
  `blocked:${blockedPath}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.16.7",
3
+ "version": "0.16.9",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",