pi-landstrip 0.3.3 → 0.5.0
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 +400 -144
- 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,
|
|
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,36 +337,67 @@ 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[] {
|
|
349
367
|
const errors: LandstripErrorResponse[] = [];
|
|
350
368
|
|
|
351
|
-
for (const
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
for (const block of output.trim().split(/\n\n+/)) {
|
|
370
|
+
const fields: Record<string, string> = {};
|
|
371
|
+
|
|
372
|
+
for (const line of block.split('\n')) {
|
|
373
|
+
const colonIndex = line.indexOf(':');
|
|
374
|
+
if (colonIndex === -1) continue;
|
|
375
|
+
const key = line.slice(0, colonIndex).trim();
|
|
376
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
377
|
+
if (key.length > 0 && value.length > 0) fields[key] = value;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
fields.category &&
|
|
382
|
+
['policy', 'tool', 'platform', 'system'].includes(fields.category) &&
|
|
383
|
+
fields.message
|
|
384
|
+
) {
|
|
385
|
+
const error: LandstripErrorResponse = {
|
|
386
|
+
category: fields.category as LandstripErrorResponse['category'],
|
|
387
|
+
message: fields.message,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (fields.file) error.file = fields.file;
|
|
391
|
+
if (fields.program) error.program = fields.program;
|
|
354
392
|
|
|
355
393
|
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
typeof parsed.category === 'string' &&
|
|
359
|
-
['policy', 'tool', 'platform', 'system'].includes(parsed.category) &&
|
|
360
|
-
(parsed.type === undefined ||
|
|
361
|
-
(typeof parsed.type === 'string' &&
|
|
362
|
-
['filesystem', 'network', 'platform', 'launch', 'encoding'].includes(parsed.type))) &&
|
|
363
|
-
typeof parsed.message === 'string' &&
|
|
364
|
-
parsed.message.length > 0
|
|
394
|
+
fields.type &&
|
|
395
|
+
['filesystem', 'network', 'platform', 'launch', 'encoding'].includes(fields.type)
|
|
365
396
|
) {
|
|
366
|
-
|
|
397
|
+
error.type = fields.type as LandstripErrorResponse['type'];
|
|
367
398
|
}
|
|
368
|
-
|
|
369
|
-
|
|
399
|
+
|
|
400
|
+
errors.push(error);
|
|
370
401
|
}
|
|
371
402
|
}
|
|
372
403
|
|
|
@@ -401,99 +432,168 @@ async function showPermissionPrompt(
|
|
|
401
432
|
): Promise<PermissionChoice> {
|
|
402
433
|
if (!ctx.hasUI) return 'abort';
|
|
403
434
|
|
|
404
|
-
const result = await ctx.ui.custom<PermissionChoice>(
|
|
405
|
-
|
|
406
|
-
|
|
435
|
+
const result = await ctx.ui.custom<PermissionChoice>(
|
|
436
|
+
(tui, theme, _kb, done) => {
|
|
437
|
+
let selectedIndex = 0;
|
|
438
|
+
let pendingAction: PermissionChoice | null = null;
|
|
407
439
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
440
|
+
function resolveChoice(action: PermissionChoice): void {
|
|
441
|
+
done(action);
|
|
442
|
+
}
|
|
411
443
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
}
|
|
431
483
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
}
|
|
444
498
|
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
508
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
509
|
+
// Hint
|
|
510
|
+
let hint = '';
|
|
511
|
+
if (option.hint && !isPending) {
|
|
512
|
+
hint = ' ' + dim(option.hint);
|
|
513
|
+
}
|
|
453
514
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
}
|
|
458
520
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
544
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
545
|
+
if (matchesKey(data, Key.enter)) {
|
|
546
|
+
resolveChoice(pendingAction ?? options[selectedIndex]?.action ?? 'abort');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
472
549
|
|
|
473
|
-
|
|
474
|
-
|
|
550
|
+
if (matchesKey(data, Key.up)) {
|
|
551
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
552
|
+
pendingAction = null;
|
|
553
|
+
tui.requestRender();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
475
556
|
|
|
476
|
-
if (data
|
|
477
|
-
|
|
557
|
+
if (matchesKey(data, Key.down)) {
|
|
558
|
+
selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
|
|
559
|
+
pendingAction = null;
|
|
560
|
+
tui.requestRender();
|
|
478
561
|
return;
|
|
479
562
|
}
|
|
480
563
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
} else {
|
|
564
|
+
for (let i = 0; i < options.length; i++) {
|
|
565
|
+
const option = options[i];
|
|
566
|
+
|
|
567
|
+
if (data === option.key) {
|
|
486
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;
|
|
487
581
|
}
|
|
488
|
-
tui.requestRender();
|
|
489
|
-
return;
|
|
490
582
|
}
|
|
491
|
-
}
|
|
492
|
-
},
|
|
583
|
+
},
|
|
493
584
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
585
|
+
invalidate(): void {},
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
overlay: true,
|
|
590
|
+
overlayOptions: {
|
|
591
|
+
anchor: 'center',
|
|
592
|
+
width: 72,
|
|
593
|
+
margin: 2,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
);
|
|
497
597
|
|
|
498
598
|
return result ?? 'abort';
|
|
499
599
|
}
|
|
@@ -911,8 +1011,11 @@ export function createLandstripIntegration(
|
|
|
911
1011
|
}
|
|
912
1012
|
|
|
913
1013
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1014
|
+
let stderrAcc = '';
|
|
1015
|
+
|
|
914
1016
|
child.stdout?.on('data', onData);
|
|
915
1017
|
child.stderr?.on('data', (data: Buffer) => {
|
|
1018
|
+
stderrAcc += data.toString('utf8');
|
|
916
1019
|
onStderr(data);
|
|
917
1020
|
onData(data);
|
|
918
1021
|
});
|
|
@@ -922,15 +1025,37 @@ export function createLandstripIntegration(
|
|
|
922
1025
|
reject(error);
|
|
923
1026
|
});
|
|
924
1027
|
|
|
925
|
-
child.on('close', (code) => {
|
|
1028
|
+
child.on('close', async (code) => {
|
|
926
1029
|
cleanup();
|
|
927
1030
|
if (signal?.aborted) {
|
|
928
1031
|
reject(new Error('aborted'));
|
|
929
|
-
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (timedOut) {
|
|
930
1035
|
reject(new Error(`timeout:${timeout}`));
|
|
931
|
-
|
|
932
|
-
resolvePromise({ exitCode: code });
|
|
1036
|
+
return;
|
|
933
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
const blockedPath = extractBlockedPath(stderrAcc, cwd);
|
|
1040
|
+
if (blockedPath && ctx.hasUI) {
|
|
1041
|
+
const config = loadConfig(cwd);
|
|
1042
|
+
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(cwd));
|
|
1043
|
+
const isWriteAllowed = !shouldPromptForWrite(
|
|
1044
|
+
blockedPath,
|
|
1045
|
+
getEffectiveAllowWrite(cwd),
|
|
1046
|
+
matchesPattern,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
if (!isReadAllowed) {
|
|
1050
|
+
const choice = await promptReadBlock(ctx, blockedPath);
|
|
1051
|
+
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1052
|
+
} else if (!isWriteAllowed) {
|
|
1053
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1054
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
resolvePromise({ exitCode: code });
|
|
934
1059
|
});
|
|
935
1060
|
});
|
|
936
1061
|
},
|
|
@@ -1024,18 +1149,28 @@ export function createLandstripIntegration(
|
|
|
1024
1149
|
}
|
|
1025
1150
|
|
|
1026
1151
|
function enableStatus(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1152
|
+
const theme = ctx.ui.theme;
|
|
1153
|
+
const dot = theme.fg('success', '●');
|
|
1154
|
+
const label = theme.fg('text', 'Sandbox');
|
|
1155
|
+
|
|
1156
|
+
let networkLabel: string;
|
|
1157
|
+
let networkColor: 'warning' | 'accent';
|
|
1158
|
+
if (config.network.allowNetwork) {
|
|
1159
|
+
networkLabel = 'unrestricted';
|
|
1160
|
+
networkColor = 'warning';
|
|
1161
|
+
} else if (allowsAllDomains(config.network.allowedDomains)) {
|
|
1162
|
+
networkLabel = 'any domain';
|
|
1163
|
+
networkColor = 'warning';
|
|
1164
|
+
} else {
|
|
1165
|
+
networkLabel = `${config.network.allowedDomains.length} domains`;
|
|
1166
|
+
networkColor = 'accent';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const sep = theme.fg('dim', '·');
|
|
1170
|
+
const net = theme.fg(networkColor, networkLabel);
|
|
1171
|
+
const write = theme.fg('accent', `${config.filesystem.allowWrite.length} write paths`);
|
|
1172
|
+
|
|
1173
|
+
ctx.ui.setStatus('sandbox', `${dot} ${label} ${sep} ${net} ${sep} ${write}`);
|
|
1039
1174
|
}
|
|
1040
1175
|
|
|
1041
1176
|
function enableSandbox(ctx: ExtensionContext): boolean {
|
|
@@ -1062,7 +1197,7 @@ export function createLandstripIntegration(
|
|
|
1062
1197
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1063
1198
|
sandboxEnabled = false;
|
|
1064
1199
|
sandboxReady = false;
|
|
1065
|
-
ctx.ui.notify(`landstrip 0.
|
|
1200
|
+
ctx.ui.notify(`landstrip 0.10.2 or newer is required; found: ${version}`, 'error');
|
|
1066
1201
|
return false;
|
|
1067
1202
|
}
|
|
1068
1203
|
|
|
@@ -1228,7 +1363,7 @@ export function createLandstripIntegration(
|
|
|
1228
1363
|
|
|
1229
1364
|
sandboxEnabled = false;
|
|
1230
1365
|
sandboxReady = false;
|
|
1231
|
-
ctx.ui.setStatus('sandbox',
|
|
1366
|
+
ctx.ui.setStatus('sandbox', undefined);
|
|
1232
1367
|
ctx.ui.notify('Sandbox disabled', 'info');
|
|
1233
1368
|
},
|
|
1234
1369
|
});
|
|
@@ -1243,34 +1378,155 @@ export function createLandstripIntegration(
|
|
|
1243
1378
|
|
|
1244
1379
|
const config = loadConfig(ctx.cwd);
|
|
1245
1380
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1381
|
+
|
|
1382
|
+
await ctx.ui.custom(
|
|
1383
|
+
(tui, theme, _kb, done) => {
|
|
1384
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
1385
|
+
const muted = (s: string) => theme.fg('muted', s);
|
|
1386
|
+
const accent = (s: string) => theme.fg('accent', s);
|
|
1387
|
+
const text = (s: string) => theme.fg('text', s);
|
|
1388
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
1389
|
+
|
|
1390
|
+
function boolVal(v: boolean): string {
|
|
1391
|
+
return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function makeRow(content: string, innerW: number, border: string): string {
|
|
1395
|
+
const line = truncateToWidth(content, innerW);
|
|
1396
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
1397
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return {
|
|
1401
|
+
render(width: number): string[] {
|
|
1402
|
+
const innerW = width - 4;
|
|
1403
|
+
const border = borderFg('│');
|
|
1404
|
+
const row = (c: string) => makeRow(c, innerW, border);
|
|
1405
|
+
const lines: string[] = [];
|
|
1406
|
+
|
|
1407
|
+
// Top border
|
|
1408
|
+
const title = accent(' Sandbox Configuration ');
|
|
1409
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
1410
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
1411
|
+
|
|
1412
|
+
// Status
|
|
1413
|
+
const statusDot = theme.fg('success', '●');
|
|
1414
|
+
const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 27)));
|
|
1415
|
+
lines.push(
|
|
1416
|
+
row(
|
|
1417
|
+
` ${statusDot} ${text('Active')} ${dim('·')} ${muted('landstrip:')} ${pathSnippet}`,
|
|
1418
|
+
),
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
// Config files
|
|
1422
|
+
lines.push(row(` ${dim('Config files:')}`));
|
|
1423
|
+
lines.push(row(` ${dim('project')} ${text(projectPath)}`));
|
|
1424
|
+
lines.push(row(` ${dim('global')} ${text(globalPath)}`));
|
|
1425
|
+
|
|
1426
|
+
// Network section
|
|
1427
|
+
lines.push(row(''));
|
|
1428
|
+
lines.push(row(`${'─'.repeat(innerW)}`));
|
|
1429
|
+
const netMode = config.network.allowNetwork ? ' (unrestricted)' : ' (proxied)';
|
|
1430
|
+
lines.push(row(` ${accent('Network')}${dim(netMode)}`));
|
|
1431
|
+
lines.push(
|
|
1432
|
+
row(
|
|
1433
|
+
` ${dim('•')} ${muted('Allow network:')} ${boolVal(config.network.allowNetwork)}`,
|
|
1434
|
+
),
|
|
1435
|
+
);
|
|
1436
|
+
const domainsStr = config.network.allowedDomains.join(', ') || '(none)';
|
|
1437
|
+
lines.push(
|
|
1438
|
+
row(
|
|
1439
|
+
` ${dim('•')} ${muted('Allowed:')} ${text(truncateToWidth(domainsStr, Math.max(10, innerW - 15)))}`,
|
|
1440
|
+
),
|
|
1441
|
+
);
|
|
1442
|
+
const denyStr = config.network.deniedDomains.join(', ') || '(none)';
|
|
1443
|
+
lines.push(
|
|
1444
|
+
row(
|
|
1445
|
+
` ${dim('•')} ${muted('Denied:')} ${text(truncateToWidth(denyStr, Math.max(10, innerW - 14)))}`,
|
|
1446
|
+
),
|
|
1447
|
+
);
|
|
1448
|
+
if (sessionAllowedDomains.length > 0) {
|
|
1449
|
+
lines.push(
|
|
1450
|
+
row(
|
|
1451
|
+
` ${dim('•')} ${muted('Session:')} ${theme.fg('accent', sessionAllowedDomains.join(', '))}`,
|
|
1452
|
+
),
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Filesystem section
|
|
1457
|
+
lines.push(row(''));
|
|
1458
|
+
lines.push(row(`${'─'.repeat(innerW)}`));
|
|
1459
|
+
lines.push(row(` ${accent('Filesystem')}`));
|
|
1460
|
+
const denyReadStr = config.filesystem.denyRead.join(', ') || '(none)';
|
|
1461
|
+
lines.push(
|
|
1462
|
+
row(
|
|
1463
|
+
` ${dim('•')} ${muted('Deny read:')} ${text(truncateToWidth(denyReadStr, Math.max(10, innerW - 16)))}`,
|
|
1464
|
+
),
|
|
1465
|
+
);
|
|
1466
|
+
const allowReadStr = config.filesystem.allowRead.join(', ') || '(none)';
|
|
1467
|
+
lines.push(
|
|
1468
|
+
row(
|
|
1469
|
+
` ${dim('•')} ${muted('Allow read:')} ${text(truncateToWidth(allowReadStr, Math.max(10, innerW - 17)))}`,
|
|
1470
|
+
),
|
|
1471
|
+
);
|
|
1472
|
+
const allowWriteStr = config.filesystem.allowWrite.join(', ') || '(none)';
|
|
1473
|
+
lines.push(
|
|
1474
|
+
row(
|
|
1475
|
+
` ${dim('•')} ${muted('Allow write:')} ${text(truncateToWidth(allowWriteStr, Math.max(10, innerW - 18)))}`,
|
|
1476
|
+
),
|
|
1477
|
+
);
|
|
1478
|
+
const denyWriteStr = config.filesystem.denyWrite.join(', ') || '(none)';
|
|
1479
|
+
lines.push(
|
|
1480
|
+
row(
|
|
1481
|
+
` ${dim('•')} ${muted('Deny write:')} ${text(truncateToWidth(denyWriteStr, Math.max(10, innerW - 17)))}`,
|
|
1482
|
+
),
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
// Session allowances
|
|
1486
|
+
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
1487
|
+
lines.push(row(''));
|
|
1488
|
+
if (sessionAllowedReadPaths.length > 0) {
|
|
1489
|
+
lines.push(
|
|
1490
|
+
row(
|
|
1491
|
+
` ${dim('•')} ${muted('Session read:')} ${theme.fg('accent', sessionAllowedReadPaths.join(', '))}`,
|
|
1492
|
+
),
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (sessionAllowedWritePaths.length > 0) {
|
|
1496
|
+
lines.push(
|
|
1497
|
+
row(
|
|
1498
|
+
` ${dim('•')} ${muted('Session write:')} ${theme.fg('accent', sessionAllowedWritePaths.join(', '))}`,
|
|
1499
|
+
),
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Footer
|
|
1505
|
+
lines.push(row(''));
|
|
1506
|
+
lines.push(row(` ${dim('esc')} ${muted('or any key to close')}`));
|
|
1507
|
+
|
|
1508
|
+
// Bottom border
|
|
1509
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
1510
|
+
|
|
1511
|
+
return lines;
|
|
1512
|
+
},
|
|
1513
|
+
|
|
1514
|
+
handleInput(): void {
|
|
1515
|
+
done(undefined);
|
|
1516
|
+
},
|
|
1517
|
+
|
|
1518
|
+
invalidate(): void {},
|
|
1519
|
+
};
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
overlay: true,
|
|
1523
|
+
overlayOptions: {
|
|
1524
|
+
anchor: 'center',
|
|
1525
|
+
width: 78,
|
|
1526
|
+
margin: 2,
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
);
|
|
1274
1530
|
},
|
|
1275
1531
|
});
|
|
1276
1532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.10.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|