pi-landstrip 0.11.2 → 0.11.3
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/index.ts +122 -43
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -85,7 +85,12 @@ interface LandstripErrorResponse {
|
|
|
85
85
|
source?: string;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
interface LandstripBashCallbacks {
|
|
89
|
+
onStderr?: (data: Buffer) => void;
|
|
90
|
+
onErrorFd?: (data: Buffer) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const LANDSTRIP_VERSION = [0, 11, 9] as const;
|
|
89
94
|
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
90
95
|
const LANDSTRIP_ERROR_REASONS = new Set<LandstripErrorReason>([
|
|
91
96
|
'Other',
|
|
@@ -419,15 +424,22 @@ function extractBlockedPath(output: string, cwd: string, command?: string): stri
|
|
|
419
424
|
}
|
|
420
425
|
}
|
|
421
426
|
|
|
427
|
+
return extractNativeDeniedPath(output, cwd);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function extractNativeDeniedPath(output: string, cwd: string): string | null {
|
|
431
|
+
let match = output.match(/['"]([^'"\n]+)['"]:\s+(?:Operation not permitted|Permission denied)/);
|
|
432
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
433
|
+
|
|
422
434
|
// bash/sh: line X: /path: Permission denied
|
|
423
|
-
|
|
435
|
+
match = output.match(
|
|
424
436
|
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
425
437
|
);
|
|
426
438
|
if (match) return normalizePathMatch(match[1], cwd);
|
|
427
439
|
|
|
428
440
|
// ls/cat/cp: cannot open/access/stat '/path': Permission denied
|
|
429
441
|
match = output.match(
|
|
430
|
-
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: Permission denied$/m,
|
|
442
|
+
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: (?:Operation not permitted|Permission denied)$/m,
|
|
431
443
|
);
|
|
432
444
|
if (match) return normalizePathMatch(match[1], cwd);
|
|
433
445
|
|
|
@@ -440,6 +452,25 @@ function extractBlockedPath(output: string, cwd: string, command?: string): stri
|
|
|
440
452
|
return null;
|
|
441
453
|
}
|
|
442
454
|
|
|
455
|
+
function extractNativeWriteDeniedPath(output: string, cwd: string): string | null {
|
|
456
|
+
let match = output.match(
|
|
457
|
+
/(?:[Uu]nable to create|cannot (?:create|touch|mkdir|remove|unlink|rename)|for writing)[^'"\n]*['"]([^'"\n]+)['"]:\s+(?:Operation not permitted|Permission denied)/m,
|
|
458
|
+
);
|
|
459
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
460
|
+
|
|
461
|
+
match = output.match(
|
|
462
|
+
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
463
|
+
);
|
|
464
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
465
|
+
|
|
466
|
+
match = output.match(
|
|
467
|
+
/^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: (?:Operation not permitted|Permission denied)$/m,
|
|
468
|
+
);
|
|
469
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
470
|
+
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
443
474
|
function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
444
475
|
for (const error of parseLandstripErrors(output).filter(isFilesystemAccessDenied)) {
|
|
445
476
|
if (error.file && error.operation === 'write') {
|
|
@@ -447,7 +478,7 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
|
447
478
|
}
|
|
448
479
|
}
|
|
449
480
|
|
|
450
|
-
return
|
|
481
|
+
return extractNativeWriteDeniedPath(output, cwd);
|
|
451
482
|
}
|
|
452
483
|
|
|
453
484
|
function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
@@ -1049,7 +1080,7 @@ export function createLandstripIntegration(
|
|
|
1049
1080
|
|
|
1050
1081
|
function createLandstripBashOps(
|
|
1051
1082
|
ctx: ExtensionContext,
|
|
1052
|
-
|
|
1083
|
+
callbacks: LandstripBashCallbacks = {},
|
|
1053
1084
|
): BashOperations {
|
|
1054
1085
|
return {
|
|
1055
1086
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
@@ -1059,9 +1090,8 @@ export function createLandstripIntegration(
|
|
|
1059
1090
|
const config = loadConfig(cwd);
|
|
1060
1091
|
const allowNetwork = config.network.allowNetwork;
|
|
1061
1092
|
const proxy = allowNetwork ? null : await startProxy(ctx, cwd);
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
const landstripArgs = ['-p', policy.path, shell, ...args, command];
|
|
1093
|
+
const policy = writePolicyFile(cwd, proxy?.port ?? null);
|
|
1094
|
+
const landstripArgs = ['--error-fd', '3', '-p', policy.path, shell, ...args, command];
|
|
1065
1095
|
|
|
1066
1096
|
return new Promise((resolvePromise, reject) => {
|
|
1067
1097
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
@@ -1081,7 +1111,7 @@ export function createLandstripIntegration(
|
|
|
1081
1111
|
cwd,
|
|
1082
1112
|
env: allowNetwork ? { ...process.env, ...env } : proxyEnv(env, proxy!.port),
|
|
1083
1113
|
detached: true,
|
|
1084
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1114
|
+
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
|
|
1085
1115
|
});
|
|
1086
1116
|
|
|
1087
1117
|
function killChild(): void {
|
|
@@ -1106,13 +1136,18 @@ export function createLandstripIntegration(
|
|
|
1106
1136
|
|
|
1107
1137
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1108
1138
|
let stderrAcc = '';
|
|
1139
|
+
let errorFdAcc = '';
|
|
1109
1140
|
|
|
1110
1141
|
child.stdout?.on('data', onData);
|
|
1111
1142
|
child.stderr?.on('data', (data: Buffer) => {
|
|
1112
1143
|
stderrAcc += data.toString('utf8');
|
|
1113
|
-
onStderr(data);
|
|
1144
|
+
callbacks.onStderr?.(data);
|
|
1114
1145
|
onData(data);
|
|
1115
1146
|
});
|
|
1147
|
+
child.stdio[3]?.on('data', (data: Buffer) => {
|
|
1148
|
+
errorFdAcc += data.toString('utf8');
|
|
1149
|
+
callbacks.onErrorFd?.(data);
|
|
1150
|
+
});
|
|
1116
1151
|
|
|
1117
1152
|
child.on('error', (error) => {
|
|
1118
1153
|
cleanup();
|
|
@@ -1130,8 +1165,14 @@ export function createLandstripIntegration(
|
|
|
1130
1165
|
return;
|
|
1131
1166
|
}
|
|
1132
1167
|
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1168
|
+
const errorOutput = errorFdAcc || stderrAcc;
|
|
1169
|
+
|
|
1170
|
+
const blockedPath =
|
|
1171
|
+
extractBlockedPath(errorOutput, cwd, command) ??
|
|
1172
|
+
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd, command) : null);
|
|
1173
|
+
const blockedWritePath =
|
|
1174
|
+
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1175
|
+
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1135
1176
|
if (blockedPath && ctx.hasUI) {
|
|
1136
1177
|
const config = loadConfig(cwd);
|
|
1137
1178
|
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
@@ -1157,7 +1198,7 @@ export function createLandstripIntegration(
|
|
|
1157
1198
|
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1158
1199
|
}
|
|
1159
1200
|
} else if (!blockedPath && ctx.hasUI) {
|
|
1160
|
-
const landstripErrors = parseLandstripErrors(
|
|
1201
|
+
const landstripErrors = parseLandstripErrors(errorOutput);
|
|
1161
1202
|
if (landstripErrors.length > 0) {
|
|
1162
1203
|
const formatted = formatLandstripErrors(landstripErrors);
|
|
1163
1204
|
ctx.ui.notify(`Sandbox blocked an operation: ${formatted}`, 'warning');
|
|
@@ -1178,56 +1219,94 @@ export function createLandstripIntegration(
|
|
|
1178
1219
|
onUpdate: AgentToolUpdateCallback<BashToolDetails | undefined> | undefined,
|
|
1179
1220
|
ctx: ExtensionContext,
|
|
1180
1221
|
): Promise<AgentToolResult<BashToolDetails | undefined>> {
|
|
1181
|
-
let
|
|
1222
|
+
let landstripErrorOutput = '';
|
|
1223
|
+
let stderrOutput = '';
|
|
1182
1224
|
const sandboxedBash = createBashToolDefinition(ctx.cwd, {
|
|
1183
|
-
operations: createLandstripBashOps(ctx,
|
|
1184
|
-
|
|
1225
|
+
operations: createLandstripBashOps(ctx, {
|
|
1226
|
+
onErrorFd: (data) => {
|
|
1227
|
+
landstripErrorOutput += data.toString('utf8');
|
|
1228
|
+
},
|
|
1229
|
+
onStderr: (data) => {
|
|
1230
|
+
stderrOutput += data.toString('utf8');
|
|
1231
|
+
},
|
|
1185
1232
|
}),
|
|
1186
1233
|
shellPath: SettingsManager.create(ctx.cwd).getShellPath(),
|
|
1187
1234
|
});
|
|
1188
1235
|
|
|
1189
1236
|
const run = () => sandboxedBash.execute(id, params, signal, onUpdate, ctx);
|
|
1237
|
+
const retryWithWriteAccess = async (
|
|
1238
|
+
blockedPath: string,
|
|
1239
|
+
): Promise<AgentToolResult<BashToolDetails | undefined> | null> => {
|
|
1240
|
+
if (!ctx.hasUI) return null;
|
|
1241
|
+
|
|
1242
|
+
let config = loadConfig(ctx.cwd);
|
|
1243
|
+
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1244
|
+
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1245
|
+
ctx.ui.notify(
|
|
1246
|
+
`"${blockedPath}" is blocked by denyWrite. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1247
|
+
'warning',
|
|
1248
|
+
);
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(ctx.cwd), matchesPattern)) {
|
|
1253
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1254
|
+
if (choice === 'abort') return null;
|
|
1255
|
+
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
config = loadConfig(ctx.cwd);
|
|
1259
|
+
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1260
|
+
ctx.ui.notify(
|
|
1261
|
+
`"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1262
|
+
'warning',
|
|
1263
|
+
);
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
onUpdate?.({
|
|
1268
|
+
content: [
|
|
1269
|
+
{ type: 'text', text: `\n--- Write access granted for "${blockedPath}", retrying ---\n` },
|
|
1270
|
+
],
|
|
1271
|
+
details: {},
|
|
1272
|
+
});
|
|
1273
|
+
landstripErrorOutput = '';
|
|
1274
|
+
stderrOutput = '';
|
|
1275
|
+
return run();
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1190
1278
|
let result: AgentToolResult<BashToolDetails | undefined>;
|
|
1191
1279
|
try {
|
|
1192
1280
|
result = await run();
|
|
1193
1281
|
} catch (error) {
|
|
1194
|
-
const
|
|
1282
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
1283
|
+
const fallbackOutput = `${stderrOutput}\n${errorText}`;
|
|
1284
|
+
const blockedPath =
|
|
1285
|
+
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1286
|
+
extractBlockedWritePath(fallbackOutput, ctx.cwd);
|
|
1287
|
+
if (blockedPath) {
|
|
1288
|
+
const retryResult = await retryWithWriteAccess(blockedPath);
|
|
1289
|
+
if (retryResult) return retryResult;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const landstripErrors = parseLandstripErrors(landstripErrorOutput || errorText);
|
|
1195
1293
|
if (landstripErrors.length > 0) {
|
|
1196
1294
|
throw new Error(formatLandstripErrors(landstripErrors));
|
|
1197
1295
|
}
|
|
1198
1296
|
throw error;
|
|
1199
1297
|
}
|
|
1200
|
-
const landstripErrors = parseLandstripErrors(
|
|
1298
|
+
const landstripErrors = parseLandstripErrors(landstripErrorOutput);
|
|
1201
1299
|
if (landstripErrors.length > 0) {
|
|
1202
1300
|
const message = formatLandstripErrors(landstripErrors);
|
|
1203
1301
|
result.content.unshift({ type: 'text', text: `\n${message}\n` });
|
|
1204
1302
|
}
|
|
1205
|
-
const blockedPath =
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1210
|
-
if (choice === 'abort') return result;
|
|
1211
|
-
|
|
1212
|
-
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
1303
|
+
const blockedPath =
|
|
1304
|
+
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1305
|
+
extractBlockedWritePath(stderrOutput, ctx.cwd);
|
|
1306
|
+
if (!blockedPath) return result;
|
|
1213
1307
|
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1217
|
-
ctx.ui.notify(
|
|
1218
|
-
`"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1219
|
-
'warning',
|
|
1220
|
-
);
|
|
1221
|
-
return result;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
onUpdate?.({
|
|
1225
|
-
content: [
|
|
1226
|
-
{ type: 'text', text: `\n--- Write access granted for "${blockedPath}", retrying ---\n` },
|
|
1227
|
-
],
|
|
1228
|
-
details: {},
|
|
1229
|
-
});
|
|
1230
|
-
return run();
|
|
1308
|
+
const retryResult = await retryWithWriteAccess(blockedPath);
|
|
1309
|
+
return retryResult ?? result;
|
|
1231
1310
|
}
|
|
1232
1311
|
|
|
1233
1312
|
async function preflightCommandDomains(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Landlock-based sandboxing for pi with interactive permission prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
34
|
-
"@jarkkojs/landstrip": "^0.11.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.11.9"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|