pi-landstrip 0.11.1 → 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 +211 -83
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -72,15 +72,41 @@ interface LandstripPolicy {
|
|
|
72
72
|
filesystem: SandboxFilesystemConfig;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
type LandstripErrorReason = 'Other' | 'AccessDenied' | 'LaunchFailed' | 'SetupFailed' | 'Usage';
|
|
76
|
+
type LandstripOperation = 'read' | 'write';
|
|
77
|
+
type LandstripErrorType = 'filesystem' | 'network' | 'platform' | 'launch' | 'encoding';
|
|
78
|
+
|
|
75
79
|
interface LandstripErrorResponse {
|
|
76
|
-
reason:
|
|
80
|
+
reason: LandstripErrorReason;
|
|
77
81
|
file?: string;
|
|
82
|
+
operation?: LandstripOperation;
|
|
78
83
|
program?: string;
|
|
79
|
-
type?:
|
|
80
|
-
source
|
|
84
|
+
type?: LandstripErrorType;
|
|
85
|
+
source?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface LandstripBashCallbacks {
|
|
89
|
+
onStderr?: (data: Buffer) => void;
|
|
90
|
+
onErrorFd?: (data: Buffer) => void;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
const LANDSTRIP_VERSION = [0, 11,
|
|
93
|
+
const LANDSTRIP_VERSION = [0, 11, 9] as const;
|
|
94
|
+
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
95
|
+
const LANDSTRIP_ERROR_REASONS = new Set<LandstripErrorReason>([
|
|
96
|
+
'Other',
|
|
97
|
+
'AccessDenied',
|
|
98
|
+
'LaunchFailed',
|
|
99
|
+
'SetupFailed',
|
|
100
|
+
'Usage',
|
|
101
|
+
]);
|
|
102
|
+
const LANDSTRIP_OPERATIONS = new Set<LandstripOperation>(['read', 'write']);
|
|
103
|
+
const LANDSTRIP_ERROR_TYPES = new Set<LandstripErrorType>([
|
|
104
|
+
'filesystem',
|
|
105
|
+
'network',
|
|
106
|
+
'platform',
|
|
107
|
+
'launch',
|
|
108
|
+
'encoding',
|
|
109
|
+
]);
|
|
84
110
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
85
111
|
|
|
86
112
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
@@ -348,6 +374,22 @@ function normalizePathMatch(value: string, cwd: string): string | null {
|
|
|
348
374
|
return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
|
|
349
375
|
}
|
|
350
376
|
|
|
377
|
+
function isFilesystemAccessDenied(error: LandstripErrorResponse): boolean {
|
|
378
|
+
return error.reason === 'AccessDenied' && error.type === 'filesystem';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isLandstripErrorReason(value: string): value is LandstripErrorReason {
|
|
382
|
+
return LANDSTRIP_ERROR_REASONS.has(value as LandstripErrorReason);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isLandstripOperation(value: string): value is LandstripOperation {
|
|
386
|
+
return LANDSTRIP_OPERATIONS.has(value as LandstripOperation);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isLandstripErrorType(value: string): value is LandstripErrorType {
|
|
390
|
+
return LANDSTRIP_ERROR_TYPES.has(value as LandstripErrorType);
|
|
391
|
+
}
|
|
392
|
+
|
|
351
393
|
function extractCandidatePaths(command: string): string[] {
|
|
352
394
|
const paths: string[] = [];
|
|
353
395
|
// Split on whitespace, preserving quoted strings minimally
|
|
@@ -362,32 +404,13 @@ function extractCandidatePaths(command: string): string[] {
|
|
|
362
404
|
}
|
|
363
405
|
|
|
364
406
|
function extractBlockedPath(output: string, cwd: string, command?: string): string | null {
|
|
365
|
-
|
|
366
|
-
let match = output.match(
|
|
367
|
-
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
368
|
-
);
|
|
369
|
-
if (match) return normalizePathMatch(match[1], cwd);
|
|
370
|
-
|
|
371
|
-
// ls/cat/cp: cannot open/access/stat '/path': Permission denied
|
|
372
|
-
match = output.match(
|
|
373
|
-
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: Permission denied$/m,
|
|
374
|
-
);
|
|
375
|
-
if (match) return normalizePathMatch(match[1], cwd);
|
|
376
|
-
|
|
377
|
-
// Generic: cmd: /absolute/path: Permission denied or Operation not permitted
|
|
378
|
-
match = output.match(
|
|
379
|
-
/^[a-zA-Z0-9_-]+: (\/[^\n:]+): (?:Operation not permitted|Permission denied)$/m,
|
|
380
|
-
);
|
|
381
|
-
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
382
|
-
|
|
383
|
-
// Landstrip structured error format with file field
|
|
384
|
-
const landstripErrors = parseLandstripErrors(output);
|
|
407
|
+
const landstripErrors = parseLandstripErrors(output).filter(isFilesystemAccessDenied);
|
|
385
408
|
for (const error of landstripErrors) {
|
|
386
409
|
if (error.file) return normalizeBlockedPath(error.file, cwd);
|
|
387
410
|
}
|
|
388
411
|
|
|
389
412
|
// If landstrip reported an error but without a file field, try to
|
|
390
|
-
// extract the blocked path from the command itself
|
|
413
|
+
// extract the blocked path from the command itself.
|
|
391
414
|
if (landstripErrors.length > 0 && command) {
|
|
392
415
|
const config = loadConfig(cwd);
|
|
393
416
|
for (const candidate of extractCandidatePaths(command)) {
|
|
@@ -401,11 +424,61 @@ function extractBlockedPath(output: string, cwd: string, command?: string): stri
|
|
|
401
424
|
}
|
|
402
425
|
}
|
|
403
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
|
+
|
|
434
|
+
// bash/sh: line X: /path: Permission denied
|
|
435
|
+
match = output.match(
|
|
436
|
+
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
437
|
+
);
|
|
438
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
439
|
+
|
|
440
|
+
// ls/cat/cp: cannot open/access/stat '/path': Permission denied
|
|
441
|
+
match = output.match(
|
|
442
|
+
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: (?:Operation not permitted|Permission denied)$/m,
|
|
443
|
+
);
|
|
444
|
+
if (match) return normalizePathMatch(match[1], cwd);
|
|
445
|
+
|
|
446
|
+
// Generic: cmd: /absolute/path: Permission denied or Operation not permitted
|
|
447
|
+
match = output.match(
|
|
448
|
+
/^[a-zA-Z0-9_-]+: (\/[^:\n]+): (?:Operation not permitted|Permission denied)$/m,
|
|
449
|
+
);
|
|
450
|
+
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
451
|
+
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
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
|
+
|
|
404
471
|
return null;
|
|
405
472
|
}
|
|
406
473
|
|
|
407
|
-
function extractBlockedWritePath(output: string, cwd: string
|
|
408
|
-
|
|
474
|
+
function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
475
|
+
for (const error of parseLandstripErrors(output).filter(isFilesystemAccessDenied)) {
|
|
476
|
+
if (error.file && error.operation === 'write') {
|
|
477
|
+
return normalizeBlockedPath(error.file, cwd);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return extractNativeWriteDeniedPath(output, cwd);
|
|
409
482
|
}
|
|
410
483
|
|
|
411
484
|
function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
@@ -424,22 +497,21 @@ function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
|
424
497
|
|
|
425
498
|
if (
|
|
426
499
|
fields.reason &&
|
|
427
|
-
|
|
428
|
-
fields.source
|
|
500
|
+
isLandstripErrorReason(fields.reason) &&
|
|
501
|
+
(fields.source || fields.reason === 'AccessDenied')
|
|
429
502
|
) {
|
|
430
|
-
const error: LandstripErrorResponse = {
|
|
431
|
-
reason: fields.reason as LandstripErrorResponse['reason'],
|
|
432
|
-
source: fields.source,
|
|
433
|
-
};
|
|
503
|
+
const error: LandstripErrorResponse = { reason: fields.reason };
|
|
434
504
|
|
|
505
|
+
if (fields.source) error.source = fields.source;
|
|
435
506
|
if (fields.file) error.file = fields.file;
|
|
436
507
|
if (fields.program) error.program = fields.program;
|
|
437
508
|
|
|
438
|
-
if (
|
|
439
|
-
fields.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
509
|
+
if (fields.operation && isLandstripOperation(fields.operation)) {
|
|
510
|
+
error.operation = fields.operation;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (fields.type && isLandstripErrorType(fields.type)) {
|
|
514
|
+
error.type = fields.type;
|
|
443
515
|
}
|
|
444
516
|
|
|
445
517
|
errors.push(error);
|
|
@@ -463,7 +535,12 @@ function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
|
|
|
463
535
|
if (err.type) {
|
|
464
536
|
parts.push(`:${err.type}`);
|
|
465
537
|
}
|
|
466
|
-
|
|
538
|
+
if (err.operation) {
|
|
539
|
+
parts.push(`:${err.operation}`);
|
|
540
|
+
}
|
|
541
|
+
if (err.source) {
|
|
542
|
+
parts.push(`: ${err.source}`);
|
|
543
|
+
}
|
|
467
544
|
|
|
468
545
|
return parts.join('');
|
|
469
546
|
})
|
|
@@ -1003,7 +1080,7 @@ export function createLandstripIntegration(
|
|
|
1003
1080
|
|
|
1004
1081
|
function createLandstripBashOps(
|
|
1005
1082
|
ctx: ExtensionContext,
|
|
1006
|
-
|
|
1083
|
+
callbacks: LandstripBashCallbacks = {},
|
|
1007
1084
|
): BashOperations {
|
|
1008
1085
|
return {
|
|
1009
1086
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
@@ -1013,9 +1090,8 @@ export function createLandstripIntegration(
|
|
|
1013
1090
|
const config = loadConfig(cwd);
|
|
1014
1091
|
const allowNetwork = config.network.allowNetwork;
|
|
1015
1092
|
const proxy = allowNetwork ? null : await startProxy(ctx, cwd);
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1018
|
-
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];
|
|
1019
1095
|
|
|
1020
1096
|
return new Promise((resolvePromise, reject) => {
|
|
1021
1097
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
@@ -1035,7 +1111,7 @@ export function createLandstripIntegration(
|
|
|
1035
1111
|
cwd,
|
|
1036
1112
|
env: allowNetwork ? { ...process.env, ...env } : proxyEnv(env, proxy!.port),
|
|
1037
1113
|
detached: true,
|
|
1038
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1114
|
+
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
|
|
1039
1115
|
});
|
|
1040
1116
|
|
|
1041
1117
|
function killChild(): void {
|
|
@@ -1060,13 +1136,18 @@ export function createLandstripIntegration(
|
|
|
1060
1136
|
|
|
1061
1137
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1062
1138
|
let stderrAcc = '';
|
|
1139
|
+
let errorFdAcc = '';
|
|
1063
1140
|
|
|
1064
1141
|
child.stdout?.on('data', onData);
|
|
1065
1142
|
child.stderr?.on('data', (data: Buffer) => {
|
|
1066
1143
|
stderrAcc += data.toString('utf8');
|
|
1067
|
-
onStderr(data);
|
|
1144
|
+
callbacks.onStderr?.(data);
|
|
1068
1145
|
onData(data);
|
|
1069
1146
|
});
|
|
1147
|
+
child.stdio[3]?.on('data', (data: Buffer) => {
|
|
1148
|
+
errorFdAcc += data.toString('utf8');
|
|
1149
|
+
callbacks.onErrorFd?.(data);
|
|
1150
|
+
});
|
|
1070
1151
|
|
|
1071
1152
|
child.on('error', (error) => {
|
|
1072
1153
|
cleanup();
|
|
@@ -1084,7 +1165,14 @@ export function createLandstripIntegration(
|
|
|
1084
1165
|
return;
|
|
1085
1166
|
}
|
|
1086
1167
|
|
|
1087
|
-
const
|
|
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);
|
|
1088
1176
|
if (blockedPath && ctx.hasUI) {
|
|
1089
1177
|
const config = loadConfig(cwd);
|
|
1090
1178
|
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
@@ -1095,7 +1183,10 @@ export function createLandstripIntegration(
|
|
|
1095
1183
|
matchesPattern,
|
|
1096
1184
|
);
|
|
1097
1185
|
|
|
1098
|
-
if (
|
|
1186
|
+
if (blockedWritePath === blockedPath && !isWriteAllowed) {
|
|
1187
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1188
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1189
|
+
} else if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1099
1190
|
const choice = await promptReadBlock(
|
|
1100
1191
|
ctx,
|
|
1101
1192
|
blockedPath,
|
|
@@ -1107,7 +1198,7 @@ export function createLandstripIntegration(
|
|
|
1107
1198
|
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1108
1199
|
}
|
|
1109
1200
|
} else if (!blockedPath && ctx.hasUI) {
|
|
1110
|
-
const landstripErrors = parseLandstripErrors(
|
|
1201
|
+
const landstripErrors = parseLandstripErrors(errorOutput);
|
|
1111
1202
|
if (landstripErrors.length > 0) {
|
|
1112
1203
|
const formatted = formatLandstripErrors(landstripErrors);
|
|
1113
1204
|
ctx.ui.notify(`Sandbox blocked an operation: ${formatted}`, 'warning');
|
|
@@ -1128,60 +1219,94 @@ export function createLandstripIntegration(
|
|
|
1128
1219
|
onUpdate: AgentToolUpdateCallback<BashToolDetails | undefined> | undefined,
|
|
1129
1220
|
ctx: ExtensionContext,
|
|
1130
1221
|
): Promise<AgentToolResult<BashToolDetails | undefined>> {
|
|
1131
|
-
let
|
|
1222
|
+
let landstripErrorOutput = '';
|
|
1223
|
+
let stderrOutput = '';
|
|
1132
1224
|
const sandboxedBash = createBashToolDefinition(ctx.cwd, {
|
|
1133
|
-
operations: createLandstripBashOps(ctx,
|
|
1134
|
-
|
|
1225
|
+
operations: createLandstripBashOps(ctx, {
|
|
1226
|
+
onErrorFd: (data) => {
|
|
1227
|
+
landstripErrorOutput += data.toString('utf8');
|
|
1228
|
+
},
|
|
1229
|
+
onStderr: (data) => {
|
|
1230
|
+
stderrOutput += data.toString('utf8');
|
|
1231
|
+
},
|
|
1135
1232
|
}),
|
|
1136
1233
|
shellPath: SettingsManager.create(ctx.cwd).getShellPath(),
|
|
1137
1234
|
});
|
|
1138
1235
|
|
|
1139
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
|
+
|
|
1140
1278
|
let result: AgentToolResult<BashToolDetails | undefined>;
|
|
1141
1279
|
try {
|
|
1142
1280
|
result = await run();
|
|
1143
1281
|
} catch (error) {
|
|
1144
|
-
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);
|
|
1145
1293
|
if (landstripErrors.length > 0) {
|
|
1146
1294
|
throw new Error(formatLandstripErrors(landstripErrors));
|
|
1147
1295
|
}
|
|
1148
1296
|
throw error;
|
|
1149
1297
|
}
|
|
1150
|
-
const
|
|
1151
|
-
.filter((content) => content.type === 'text')
|
|
1152
|
-
.map((content) => content.text)
|
|
1153
|
-
.join('\n');
|
|
1154
|
-
const landstripErrors = parseLandstripErrors(landstripStderr);
|
|
1298
|
+
const landstripErrors = parseLandstripErrors(landstripErrorOutput);
|
|
1155
1299
|
if (landstripErrors.length > 0) {
|
|
1156
1300
|
const message = formatLandstripErrors(landstripErrors);
|
|
1157
1301
|
result.content.unshift({ type: 'text', text: `\n${message}\n` });
|
|
1158
1302
|
}
|
|
1159
|
-
const blockedPath =
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1164
|
-
if (choice === 'abort') return result;
|
|
1165
|
-
|
|
1166
|
-
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
1303
|
+
const blockedPath =
|
|
1304
|
+
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1305
|
+
extractBlockedWritePath(stderrOutput, ctx.cwd);
|
|
1306
|
+
if (!blockedPath) return result;
|
|
1167
1307
|
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1171
|
-
ctx.ui.notify(
|
|
1172
|
-
`"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1173
|
-
'warning',
|
|
1174
|
-
);
|
|
1175
|
-
return result;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
onUpdate?.({
|
|
1179
|
-
content: [
|
|
1180
|
-
{ type: 'text', text: `\n--- Write access granted for "${blockedPath}", retrying ---\n` },
|
|
1181
|
-
],
|
|
1182
|
-
details: {},
|
|
1183
|
-
});
|
|
1184
|
-
return run();
|
|
1308
|
+
const retryResult = await retryWithWriteAccess(blockedPath);
|
|
1309
|
+
return retryResult ?? result;
|
|
1185
1310
|
}
|
|
1186
1311
|
|
|
1187
1312
|
async function preflightCommandDomains(
|
|
@@ -1256,7 +1381,10 @@ export function createLandstripIntegration(
|
|
|
1256
1381
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1257
1382
|
sandboxEnabled = false;
|
|
1258
1383
|
sandboxReady = false;
|
|
1259
|
-
ctx.ui.notify(
|
|
1384
|
+
ctx.ui.notify(
|
|
1385
|
+
`landstrip ${REQUIRED_LANDSTRIP_VERSION} or newer is required; found: ${version}`,
|
|
1386
|
+
'error',
|
|
1387
|
+
);
|
|
1260
1388
|
return false;
|
|
1261
1389
|
}
|
|
1262
1390
|
|
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",
|