pi-landstrip 0.4.0 → 0.5.1
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 +386 -135
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
isToolCallEventType,
|
|
38
38
|
SettingsManager,
|
|
39
39
|
} from '@earendil-works/pi-coding-agent';
|
|
40
|
-
import { Key, matchesKey, truncateToWidth } from '@earendil-works/pi-tui';
|
|
40
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
41
41
|
|
|
42
42
|
interface SandboxFilesystemConfig {
|
|
43
43
|
denyRead: string[];
|
|
@@ -80,7 +80,7 @@ interface LandstripErrorResponse {
|
|
|
80
80
|
message: string;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const LANDSTRIP_VERSION = [0, 10,
|
|
83
|
+
const LANDSTRIP_VERSION = [0, 10, 2] as const;
|
|
84
84
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
85
85
|
|
|
86
86
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
@@ -109,8 +109,8 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
109
109
|
},
|
|
110
110
|
filesystem: {
|
|
111
111
|
denyRead: ['/Users', '/home'],
|
|
112
|
-
allowRead: ['.', '~/.config', '~/.gitconfig', '~/.local', '~/.cargo'],
|
|
113
|
-
allowWrite: ['.', '/tmp'],
|
|
112
|
+
allowRead: ['.', '~/.config', '~/.gitconfig', '~/.local', '~/.cargo', '/dev/null'],
|
|
113
|
+
allowWrite: ['.', '/tmp', '/dev/null'],
|
|
114
114
|
denyWrite: ['.env', '.env.*', '*.pem', '*.key'],
|
|
115
115
|
},
|
|
116
116
|
};
|
|
@@ -337,12 +337,30 @@ function normalizeBlockedPath(path: string, cwd: string): string {
|
|
|
337
337
|
return canonicalizePath(isAbsolute(path) ? path : join(cwd, path));
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
function
|
|
341
|
-
|
|
340
|
+
function extractBlockedPath(output: string, cwd: string): string | null {
|
|
341
|
+
// bash/sh: line X: /path: Permission denied
|
|
342
|
+
let match = output.match(
|
|
342
343
|
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
343
344
|
);
|
|
345
|
+
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
346
|
+
|
|
347
|
+
// ls/cat/cp: cannot open/access/stat '/path': Permission denied
|
|
348
|
+
match = output.match(
|
|
349
|
+
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: Permission denied$/m,
|
|
350
|
+
);
|
|
351
|
+
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
344
352
|
|
|
345
|
-
|
|
353
|
+
// Generic: cmd: /absolute/path: Permission denied or Operation not permitted
|
|
354
|
+
match = output.match(
|
|
355
|
+
/^[a-zA-Z0-9_-]+: (\/[^\n:]+): (?:Operation not permitted|Permission denied)$/m,
|
|
356
|
+
);
|
|
357
|
+
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
363
|
+
return extractBlockedPath(output, cwd);
|
|
346
364
|
}
|
|
347
365
|
|
|
348
366
|
function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
@@ -414,99 +432,168 @@ async function showPermissionPrompt(
|
|
|
414
432
|
): Promise<PermissionChoice> {
|
|
415
433
|
if (!ctx.hasUI) return 'abort';
|
|
416
434
|
|
|
417
|
-
const result = await ctx.ui.custom<PermissionChoice>(
|
|
418
|
-
|
|
419
|
-
|
|
435
|
+
const result = await ctx.ui.custom<PermissionChoice>(
|
|
436
|
+
(tui, theme, _kb, done) => {
|
|
437
|
+
let selectedIndex = 0;
|
|
438
|
+
let pendingAction: PermissionChoice | null = null;
|
|
420
439
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
440
|
+
function resolveChoice(action: PermissionChoice): void {
|
|
441
|
+
done(action);
|
|
442
|
+
}
|
|
424
443
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
return {
|
|
445
|
+
render(width: number): string[] {
|
|
446
|
+
const innerW = width - 4;
|
|
447
|
+
const lines: string[] = [];
|
|
448
|
+
const border = theme.fg('border', '│');
|
|
449
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
450
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
451
|
+
|
|
452
|
+
// Top border with title
|
|
453
|
+
const label = ' Sandbox ';
|
|
454
|
+
const topLeft = borderFg('╭─');
|
|
455
|
+
const topRight = borderFg('─╮');
|
|
456
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
457
|
+
lines.push(`${topLeft}${theme.fg('accent', label)}${topFill}${topRight}`);
|
|
458
|
+
|
|
459
|
+
// Blank spacing
|
|
460
|
+
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
461
|
+
|
|
462
|
+
// Title line
|
|
463
|
+
const titleText = truncateToWidth(theme.fg('warning', title), innerW);
|
|
464
|
+
const titlePad = Math.max(0, innerW - visibleWidth(titleText));
|
|
465
|
+
lines.push(`${border} ${titleText}${' '.repeat(titlePad)} ${border}`);
|
|
466
|
+
|
|
467
|
+
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
468
|
+
|
|
469
|
+
// Options
|
|
470
|
+
for (let i = 0; i < options.length; i++) {
|
|
471
|
+
const option = options[i];
|
|
472
|
+
const isSelected = i === selectedIndex;
|
|
473
|
+
const isPending = pendingAction === option.action;
|
|
474
|
+
|
|
475
|
+
// Section divider before the permanent options (index 2 and 3)
|
|
476
|
+
if (i === 2) {
|
|
477
|
+
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
478
|
+
const secLabel = ' Permanent ';
|
|
479
|
+
const secDash = '─'.repeat(Math.max(0, innerW - visibleWidth(secLabel)));
|
|
480
|
+
lines.push(`${border} ${dim(secDash + secLabel)} ${border}`);
|
|
481
|
+
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
482
|
+
}
|
|
444
483
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
484
|
+
// Key badge
|
|
485
|
+
const keyBadge = isSelected
|
|
486
|
+
? theme.fg('accent', `[${option.key}]`)
|
|
487
|
+
: dim(` ${option.key} `);
|
|
488
|
+
|
|
489
|
+
// Selection indicator
|
|
490
|
+
let cursor: string;
|
|
491
|
+
if (isSelected && isPending) {
|
|
492
|
+
cursor = theme.fg('warning', '▶');
|
|
493
|
+
} else if (isSelected) {
|
|
494
|
+
cursor = theme.fg('accent', '▶');
|
|
495
|
+
} else {
|
|
496
|
+
cursor = ' ';
|
|
497
|
+
}
|
|
457
498
|
|
|
458
|
-
|
|
459
|
-
|
|
499
|
+
// Label
|
|
500
|
+
let label: string;
|
|
501
|
+
if (isPending) {
|
|
502
|
+
label = theme.fg('warning', option.label + ' — press Enter to confirm');
|
|
503
|
+
} else if (isSelected) {
|
|
504
|
+
label = theme.fg('text', option.label);
|
|
505
|
+
} else {
|
|
506
|
+
label = dim(option.label);
|
|
507
|
+
}
|
|
460
508
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
509
|
+
// Hint
|
|
510
|
+
let hint = '';
|
|
511
|
+
if (option.hint && !isPending) {
|
|
512
|
+
hint = ' ' + dim(option.hint);
|
|
513
|
+
}
|
|
466
514
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
515
|
+
const fullLine = ` ${cursor} ${keyBadge} ${label}${hint}`;
|
|
516
|
+
const line = truncateToWidth(fullLine, innerW);
|
|
517
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
518
|
+
lines.push(`${border} ${line}${' '.repeat(pad)} ${border}`);
|
|
519
|
+
}
|
|
471
520
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
521
|
+
// Footer
|
|
522
|
+
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
523
|
+
const footerText = pendingAction
|
|
524
|
+
? '↑↓ navigate enter confirm esc cancel'
|
|
525
|
+
: '↑↓ navigate enter select esc dismiss';
|
|
526
|
+
const footerLine = dim(footerText);
|
|
527
|
+
const footerPad = Math.max(0, innerW - visibleWidth(footerLine));
|
|
528
|
+
lines.push(`${border} ${footerLine}${' '.repeat(footerPad)} ${border}`);
|
|
529
|
+
|
|
530
|
+
// Bottom border
|
|
531
|
+
const botLeft = borderFg('╰');
|
|
532
|
+
const botRight = borderFg('╯');
|
|
533
|
+
const botFill = borderFg('─'.repeat(width - 2));
|
|
534
|
+
lines.push(`${botLeft}${botFill}${botRight}`);
|
|
535
|
+
|
|
536
|
+
return lines;
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
handleInput(data: string): void {
|
|
540
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
541
|
+
resolveChoice('abort');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
478
544
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
545
|
+
if (matchesKey(data, Key.enter)) {
|
|
546
|
+
resolveChoice(pendingAction ?? options[selectedIndex]?.action ?? 'abort');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
485
549
|
|
|
486
|
-
|
|
487
|
-
|
|
550
|
+
if (matchesKey(data, Key.up)) {
|
|
551
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
552
|
+
pendingAction = null;
|
|
553
|
+
tui.requestRender();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
488
556
|
|
|
489
|
-
if (data
|
|
490
|
-
|
|
557
|
+
if (matchesKey(data, Key.down)) {
|
|
558
|
+
selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
|
|
559
|
+
pendingAction = null;
|
|
560
|
+
tui.requestRender();
|
|
491
561
|
return;
|
|
492
562
|
}
|
|
493
563
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
} else {
|
|
564
|
+
for (let i = 0; i < options.length; i++) {
|
|
565
|
+
const option = options[i];
|
|
566
|
+
|
|
567
|
+
if (data === option.key) {
|
|
499
568
|
resolveChoice(option.action);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (data.toLowerCase() === option.key.toLowerCase()) {
|
|
573
|
+
if (option.confirm) {
|
|
574
|
+
pendingAction = option.action;
|
|
575
|
+
selectedIndex = i;
|
|
576
|
+
} else {
|
|
577
|
+
resolveChoice(option.action);
|
|
578
|
+
}
|
|
579
|
+
tui.requestRender();
|
|
580
|
+
return;
|
|
500
581
|
}
|
|
501
|
-
tui.requestRender();
|
|
502
|
-
return;
|
|
503
582
|
}
|
|
504
|
-
}
|
|
505
|
-
},
|
|
583
|
+
},
|
|
506
584
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
585
|
+
invalidate(): void {},
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
overlay: true,
|
|
590
|
+
overlayOptions: {
|
|
591
|
+
anchor: 'center',
|
|
592
|
+
width: 72,
|
|
593
|
+
margin: 2,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
);
|
|
510
597
|
|
|
511
598
|
return result ?? 'abort';
|
|
512
599
|
}
|
|
@@ -519,12 +606,15 @@ function promptDomainBlock(ctx: ExtensionContext, domain: string): Promise<Permi
|
|
|
519
606
|
);
|
|
520
607
|
}
|
|
521
608
|
|
|
522
|
-
function promptReadBlock(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
609
|
+
function promptReadBlock(
|
|
610
|
+
ctx: ExtensionContext,
|
|
611
|
+
filePath: string,
|
|
612
|
+
reason?: string,
|
|
613
|
+
): Promise<PermissionChoice> {
|
|
614
|
+
const title = reason
|
|
615
|
+
? `Read blocked: "${filePath}" is in denyRead (${reason})`
|
|
616
|
+
: `Read blocked: "${filePath}" is not in allowRead`;
|
|
617
|
+
return showPermissionPrompt(ctx, title, PERMISSION_OPTIONS);
|
|
528
618
|
}
|
|
529
619
|
|
|
530
620
|
function promptWriteBlock(ctx: ExtensionContext, filePath: string): Promise<PermissionChoice> {
|
|
@@ -924,8 +1014,11 @@ export function createLandstripIntegration(
|
|
|
924
1014
|
}
|
|
925
1015
|
|
|
926
1016
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1017
|
+
let stderrAcc = '';
|
|
1018
|
+
|
|
927
1019
|
child.stdout?.on('data', onData);
|
|
928
1020
|
child.stderr?.on('data', (data: Buffer) => {
|
|
1021
|
+
stderrAcc += data.toString('utf8');
|
|
929
1022
|
onStderr(data);
|
|
930
1023
|
onData(data);
|
|
931
1024
|
});
|
|
@@ -935,15 +1028,42 @@ export function createLandstripIntegration(
|
|
|
935
1028
|
reject(error);
|
|
936
1029
|
});
|
|
937
1030
|
|
|
938
|
-
child.on('close', (code) => {
|
|
1031
|
+
child.on('close', async (code) => {
|
|
939
1032
|
cleanup();
|
|
940
1033
|
if (signal?.aborted) {
|
|
941
1034
|
reject(new Error('aborted'));
|
|
942
|
-
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (timedOut) {
|
|
943
1038
|
reject(new Error(`timeout:${timeout}`));
|
|
944
|
-
|
|
945
|
-
resolvePromise({ exitCode: code });
|
|
1039
|
+
return;
|
|
946
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
const blockedPath = extractBlockedPath(stderrAcc, cwd);
|
|
1043
|
+
if (blockedPath && ctx.hasUI) {
|
|
1044
|
+
const config = loadConfig(cwd);
|
|
1045
|
+
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
1046
|
+
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(cwd));
|
|
1047
|
+
const isWriteAllowed = !shouldPromptForWrite(
|
|
1048
|
+
blockedPath,
|
|
1049
|
+
getEffectiveAllowWrite(cwd),
|
|
1050
|
+
matchesPattern,
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1054
|
+
const choice = await promptReadBlock(
|
|
1055
|
+
ctx,
|
|
1056
|
+
blockedPath,
|
|
1057
|
+
isDeniedByDenyRead ? 'denyRead overrides allowRead' : undefined,
|
|
1058
|
+
);
|
|
1059
|
+
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1060
|
+
} else if (!isWriteAllowed) {
|
|
1061
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1062
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
resolvePromise({ exitCode: code });
|
|
947
1067
|
});
|
|
948
1068
|
});
|
|
949
1069
|
},
|
|
@@ -1037,18 +1157,28 @@ export function createLandstripIntegration(
|
|
|
1037
1157
|
}
|
|
1038
1158
|
|
|
1039
1159
|
function enableStatus(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1160
|
+
const theme = ctx.ui.theme;
|
|
1161
|
+
const dot = theme.fg('success', '●');
|
|
1162
|
+
const label = theme.fg('text', 'Sandbox');
|
|
1163
|
+
|
|
1164
|
+
let networkLabel: string;
|
|
1165
|
+
let networkColor: 'warning' | 'accent';
|
|
1166
|
+
if (config.network.allowNetwork) {
|
|
1167
|
+
networkLabel = 'unrestricted';
|
|
1168
|
+
networkColor = 'warning';
|
|
1169
|
+
} else if (allowsAllDomains(config.network.allowedDomains)) {
|
|
1170
|
+
networkLabel = 'any domain';
|
|
1171
|
+
networkColor = 'warning';
|
|
1172
|
+
} else {
|
|
1173
|
+
networkLabel = `${config.network.allowedDomains.length} domains`;
|
|
1174
|
+
networkColor = 'accent';
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const sep = theme.fg('dim', '·');
|
|
1178
|
+
const net = theme.fg(networkColor, networkLabel);
|
|
1179
|
+
const write = theme.fg('accent', `${config.filesystem.allowWrite.length} write paths`);
|
|
1180
|
+
|
|
1181
|
+
ctx.ui.setStatus('sandbox', `${dot} ${label} ${sep} ${net} ${sep} ${write}`);
|
|
1052
1182
|
}
|
|
1053
1183
|
|
|
1054
1184
|
function enableSandbox(ctx: ExtensionContext): boolean {
|
|
@@ -1075,7 +1205,7 @@ export function createLandstripIntegration(
|
|
|
1075
1205
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1076
1206
|
sandboxEnabled = false;
|
|
1077
1207
|
sandboxReady = false;
|
|
1078
|
-
ctx.ui.notify(`landstrip 0.10.
|
|
1208
|
+
ctx.ui.notify(`landstrip 0.10.2 or newer is required; found: ${version}`, 'error');
|
|
1079
1209
|
return false;
|
|
1080
1210
|
}
|
|
1081
1211
|
|
|
@@ -1241,7 +1371,7 @@ export function createLandstripIntegration(
|
|
|
1241
1371
|
|
|
1242
1372
|
sandboxEnabled = false;
|
|
1243
1373
|
sandboxReady = false;
|
|
1244
|
-
ctx.ui.setStatus('sandbox',
|
|
1374
|
+
ctx.ui.setStatus('sandbox', undefined);
|
|
1245
1375
|
ctx.ui.notify('Sandbox disabled', 'info');
|
|
1246
1376
|
},
|
|
1247
1377
|
});
|
|
@@ -1256,34 +1386,155 @@ export function createLandstripIntegration(
|
|
|
1256
1386
|
|
|
1257
1387
|
const config = loadConfig(ctx.cwd);
|
|
1258
1388
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1389
|
+
|
|
1390
|
+
await ctx.ui.custom(
|
|
1391
|
+
(tui, theme, _kb, done) => {
|
|
1392
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
1393
|
+
const muted = (s: string) => theme.fg('muted', s);
|
|
1394
|
+
const accent = (s: string) => theme.fg('accent', s);
|
|
1395
|
+
const text = (s: string) => theme.fg('text', s);
|
|
1396
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
1397
|
+
|
|
1398
|
+
function boolVal(v: boolean): string {
|
|
1399
|
+
return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function makeRow(content: string, innerW: number, border: string): string {
|
|
1403
|
+
const line = truncateToWidth(content, innerW);
|
|
1404
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
1405
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
render(width: number): string[] {
|
|
1410
|
+
const innerW = width - 4;
|
|
1411
|
+
const border = borderFg('│');
|
|
1412
|
+
const row = (c: string) => makeRow(c, innerW, border);
|
|
1413
|
+
const lines: string[] = [];
|
|
1414
|
+
|
|
1415
|
+
// Top border
|
|
1416
|
+
const title = accent(' Sandbox Configuration ');
|
|
1417
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
1418
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
1419
|
+
|
|
1420
|
+
// Status
|
|
1421
|
+
const statusDot = theme.fg('success', '●');
|
|
1422
|
+
const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 27)));
|
|
1423
|
+
lines.push(
|
|
1424
|
+
row(
|
|
1425
|
+
` ${statusDot} ${text('Active')} ${dim('·')} ${muted('landstrip:')} ${pathSnippet}`,
|
|
1426
|
+
),
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
// Config files
|
|
1430
|
+
lines.push(row(` ${dim('Config files:')}`));
|
|
1431
|
+
lines.push(row(` ${dim('project')} ${text(projectPath)}`));
|
|
1432
|
+
lines.push(row(` ${dim('global')} ${text(globalPath)}`));
|
|
1433
|
+
|
|
1434
|
+
// Network section
|
|
1435
|
+
lines.push(row(''));
|
|
1436
|
+
lines.push(row(`${'─'.repeat(innerW)}`));
|
|
1437
|
+
const netMode = config.network.allowNetwork ? ' (unrestricted)' : ' (proxied)';
|
|
1438
|
+
lines.push(row(` ${accent('Network')}${dim(netMode)}`));
|
|
1439
|
+
lines.push(
|
|
1440
|
+
row(
|
|
1441
|
+
` ${dim('•')} ${muted('Allow network:')} ${boolVal(config.network.allowNetwork)}`,
|
|
1442
|
+
),
|
|
1443
|
+
);
|
|
1444
|
+
const domainsStr = config.network.allowedDomains.join(', ') || '(none)';
|
|
1445
|
+
lines.push(
|
|
1446
|
+
row(
|
|
1447
|
+
` ${dim('•')} ${muted('Allowed:')} ${text(truncateToWidth(domainsStr, Math.max(10, innerW - 15)))}`,
|
|
1448
|
+
),
|
|
1449
|
+
);
|
|
1450
|
+
const denyStr = config.network.deniedDomains.join(', ') || '(none)';
|
|
1451
|
+
lines.push(
|
|
1452
|
+
row(
|
|
1453
|
+
` ${dim('•')} ${muted('Denied:')} ${text(truncateToWidth(denyStr, Math.max(10, innerW - 14)))}`,
|
|
1454
|
+
),
|
|
1455
|
+
);
|
|
1456
|
+
if (sessionAllowedDomains.length > 0) {
|
|
1457
|
+
lines.push(
|
|
1458
|
+
row(
|
|
1459
|
+
` ${dim('•')} ${muted('Session:')} ${theme.fg('accent', sessionAllowedDomains.join(', '))}`,
|
|
1460
|
+
),
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Filesystem section
|
|
1465
|
+
lines.push(row(''));
|
|
1466
|
+
lines.push(row(`${'─'.repeat(innerW)}`));
|
|
1467
|
+
lines.push(row(` ${accent('Filesystem')}`));
|
|
1468
|
+
const denyReadStr = config.filesystem.denyRead.join(', ') || '(none)';
|
|
1469
|
+
lines.push(
|
|
1470
|
+
row(
|
|
1471
|
+
` ${dim('•')} ${muted('Deny read:')} ${text(truncateToWidth(denyReadStr, Math.max(10, innerW - 16)))}`,
|
|
1472
|
+
),
|
|
1473
|
+
);
|
|
1474
|
+
const allowReadStr = config.filesystem.allowRead.join(', ') || '(none)';
|
|
1475
|
+
lines.push(
|
|
1476
|
+
row(
|
|
1477
|
+
` ${dim('•')} ${muted('Allow read:')} ${text(truncateToWidth(allowReadStr, Math.max(10, innerW - 17)))}`,
|
|
1478
|
+
),
|
|
1479
|
+
);
|
|
1480
|
+
const allowWriteStr = config.filesystem.allowWrite.join(', ') || '(none)';
|
|
1481
|
+
lines.push(
|
|
1482
|
+
row(
|
|
1483
|
+
` ${dim('•')} ${muted('Allow write:')} ${text(truncateToWidth(allowWriteStr, Math.max(10, innerW - 18)))}`,
|
|
1484
|
+
),
|
|
1485
|
+
);
|
|
1486
|
+
const denyWriteStr = config.filesystem.denyWrite.join(', ') || '(none)';
|
|
1487
|
+
lines.push(
|
|
1488
|
+
row(
|
|
1489
|
+
` ${dim('•')} ${muted('Deny write:')} ${text(truncateToWidth(denyWriteStr, Math.max(10, innerW - 17)))}`,
|
|
1490
|
+
),
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
// Session allowances
|
|
1494
|
+
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
1495
|
+
lines.push(row(''));
|
|
1496
|
+
if (sessionAllowedReadPaths.length > 0) {
|
|
1497
|
+
lines.push(
|
|
1498
|
+
row(
|
|
1499
|
+
` ${dim('•')} ${muted('Session read:')} ${theme.fg('accent', sessionAllowedReadPaths.join(', '))}`,
|
|
1500
|
+
),
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
if (sessionAllowedWritePaths.length > 0) {
|
|
1504
|
+
lines.push(
|
|
1505
|
+
row(
|
|
1506
|
+
` ${dim('•')} ${muted('Session write:')} ${theme.fg('accent', sessionAllowedWritePaths.join(', '))}`,
|
|
1507
|
+
),
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Footer
|
|
1513
|
+
lines.push(row(''));
|
|
1514
|
+
lines.push(row(` ${dim('esc')} ${muted('or any key to close')}`));
|
|
1515
|
+
|
|
1516
|
+
// Bottom border
|
|
1517
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
1518
|
+
|
|
1519
|
+
return lines;
|
|
1520
|
+
},
|
|
1521
|
+
|
|
1522
|
+
handleInput(): void {
|
|
1523
|
+
done(undefined);
|
|
1524
|
+
},
|
|
1525
|
+
|
|
1526
|
+
invalidate(): void {},
|
|
1527
|
+
};
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
overlay: true,
|
|
1531
|
+
overlayOptions: {
|
|
1532
|
+
anchor: 'center',
|
|
1533
|
+
width: 78,
|
|
1534
|
+
margin: 2,
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
);
|
|
1287
1538
|
},
|
|
1288
1539
|
});
|
|
1289
1540
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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.10.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.10.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|