pi-landstrip 0.3.1 → 0.3.2

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/index.ts +193 -151
  2. package/landstrip.d.ts +3 -0
  3. package/package.json +2 -1
package/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (C) Jarkko Sakkinen 2026
3
3
 
4
+ /// <reference path="./landstrip.d.ts" />
5
+
4
6
  import type {
5
7
  AgentToolResult,
6
8
  AgentToolUpdateCallback,
@@ -593,16 +595,33 @@ function pipeSockets(client: Socket, upstream: Socket, initialData?: Buffer): vo
593
595
  upstream.pipe(client);
594
596
  }
595
597
 
598
+ type LandstripBashTool = ReturnType<typeof createBashToolDefinition>;
599
+
600
+ export interface LandstripIntegrationOptions {
601
+ registerBashTool?: boolean;
602
+ cwd?: string;
603
+ }
604
+
605
+ export interface LandstripIntegration {
606
+ createBashTool(cwd: string, ctx?: ExtensionContext): LandstripBashTool;
607
+ register(pi: ExtensionAPI): void;
608
+ }
609
+
596
610
  export default function (pi: ExtensionAPI) {
597
- pi.registerFlag('no-sandbox', {
598
- description: 'Disable landstrip sandboxing for bash commands',
599
- type: 'boolean',
600
- default: false,
601
- });
611
+ createLandstripIntegration().register(pi);
612
+ }
613
+
614
+ export function createLandstripIntegration(
615
+ options: LandstripIntegrationOptions = {},
616
+ ): LandstripIntegration {
617
+ const shouldRegisterBashTool = options.registerBashTool ?? true;
618
+ const localCwd = options.cwd ?? process.cwd();
602
619
 
603
- const localCwd = process.cwd();
604
- const userShellPath = SettingsManager.create(localCwd).getShellPath();
605
- const localBash = createBashToolDefinition(localCwd, { shellPath: userShellPath });
620
+ function createPlainBashTool(cwd: string): LandstripBashTool {
621
+ return createBashToolDefinition(cwd, {
622
+ shellPath: SettingsManager.create(cwd).getShellPath(),
623
+ });
624
+ }
606
625
 
607
626
  let sandboxEnabled = false;
608
627
  let sandboxReady = false;
@@ -838,7 +857,7 @@ export default function (pi: ExtensionAPI) {
838
857
  async exec(command, cwd, { onData, signal, timeout, env }) {
839
858
  if (!existsSync(cwd)) throw new Error(`Working directory does not exist: ${cwd}`);
840
859
 
841
- const { shell, args } = getShellConfig(userShellPath);
860
+ const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
842
861
  const proxy = await startProxy(ctx, cwd);
843
862
  const policy = writePolicyFile(cwd, proxy.port);
844
863
  const landstripArgs = ['-p', policy.path, shell, ...args, command];
@@ -919,11 +938,11 @@ export default function (pi: ExtensionAPI) {
919
938
  ctx: ExtensionContext,
920
939
  ): Promise<AgentToolResult<BashToolDetails | undefined>> {
921
940
  let landstripStderr = '';
922
- const sandboxedBash = createBashToolDefinition(localCwd, {
941
+ const sandboxedBash = createBashToolDefinition(ctx.cwd, {
923
942
  operations: createLandstripBashOps(ctx, (data) => {
924
943
  landstripStderr += data.toString('utf8');
925
944
  }),
926
- shellPath: userShellPath,
945
+ shellPath: SettingsManager.create(ctx.cwd).getShellPath(),
927
946
  });
928
947
 
929
948
  const run = () => sandboxedBash.execute(id, params, signal, onUpdate, ctx);
@@ -1041,178 +1060,201 @@ export default function (pi: ExtensionAPI) {
1041
1060
  return true;
1042
1061
  }
1043
1062
 
1044
- pi.registerTool({
1045
- ...localBash,
1046
- label: 'bash (landstrip)',
1047
- async execute(id, params, signal, onUpdate, ctx) {
1048
- if (!sandboxEnabled || !sandboxReady)
1049
- return localBash.execute(id, params, signal, onUpdate, ctx);
1063
+ function createBashTool(cwd: string, ctx?: ExtensionContext): LandstripBashTool {
1064
+ const localBash = createPlainBashTool(cwd);
1050
1065
 
1051
- return runBashWithOptionalRetry(id, params, signal, onUpdate, ctx);
1052
- },
1053
- });
1066
+ return {
1067
+ ...localBash,
1068
+ label: 'bash (landstrip)',
1069
+ async execute(id, params, signal, onUpdate, callCtx) {
1070
+ const effectiveCtx = callCtx ?? ctx;
1071
+ if (!sandboxEnabled || !sandboxReady || !effectiveCtx)
1072
+ return localBash.execute(id, params, signal, onUpdate, effectiveCtx);
1073
+
1074
+ return runBashWithOptionalRetry(id, params, signal, onUpdate, effectiveCtx);
1075
+ },
1076
+ };
1077
+ }
1054
1078
 
1055
- pi.on('user_bash', async (event, ctx) => {
1056
- if (!sandboxEnabled || !sandboxReady) return;
1057
- if (!loadConfig(ctx.cwd).enabled) return;
1058
-
1059
- const blockedDomain = await preflightCommandDomains(event.command, ctx);
1060
- if (blockedDomain) {
1061
- return {
1062
- result: {
1063
- output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
1064
- exitCode: 1,
1065
- cancelled: false,
1066
- truncated: false,
1067
- },
1068
- };
1069
- }
1079
+ function register(pi: ExtensionAPI): void {
1080
+ const maybePi = pi as ExtensionAPI & {
1081
+ getFlag?: (name: string) => unknown;
1082
+ registerCommand?: ExtensionAPI['registerCommand'];
1083
+ registerFlag?: ExtensionAPI['registerFlag'];
1084
+ };
1070
1085
 
1071
- return { operations: createLandstripBashOps(ctx) };
1072
- });
1086
+ maybePi.registerFlag?.('no-sandbox', {
1087
+ description: 'Disable landstrip sandboxing for bash commands',
1088
+ type: 'boolean',
1089
+ default: false,
1090
+ });
1073
1091
 
1074
- pi.on('tool_call', async (event, ctx) => {
1075
- if (!sandboxEnabled) return;
1092
+ if (shouldRegisterBashTool) pi.registerTool(createBashTool(localCwd));
1076
1093
 
1077
- const config = loadConfig(ctx.cwd);
1078
- if (!config.enabled) return;
1094
+ pi.on('user_bash', async (event, ctx) => {
1095
+ if (!sandboxEnabled || !sandboxReady) return;
1096
+ if (!loadConfig(ctx.cwd).enabled) return;
1079
1097
 
1080
- const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
1081
-
1082
- if (sandboxReady && isToolCallEventType('bash', event)) {
1083
- const blockedDomain = await preflightCommandDomains(event.input.command, ctx);
1098
+ const blockedDomain = await preflightCommandDomains(event.command, ctx);
1084
1099
  if (blockedDomain) {
1085
1100
  return {
1086
- block: true,
1087
- reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
1101
+ result: {
1102
+ output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
1103
+ exitCode: 1,
1104
+ cancelled: false,
1105
+ truncated: false,
1106
+ },
1088
1107
  };
1089
1108
  }
1090
- }
1091
1109
 
1092
- if (isToolCallEventType('read', event)) {
1093
- const filePath = canonicalizePath(event.input.path);
1094
- if (!matchesPattern(filePath, getEffectiveAllowRead(ctx.cwd))) {
1095
- const choice = await promptReadBlock(ctx, filePath);
1096
- if (choice === 'abort') {
1110
+ return { operations: createLandstripBashOps(ctx) };
1111
+ });
1112
+
1113
+ pi.on('tool_call', async (event, ctx) => {
1114
+ if (!sandboxEnabled) return;
1115
+
1116
+ const config = loadConfig(ctx.cwd);
1117
+ if (!config.enabled) return;
1118
+
1119
+ const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
1120
+
1121
+ if (sandboxReady && isToolCallEventType('bash', event)) {
1122
+ const blockedDomain = await preflightCommandDomains(event.input.command, ctx);
1123
+ if (blockedDomain) {
1097
1124
  return {
1098
1125
  block: true,
1099
- reason: `Sandbox: read access denied for "${filePath}"`,
1126
+ reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
1100
1127
  };
1101
1128
  }
1102
- await applyReadChoice(choice, filePath, ctx.cwd);
1103
1129
  }
1104
- }
1105
-
1106
- if (isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
1107
- const filePath = canonicalizePath((event.input as { path: string }).path);
1108
1130
 
1109
- if (matchesPattern(filePath, config.filesystem.denyWrite)) {
1110
- return {
1111
- block: true,
1112
- reason:
1113
- `Sandbox: write access denied for "${filePath}" (in denyWrite). ` +
1114
- `To change this, edit denyWrite in:\n ${projectPath}\n ${globalPath}`,
1115
- };
1131
+ if (isToolCallEventType('read', event)) {
1132
+ const filePath = canonicalizePath(event.input.path);
1133
+ if (!matchesPattern(filePath, getEffectiveAllowRead(ctx.cwd))) {
1134
+ const choice = await promptReadBlock(ctx, filePath);
1135
+ if (choice === 'abort') {
1136
+ return {
1137
+ block: true,
1138
+ reason: `Sandbox: read access denied for "${filePath}"`,
1139
+ };
1140
+ }
1141
+ await applyReadChoice(choice, filePath, ctx.cwd);
1142
+ }
1116
1143
  }
1117
1144
 
1118
- if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(ctx.cwd), matchesPattern)) {
1119
- const choice = await promptWriteBlock(ctx, filePath);
1120
- if (choice === 'abort') {
1145
+ if (isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
1146
+ const filePath = canonicalizePath((event.input as { path: string }).path);
1147
+
1148
+ if (matchesPattern(filePath, config.filesystem.denyWrite)) {
1121
1149
  return {
1122
1150
  block: true,
1123
- reason: `Sandbox: write access denied for "${filePath}" (not in allowWrite)`,
1151
+ reason:
1152
+ `Sandbox: write access denied for "${filePath}" (in denyWrite). ` +
1153
+ `To change this, edit denyWrite in:\n ${projectPath}\n ${globalPath}`,
1124
1154
  };
1125
1155
  }
1126
- await applyWriteChoice(choice, filePath, ctx.cwd);
1127
- }
1128
- }
1129
- });
1130
-
1131
- pi.on('session_start', async (_event, ctx) => {
1132
- const noSandbox = pi.getFlag('no-sandbox') as boolean;
1133
1156
 
1134
- if (noSandbox) {
1135
- sandboxEnabled = false;
1136
- sandboxReady = false;
1137
- ctx.ui.notify('Sandbox disabled via --no-sandbox', 'warning');
1138
- return;
1139
- }
1140
-
1141
- const config = loadConfig(ctx.cwd);
1142
- if (!config.enabled) {
1143
- sandboxEnabled = false;
1144
- sandboxReady = false;
1145
- ctx.ui.notify('Sandbox disabled via config', 'info');
1146
- return;
1147
- }
1157
+ if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(ctx.cwd), matchesPattern)) {
1158
+ const choice = await promptWriteBlock(ctx, filePath);
1159
+ if (choice === 'abort') {
1160
+ return {
1161
+ block: true,
1162
+ reason: `Sandbox: write access denied for "${filePath}" (not in allowWrite)`,
1163
+ };
1164
+ }
1165
+ await applyWriteChoice(choice, filePath, ctx.cwd);
1166
+ }
1167
+ }
1168
+ });
1148
1169
 
1149
- enableSandbox(ctx);
1150
- });
1170
+ pi.on('session_start', async (_event, ctx) => {
1171
+ const noSandbox = maybePi.getFlag?.('no-sandbox') as boolean;
1151
1172
 
1152
- pi.registerCommand('sandbox-enable', {
1153
- description: 'Enable the landstrip sandbox for this session',
1154
- handler: async (_args, ctx) => {
1155
- if (sandboxEnabled) {
1156
- ctx.ui.notify('Sandbox is already enabled', 'info');
1173
+ if (noSandbox) {
1174
+ sandboxEnabled = false;
1175
+ sandboxReady = false;
1176
+ ctx.ui.notify('Sandbox disabled via --no-sandbox', 'warning');
1157
1177
  return;
1158
1178
  }
1159
1179
 
1160
- if (enableSandbox(ctx)) ctx.ui.notify('Sandbox enabled', 'info');
1161
- },
1162
- });
1163
-
1164
- pi.registerCommand('sandbox-disable', {
1165
- description: 'Disable the landstrip sandbox for this session',
1166
- handler: async (_args, ctx) => {
1167
- if (!sandboxEnabled) {
1168
- ctx.ui.notify('Sandbox is already disabled', 'info');
1180
+ const config = loadConfig(ctx.cwd);
1181
+ if (!config.enabled) {
1182
+ sandboxEnabled = false;
1183
+ sandboxReady = false;
1184
+ ctx.ui.notify('Sandbox disabled via config', 'info');
1169
1185
  return;
1170
1186
  }
1171
1187
 
1172
- sandboxEnabled = false;
1173
- sandboxReady = false;
1174
- ctx.ui.setStatus('sandbox', '');
1175
- ctx.ui.notify('Sandbox disabled', 'info');
1176
- },
1177
- });
1188
+ enableSandbox(ctx);
1189
+ });
1178
1190
 
1179
- pi.registerCommand('sandbox', {
1180
- description: 'Show sandbox configuration',
1181
- handler: async (_args, ctx) => {
1182
- if (!sandboxEnabled) {
1183
- ctx.ui.notify('Sandbox is disabled', 'info');
1184
- return;
1185
- }
1191
+ maybePi.registerCommand?.('sandbox-enable', {
1192
+ description: 'Enable the landstrip sandbox for this session',
1193
+ handler: async (_args, ctx) => {
1194
+ if (sandboxEnabled) {
1195
+ ctx.ui.notify('Sandbox is already enabled', 'info');
1196
+ return;
1197
+ }
1186
1198
 
1187
- const config = loadConfig(ctx.cwd);
1188
- const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
1189
- const lines = [
1190
- 'Sandbox Configuration',
1191
- ` Project config: ${projectPath}`,
1192
- ` Global config: ${globalPath}`,
1193
- ` landstrip: ${binaryPath()}`,
1194
- '',
1195
- 'Network (bash through HTTP proxy):',
1196
- ` Allowed domains: ${config.network.allowedDomains.join(', ') || '(none)'}`,
1197
- ` Denied domains: ${config.network.deniedDomains.join(', ') || '(none)'}`,
1198
- ...(sessionAllowedDomains.length > 0
1199
- ? [` Session allowed: ${sessionAllowedDomains.join(', ')}`]
1200
- : []),
1201
- '',
1202
- 'Filesystem (bash + read/write/edit tools):',
1203
- ` Deny Read: ${config.filesystem.denyRead.join(', ') || '(none)'}`,
1204
- ` Allow Read: ${config.filesystem.allowRead.join(', ') || '(none)'}`,
1205
- ` Allow Write: ${config.filesystem.allowWrite.join(', ') || '(none)'}`,
1206
- ` Deny Write: ${config.filesystem.denyWrite.join(', ') || '(none)'}`,
1207
- ...(sessionAllowedReadPaths.length > 0
1208
- ? [` Session read: ${sessionAllowedReadPaths.join(', ')}`]
1209
- : []),
1210
- ...(sessionAllowedWritePaths.length > 0
1211
- ? [` Session write: ${sessionAllowedWritePaths.join(', ')}`]
1212
- : []),
1213
- ];
1214
-
1215
- ctx.ui.notify(lines.join('\n'), 'info');
1216
- },
1217
- });
1199
+ if (enableSandbox(ctx)) ctx.ui.notify('Sandbox enabled', 'info');
1200
+ },
1201
+ });
1202
+
1203
+ maybePi.registerCommand?.('sandbox-disable', {
1204
+ description: 'Disable the landstrip sandbox for this session',
1205
+ handler: async (_args, ctx) => {
1206
+ if (!sandboxEnabled) {
1207
+ ctx.ui.notify('Sandbox is already disabled', 'info');
1208
+ return;
1209
+ }
1210
+
1211
+ sandboxEnabled = false;
1212
+ sandboxReady = false;
1213
+ ctx.ui.setStatus('sandbox', '');
1214
+ ctx.ui.notify('Sandbox disabled', 'info');
1215
+ },
1216
+ });
1217
+
1218
+ maybePi.registerCommand?.('sandbox', {
1219
+ description: 'Show sandbox configuration',
1220
+ handler: async (_args, ctx) => {
1221
+ if (!sandboxEnabled) {
1222
+ ctx.ui.notify('Sandbox is disabled', 'info');
1223
+ return;
1224
+ }
1225
+
1226
+ const config = loadConfig(ctx.cwd);
1227
+ const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
1228
+ const lines = [
1229
+ 'Sandbox Configuration',
1230
+ ` Project config: ${projectPath}`,
1231
+ ` Global config: ${globalPath}`,
1232
+ ` landstrip: ${binaryPath()}`,
1233
+ '',
1234
+ 'Network (bash through HTTP proxy):',
1235
+ ` Allowed domains: ${config.network.allowedDomains.join(', ') || '(none)'}`,
1236
+ ` Denied domains: ${config.network.deniedDomains.join(', ') || '(none)'}`,
1237
+ ...(sessionAllowedDomains.length > 0
1238
+ ? [` Session allowed: ${sessionAllowedDomains.join(', ')}`]
1239
+ : []),
1240
+ '',
1241
+ 'Filesystem (bash + read/write/edit tools):',
1242
+ ` Deny Read: ${config.filesystem.denyRead.join(', ') || '(none)'}`,
1243
+ ` Allow Read: ${config.filesystem.allowRead.join(', ') || '(none)'}`,
1244
+ ` Allow Write: ${config.filesystem.allowWrite.join(', ') || '(none)'}`,
1245
+ ` Deny Write: ${config.filesystem.denyWrite.join(', ') || '(none)'}`,
1246
+ ...(sessionAllowedReadPaths.length > 0
1247
+ ? [` Session read: ${sessionAllowedReadPaths.join(', ')}`]
1248
+ : []),
1249
+ ...(sessionAllowedWritePaths.length > 0
1250
+ ? [` Session write: ${sessionAllowedWritePaths.join(', ')}`]
1251
+ : []),
1252
+ ];
1253
+
1254
+ ctx.ui.notify(lines.join('\n'), 'info');
1255
+ },
1256
+ });
1257
+ }
1258
+
1259
+ return { createBashTool, register };
1218
1260
  }
package/landstrip.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare module '@jarkkojs/landstrip' {
2
+ function binaryPath(): string;
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "index.ts",
18
+ "landstrip.d.ts",
18
19
  "README.md",
19
20
  "sandbox.json"
20
21
  ],