pi-landstrip 0.15.6 → 0.15.8
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 +154 -188
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
|
-
/// <reference path="./landstrip.d.ts" />
|
|
5
|
-
|
|
6
4
|
import type {
|
|
7
5
|
AgentToolResult,
|
|
8
6
|
AgentToolUpdateCallback,
|
|
@@ -10,6 +8,7 @@ import type {
|
|
|
10
8
|
BashToolInput,
|
|
11
9
|
ExtensionAPI,
|
|
12
10
|
ExtensionContext,
|
|
11
|
+
Theme,
|
|
13
12
|
} from '@earendil-works/pi-coding-agent';
|
|
14
13
|
|
|
15
14
|
import { binaryPath } from '@landstrip/landstrip';
|
|
@@ -69,6 +68,15 @@ interface SandboxConfig {
|
|
|
69
68
|
filesystem: SandboxFilesystemConfig;
|
|
70
69
|
}
|
|
71
70
|
|
|
71
|
+
type SandboxFilesystemConfigFile = Partial<SandboxFilesystemConfig>;
|
|
72
|
+
type SandboxNetworkConfigFile = Partial<SandboxNetworkConfig>;
|
|
73
|
+
|
|
74
|
+
interface SandboxConfigFile {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
network?: SandboxNetworkConfigFile;
|
|
77
|
+
filesystem?: SandboxFilesystemConfigFile;
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
type SandboxConfigScope = 'global' | 'project';
|
|
73
81
|
|
|
74
82
|
interface LandstripPolicy {
|
|
@@ -95,6 +103,7 @@ type LandstripTrap =
|
|
|
95
103
|
interface LandstripBashCallbacks {
|
|
96
104
|
onStderr?: (data: Buffer) => void;
|
|
97
105
|
onErrorFd?: (data: Buffer) => void;
|
|
106
|
+
promptOnBlock?: boolean;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
const LANDSTRIP_VERSION = [0, 15, 9] as const;
|
|
@@ -153,8 +162,8 @@ function loadConfig(cwd: string): SandboxConfig {
|
|
|
153
162
|
const projectConfigPath = join(cwd, '.pi', 'sandbox.json');
|
|
154
163
|
const globalConfigPath = join(getAgentDir(), 'sandbox.json');
|
|
155
164
|
|
|
156
|
-
let globalConfig:
|
|
157
|
-
let projectConfig:
|
|
165
|
+
let globalConfig: SandboxConfigFile = {};
|
|
166
|
+
let projectConfig: SandboxConfigFile = {};
|
|
158
167
|
|
|
159
168
|
if (existsSync(globalConfigPath)) {
|
|
160
169
|
try {
|
|
@@ -180,7 +189,7 @@ function mergeArray(base: string[], override?: string[]): string[] {
|
|
|
180
189
|
return [...new Set([...base, ...override])];
|
|
181
190
|
}
|
|
182
191
|
|
|
183
|
-
function deepMerge(base: SandboxConfig, overrides:
|
|
192
|
+
function deepMerge(base: SandboxConfig, overrides: SandboxConfigFile): SandboxConfig {
|
|
184
193
|
const network = overrides.network;
|
|
185
194
|
const filesystem = overrides.filesystem;
|
|
186
195
|
|
|
@@ -210,7 +219,7 @@ function getConfigPaths(cwd: string): { globalPath: string; projectPath: string
|
|
|
210
219
|
};
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
function readOrEmptyConfig(configPath: string):
|
|
222
|
+
function readOrEmptyConfig(configPath: string): SandboxConfigFile {
|
|
214
223
|
if (!existsSync(configPath)) return {};
|
|
215
224
|
try {
|
|
216
225
|
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
@@ -219,7 +228,7 @@ function readOrEmptyConfig(configPath: string): Partial<SandboxConfig> {
|
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
function writeConfigFile(configPath: string, config:
|
|
231
|
+
function writeConfigFile(configPath: string, config: SandboxConfigFile): void {
|
|
223
232
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
224
233
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
225
234
|
}
|
|
@@ -253,7 +262,7 @@ async function addDomainToConfig(configPath: string, domain: string): Promise<vo
|
|
|
253
262
|
...config.network,
|
|
254
263
|
allowedDomains: [...existing, domain],
|
|
255
264
|
deniedDomains: config.network?.deniedDomains ?? [],
|
|
256
|
-
}
|
|
265
|
+
};
|
|
257
266
|
writeConfigFile(configPath, config);
|
|
258
267
|
});
|
|
259
268
|
}
|
|
@@ -267,7 +276,7 @@ async function addReadPathToConfig(configPath: string, pathToAdd: string): Promi
|
|
|
267
276
|
config.filesystem = {
|
|
268
277
|
...config.filesystem,
|
|
269
278
|
allowRead: [...existing, pathToAdd],
|
|
270
|
-
}
|
|
279
|
+
};
|
|
271
280
|
writeConfigFile(configPath, config);
|
|
272
281
|
});
|
|
273
282
|
}
|
|
@@ -281,7 +290,7 @@ async function addWritePathToConfig(configPath: string, pathToAdd: string): Prom
|
|
|
281
290
|
config.filesystem = {
|
|
282
291
|
...config.filesystem,
|
|
283
292
|
allowWrite: [...existing, pathToAdd],
|
|
284
|
-
}
|
|
293
|
+
};
|
|
285
294
|
writeConfigFile(configPath, config);
|
|
286
295
|
});
|
|
287
296
|
}
|
|
@@ -362,7 +371,9 @@ function matchesPattern(filePath: string, patterns: string[]): boolean {
|
|
|
362
371
|
const absPattern = pattern.includes('*') ? expandPath(pattern) : canonicalizePath(pattern);
|
|
363
372
|
|
|
364
373
|
if (pattern.includes('*')) {
|
|
365
|
-
const escaped = absPattern
|
|
374
|
+
const escaped = absPattern
|
|
375
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
376
|
+
.replace(/\*\*\/|\*\*|\*/g, (token) => (token === '**/' ? '(?:.*/)?' : '.*'));
|
|
366
377
|
return new RegExp(`^${escaped}$`).test(abs);
|
|
367
378
|
}
|
|
368
379
|
|
|
@@ -438,11 +449,6 @@ function extractNativeWriteDeniedPath(output: string, cwd: string): string | nul
|
|
|
438
449
|
);
|
|
439
450
|
if (match) return normalizePathMatch(match[1], cwd);
|
|
440
451
|
|
|
441
|
-
match = output.match(
|
|
442
|
-
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
443
|
-
);
|
|
444
|
-
if (match) return normalizePathMatch(match[1], cwd);
|
|
445
|
-
|
|
446
452
|
match = output.match(
|
|
447
453
|
/^[a-zA-Z0-9_-]+: cannot create(?: directory)? '?([^'\n]+?)'?(?: for writing)?: (?:Operation not permitted|Permission denied)$/m,
|
|
448
454
|
);
|
|
@@ -580,6 +586,25 @@ function setTuiStatus(ctx: ExtensionContext, key: string, value: string | undefi
|
|
|
580
586
|
ctx.ui.setStatus(key, value);
|
|
581
587
|
}
|
|
582
588
|
|
|
589
|
+
function boxTop(theme: Theme, width: number, title: string): string {
|
|
590
|
+
const label = theme.fg('accent', ` ${title} `);
|
|
591
|
+
const fill = theme.fg('border', '─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
592
|
+
return `${theme.fg('border', '╭─')}${label}${fill}${theme.fg('border', '─╮')}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function boxRow(theme: Theme, width: number, content = ''): string {
|
|
596
|
+
const innerW = Math.max(1, width - 4);
|
|
597
|
+
const border = theme.fg('border', '│');
|
|
598
|
+
const line = truncateToWidth(content, innerW);
|
|
599
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
600
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function boxBottom(theme: Theme, width: number): string {
|
|
604
|
+
const border = (s: string) => theme.fg('border', s);
|
|
605
|
+
return `${border('╰')}${border('─'.repeat(Math.max(0, width - 2)))}${border('╯')}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
583
608
|
async function showPermissionPrompt(
|
|
584
609
|
ctx: ExtensionContext,
|
|
585
610
|
title: string,
|
|
@@ -598,28 +623,14 @@ async function showPermissionPrompt(
|
|
|
598
623
|
|
|
599
624
|
return {
|
|
600
625
|
render(width: number): string[] {
|
|
601
|
-
const innerW = width - 4;
|
|
626
|
+
const innerW = Math.max(1, width - 4);
|
|
602
627
|
const lines: string[] = [];
|
|
603
|
-
const border = theme.fg('border', '│');
|
|
604
628
|
const dim = (s: string) => theme.fg('dim', s);
|
|
605
|
-
const borderFg = (s: string) => theme.fg('border', s);
|
|
606
|
-
|
|
607
|
-
// Top border with title
|
|
608
|
-
const label = ' Sandbox ';
|
|
609
|
-
const topLeft = borderFg('╭─');
|
|
610
|
-
const topRight = borderFg('─╮');
|
|
611
|
-
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(label))));
|
|
612
|
-
lines.push(`${topLeft}${theme.fg('accent', label)}${topFill}${topRight}`);
|
|
613
629
|
|
|
614
|
-
|
|
615
|
-
lines.push(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const titleText = truncateToWidth(theme.fg('warning', title), innerW);
|
|
619
|
-
const titlePad = Math.max(0, innerW - visibleWidth(titleText));
|
|
620
|
-
lines.push(`${border} ${titleText}${' '.repeat(titlePad)} ${border}`);
|
|
621
|
-
|
|
622
|
-
lines.push(`${border} ${' '.repeat(innerW)} ${border}`);
|
|
630
|
+
lines.push(boxTop(theme, width, 'Sandbox'));
|
|
631
|
+
lines.push(boxRow(theme, width));
|
|
632
|
+
lines.push(boxRow(theme, width, theme.fg('warning', title)));
|
|
633
|
+
lines.push(boxRow(theme, width));
|
|
623
634
|
|
|
624
635
|
// Options
|
|
625
636
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -629,11 +640,11 @@ async function showPermissionPrompt(
|
|
|
629
640
|
|
|
630
641
|
// Section divider before the permanent options (index 2 and 3)
|
|
631
642
|
if (i === 2) {
|
|
632
|
-
lines.push(
|
|
643
|
+
lines.push(boxRow(theme, width));
|
|
633
644
|
const secLabel = ' Permanent ';
|
|
634
645
|
const secDash = '─'.repeat(Math.max(0, innerW - visibleWidth(secLabel)));
|
|
635
|
-
lines.push(
|
|
636
|
-
lines.push(
|
|
646
|
+
lines.push(boxRow(theme, width, dim(secDash + secLabel)));
|
|
647
|
+
lines.push(boxRow(theme, width));
|
|
637
648
|
}
|
|
638
649
|
|
|
639
650
|
// Key badge
|
|
@@ -668,25 +679,16 @@ async function showPermissionPrompt(
|
|
|
668
679
|
}
|
|
669
680
|
|
|
670
681
|
const fullLine = ` ${cursor} ${keyBadge} ${label}${hint}`;
|
|
671
|
-
|
|
672
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
673
|
-
lines.push(`${border} ${line}${' '.repeat(pad)} ${border}`);
|
|
682
|
+
lines.push(boxRow(theme, width, fullLine));
|
|
674
683
|
}
|
|
675
684
|
|
|
676
685
|
// Footer
|
|
677
|
-
lines.push(
|
|
686
|
+
lines.push(boxRow(theme, width));
|
|
678
687
|
const footerText = pendingAction
|
|
679
688
|
? '↑↓ navigate enter confirm esc cancel'
|
|
680
689
|
: '↑↓ navigate enter select esc dismiss';
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
lines.push(`${border} ${footerLine}${' '.repeat(footerPad)} ${border}`);
|
|
684
|
-
|
|
685
|
-
// Bottom border
|
|
686
|
-
const botLeft = borderFg('╰');
|
|
687
|
-
const botRight = borderFg('╯');
|
|
688
|
-
const botFill = borderFg('─'.repeat(width - 2));
|
|
689
|
-
lines.push(`${botLeft}${botFill}${botRight}`);
|
|
690
|
+
lines.push(boxRow(theme, width, dim(footerText)));
|
|
691
|
+
lines.push(boxBottom(theme, width));
|
|
690
692
|
|
|
691
693
|
return lines;
|
|
692
694
|
},
|
|
@@ -821,21 +823,25 @@ function proxyEnv(env: NodeJS.ProcessEnv | undefined, port: number): NodeJS.Proc
|
|
|
821
823
|
};
|
|
822
824
|
}
|
|
823
825
|
|
|
826
|
+
function parseProxyPort(value: string | undefined, defaultPort: number): number | null {
|
|
827
|
+
const rawPort = value ?? String(defaultPort);
|
|
828
|
+
if (!/^\d+$/.test(rawPort)) return null;
|
|
829
|
+
|
|
830
|
+
const port = Number(rawPort);
|
|
831
|
+
return port >= 1 && port <= 65535 ? port : null;
|
|
832
|
+
}
|
|
833
|
+
|
|
824
834
|
function splitHostPort(target: string, defaultPort: number): { host: string; port: number } | null {
|
|
825
|
-
const bracketMatch = target.match(/^\[([^\]]+)\](?::(
|
|
835
|
+
const bracketMatch = target.match(/^\[([^\]]+)\](?::(.*))?$/);
|
|
826
836
|
if (bracketMatch) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
port: bracketMatch[2] ? Number(bracketMatch[2]) : defaultPort,
|
|
830
|
-
};
|
|
837
|
+
const port = parseProxyPort(bracketMatch[2], defaultPort);
|
|
838
|
+
return port === null ? null : { host: bracketMatch[1], port };
|
|
831
839
|
}
|
|
832
840
|
|
|
833
841
|
const lastColon = target.lastIndexOf(':');
|
|
834
842
|
if (lastColon > -1 && target.indexOf(':') === lastColon) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
port: Number(target.slice(lastColon + 1)),
|
|
838
|
-
};
|
|
843
|
+
const port = parseProxyPort(target.slice(lastColon + 1), defaultPort);
|
|
844
|
+
return port === null ? null : { host: target.slice(0, lastColon), port };
|
|
839
845
|
}
|
|
840
846
|
|
|
841
847
|
return { host: target, port: defaultPort };
|
|
@@ -858,20 +864,28 @@ function pipeSockets(client: Socket, upstream: Socket, initialData?: Buffer): vo
|
|
|
858
864
|
|
|
859
865
|
type LandstripBashTool = ReturnType<typeof createBashToolDefinition>;
|
|
860
866
|
|
|
867
|
+
/** Options for creating a landstrip sandbox integration. */
|
|
861
868
|
export interface LandstripIntegrationOptions {
|
|
869
|
+
/** Register a sandboxed bash tool when the integration is registered. */
|
|
862
870
|
registerBashTool?: boolean;
|
|
871
|
+
/** Working directory used when registering the default bash tool. */
|
|
863
872
|
cwd?: string;
|
|
864
873
|
}
|
|
865
874
|
|
|
875
|
+
/** Landstrip sandbox integration hooks for Pi. */
|
|
866
876
|
export interface LandstripIntegration {
|
|
877
|
+
/** Create a bash tool definition that runs commands through landstrip when enabled. */
|
|
867
878
|
createBashTool(cwd: string, ctx?: ExtensionContext): LandstripBashTool;
|
|
879
|
+
/** Register the integration's tools, events, flags, and commands with Pi. */
|
|
868
880
|
register(pi: ExtensionAPI): void;
|
|
869
881
|
}
|
|
870
882
|
|
|
883
|
+
/** Register the landstrip extension with Pi. */
|
|
871
884
|
export default function (pi: ExtensionAPI) {
|
|
872
885
|
createLandstripIntegration().register(pi);
|
|
873
886
|
}
|
|
874
887
|
|
|
888
|
+
/** Create a landstrip integration for registration or custom embedding. */
|
|
875
889
|
export function createLandstripIntegration(
|
|
876
890
|
options: LandstripIntegrationOptions = {},
|
|
877
891
|
): LandstripIntegration {
|
|
@@ -896,18 +910,15 @@ export function createLandstripIntegration(
|
|
|
896
910
|
sessionAllowedWritePaths.length = 0;
|
|
897
911
|
}
|
|
898
912
|
|
|
899
|
-
function getEffectiveAllowedDomains(
|
|
900
|
-
const config = loadConfig(cwd);
|
|
913
|
+
function getEffectiveAllowedDomains(config: SandboxConfig): string[] {
|
|
901
914
|
return [...config.network.allowedDomains, ...sessionAllowedDomains];
|
|
902
915
|
}
|
|
903
916
|
|
|
904
|
-
function getEffectiveAllowRead(
|
|
905
|
-
const config = loadConfig(cwd);
|
|
917
|
+
function getEffectiveAllowRead(config: SandboxConfig): string[] {
|
|
906
918
|
return [...config.filesystem.allowRead, ...sessionAllowedReadPaths];
|
|
907
919
|
}
|
|
908
920
|
|
|
909
|
-
function getEffectiveAllowWrite(
|
|
910
|
-
const config = loadConfig(cwd);
|
|
921
|
+
function getEffectiveAllowWrite(config: SandboxConfig): string[] {
|
|
911
922
|
return [...config.filesystem.allowWrite, ...sessionAllowedWritePaths];
|
|
912
923
|
}
|
|
913
924
|
|
|
@@ -952,7 +963,7 @@ export function createLandstripIntegration(
|
|
|
952
963
|
const config = loadConfig(cwd);
|
|
953
964
|
|
|
954
965
|
if (domainMatchesAny(domain, config.network.deniedDomains)) return false;
|
|
955
|
-
if (domainMatchesAny(domain, getEffectiveAllowedDomains(
|
|
966
|
+
if (domainMatchesAny(domain, getEffectiveAllowedDomains(config))) return true;
|
|
956
967
|
|
|
957
968
|
const choice = await promptDomainBlock(ctx, domain);
|
|
958
969
|
if (choice === 'abort') return false;
|
|
@@ -974,8 +985,8 @@ export function createLandstripIntegration(
|
|
|
974
985
|
},
|
|
975
986
|
filesystem: {
|
|
976
987
|
denyRead: config.filesystem.denyRead,
|
|
977
|
-
allowRead: getEffectiveAllowRead(
|
|
978
|
-
allowWrite: getEffectiveAllowWrite(
|
|
988
|
+
allowRead: getEffectiveAllowRead(config),
|
|
989
|
+
allowWrite: getEffectiveAllowWrite(config),
|
|
979
990
|
denyWrite: config.filesystem.denyWrite,
|
|
980
991
|
},
|
|
981
992
|
};
|
|
@@ -1045,7 +1056,13 @@ export function createLandstripIntegration(
|
|
|
1045
1056
|
denyProxyRequest(client, '400 Bad Request');
|
|
1046
1057
|
return;
|
|
1047
1058
|
}
|
|
1048
|
-
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
url = new URL(`http://${host}${rawTarget}`);
|
|
1062
|
+
} catch {
|
|
1063
|
+
denyProxyRequest(client, '400 Bad Request');
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1049
1066
|
}
|
|
1050
1067
|
|
|
1051
1068
|
if (!(await ensureDomainAllowed(ctx, url.hostname, cwd))) {
|
|
@@ -1053,7 +1070,12 @@ export function createLandstripIntegration(
|
|
|
1053
1070
|
return;
|
|
1054
1071
|
}
|
|
1055
1072
|
|
|
1056
|
-
const
|
|
1073
|
+
const defaultPort = url.protocol === 'https:' ? 443 : 80;
|
|
1074
|
+
const port = parseProxyPort(url.port || undefined, defaultPort);
|
|
1075
|
+
if (port === null) {
|
|
1076
|
+
denyProxyRequest(client, '400 Bad Request');
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1057
1079
|
const path = `${url.pathname}${url.search}` || '/';
|
|
1058
1080
|
lines[0] = `${method} ${path} ${version}`;
|
|
1059
1081
|
|
|
@@ -1219,7 +1241,6 @@ export function createLandstripIntegration(
|
|
|
1219
1241
|
}
|
|
1220
1242
|
|
|
1221
1243
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1222
|
-
const resolvedQueryIds = new Set<number>();
|
|
1223
1244
|
let stderrAcc = '';
|
|
1224
1245
|
let errorFdAcc = '';
|
|
1225
1246
|
|
|
@@ -1232,109 +1253,70 @@ export function createLandstripIntegration(
|
|
|
1232
1253
|
trapSocket.on('data', (data: Buffer) => {
|
|
1233
1254
|
errorFdAcc += data.toString('utf8');
|
|
1234
1255
|
callbacks.onErrorFd?.(data);
|
|
1235
|
-
// Process query traps in real-time.
|
|
1236
|
-
if (ctx.hasUI) {
|
|
1237
|
-
const traps = parseLandstripTraps(errorFdAcc);
|
|
1238
|
-
for (const trap of traps) {
|
|
1239
|
-
if (
|
|
1240
|
-
trap.kind === 'filesystem' &&
|
|
1241
|
-
(trap.operation === 'write' || trap.operation === 'read') &&
|
|
1242
|
-
'state' in trap &&
|
|
1243
|
-
(trap as any).state === 'query' &&
|
|
1244
|
-
'query_id' in trap
|
|
1245
|
-
) {
|
|
1246
|
-
const queryId = (trap as any).query_id as number;
|
|
1247
|
-
if (!resolvedQueryIds.has(queryId)) {
|
|
1248
|
-
resolvedQueryIds.add(queryId);
|
|
1249
|
-
handleFsQuery(trapSocket, queryId, trap.operation, trap.file, cwd, ctx).catch(
|
|
1250
|
-
() => {},
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
1256
|
});
|
|
1257
1257
|
|
|
1258
|
-
async function handleFsQuery(
|
|
1259
|
-
socket: NetSocket,
|
|
1260
|
-
queryId: number,
|
|
1261
|
-
operation: 'read' | 'write',
|
|
1262
|
-
file: string,
|
|
1263
|
-
cwd: string,
|
|
1264
|
-
ctx: ExtensionContext,
|
|
1265
|
-
): Promise<void> {
|
|
1266
|
-
const choice =
|
|
1267
|
-
operation === 'read'
|
|
1268
|
-
? await promptReadBlock(ctx, file)
|
|
1269
|
-
: await promptWriteBlock(ctx, file);
|
|
1270
|
-
if (choice !== 'abort') {
|
|
1271
|
-
if (operation === 'read') await applyReadChoice(choice, file, cwd);
|
|
1272
|
-
else await applyWriteChoice(choice, file, cwd);
|
|
1273
|
-
}
|
|
1274
|
-
const action = choice === 'abort' ? 'deny' : 'allow';
|
|
1275
|
-
const response = JSON.stringify({ query_id: queryId, action }) + '\n';
|
|
1276
|
-
if (!socket.destroyed) {
|
|
1277
|
-
socket.write(response);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1258
|
child.on('error', (error) => {
|
|
1282
1259
|
cleanup();
|
|
1283
1260
|
reject(error);
|
|
1284
1261
|
});
|
|
1285
1262
|
|
|
1286
|
-
child.on('close',
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const errorOutput = errorFdAcc || stderrAcc;
|
|
1298
|
-
|
|
1299
|
-
const blockedPath =
|
|
1300
|
-
extractBlockedPath(errorOutput, cwd) ??
|
|
1301
|
-
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
|
|
1302
|
-
const blockedWritePath =
|
|
1303
|
-
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1304
|
-
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1305
|
-
if (blockedPath && ctx.hasUI) {
|
|
1306
|
-
const config = loadConfig(cwd);
|
|
1307
|
-
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
1308
|
-
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(cwd));
|
|
1309
|
-
const isWriteAllowed = !shouldPromptForWrite(
|
|
1310
|
-
blockedPath,
|
|
1311
|
-
getEffectiveAllowWrite(cwd),
|
|
1312
|
-
matchesPattern,
|
|
1313
|
-
);
|
|
1263
|
+
child.on('close', (code) => {
|
|
1264
|
+
void (async () => {
|
|
1265
|
+
cleanup();
|
|
1266
|
+
if (signal?.aborted) {
|
|
1267
|
+
reject(new Error('aborted'));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (timedOut) {
|
|
1271
|
+
reject(new Error(`timeout:${timeout}`));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1314
1274
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1275
|
+
const errorOutput = errorFdAcc || stderrAcc;
|
|
1276
|
+
|
|
1277
|
+
const blockedPath =
|
|
1278
|
+
extractBlockedPath(errorOutput, cwd) ??
|
|
1279
|
+
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
|
|
1280
|
+
const blockedWritePath =
|
|
1281
|
+
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1282
|
+
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1283
|
+
if (blockedPath && ctx.hasUI && callbacks.promptOnBlock) {
|
|
1284
|
+
const config = loadConfig(cwd);
|
|
1285
|
+
const isDeniedByDenyRead = matchesPattern(
|
|
1321
1286
|
blockedPath,
|
|
1322
|
-
|
|
1287
|
+
config.filesystem.denyRead,
|
|
1323
1288
|
);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1289
|
+
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(config));
|
|
1290
|
+
const isWriteAllowed = !shouldPromptForWrite(
|
|
1291
|
+
blockedPath,
|
|
1292
|
+
getEffectiveAllowWrite(config),
|
|
1293
|
+
matchesPattern,
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
if (blockedWritePath === blockedPath && !isWriteAllowed) {
|
|
1297
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1298
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1299
|
+
} else if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1300
|
+
const choice = await promptReadBlock(
|
|
1301
|
+
ctx,
|
|
1302
|
+
blockedPath,
|
|
1303
|
+
isDeniedByDenyRead ? 'granting allowRead will override it' : undefined,
|
|
1304
|
+
);
|
|
1305
|
+
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1306
|
+
} else if (!isWriteAllowed) {
|
|
1307
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1308
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1309
|
+
}
|
|
1310
|
+
} else if (!blockedPath && ctx.hasUI) {
|
|
1311
|
+
const landstripErrors = parseLandstripTraps(errorOutput);
|
|
1312
|
+
if (landstripErrors.length > 0) {
|
|
1313
|
+
const formatted = formatLandstripTraps(landstripErrors);
|
|
1314
|
+
notify(ctx, `Sandbox blocked an operation: ${formatted}`, 'warning');
|
|
1315
|
+
}
|
|
1334
1316
|
}
|
|
1335
|
-
}
|
|
1336
1317
|
|
|
1337
|
-
|
|
1318
|
+
resolvePromise({ exitCode: code });
|
|
1319
|
+
})().catch(reject);
|
|
1338
1320
|
});
|
|
1339
1321
|
})().catch(reject);
|
|
1340
1322
|
});
|
|
@@ -1380,7 +1362,7 @@ export function createLandstripIntegration(
|
|
|
1380
1362
|
return null;
|
|
1381
1363
|
}
|
|
1382
1364
|
|
|
1383
|
-
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(
|
|
1365
|
+
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config), matchesPattern)) {
|
|
1384
1366
|
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1385
1367
|
if (choice === 'abort') return null;
|
|
1386
1368
|
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
@@ -1412,8 +1394,8 @@ export function createLandstripIntegration(
|
|
|
1412
1394
|
): Promise<AgentToolResult<BashToolDetails | undefined> | null> => {
|
|
1413
1395
|
if (!ctx.hasUI) return null;
|
|
1414
1396
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1397
|
+
const config = loadConfig(ctx.cwd);
|
|
1398
|
+
if (!matchesPattern(blockedPath, getEffectiveAllowRead(config))) {
|
|
1417
1399
|
const choice = await promptReadBlock(
|
|
1418
1400
|
ctx,
|
|
1419
1401
|
blockedPath,
|
|
@@ -1648,7 +1630,7 @@ export function createLandstripIntegration(
|
|
|
1648
1630
|
}
|
|
1649
1631
|
}
|
|
1650
1632
|
|
|
1651
|
-
return { operations: createLandstripBashOps(ctx) };
|
|
1633
|
+
return { operations: createLandstripBashOps(ctx, { promptOnBlock: true }) };
|
|
1652
1634
|
});
|
|
1653
1635
|
|
|
1654
1636
|
pi.on('tool_call', async (event, ctx) => {
|
|
@@ -1672,7 +1654,7 @@ export function createLandstripIntegration(
|
|
|
1672
1654
|
|
|
1673
1655
|
if (isToolCallEventType('read', event)) {
|
|
1674
1656
|
const filePath = canonicalizePath(event.input.path);
|
|
1675
|
-
if (!matchesPattern(filePath, getEffectiveAllowRead(
|
|
1657
|
+
if (!matchesPattern(filePath, getEffectiveAllowRead(config))) {
|
|
1676
1658
|
const choice = await promptReadBlock(ctx, filePath);
|
|
1677
1659
|
if (choice === 'abort') {
|
|
1678
1660
|
return {
|
|
@@ -1696,7 +1678,7 @@ export function createLandstripIntegration(
|
|
|
1696
1678
|
};
|
|
1697
1679
|
}
|
|
1698
1680
|
|
|
1699
|
-
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(
|
|
1681
|
+
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(config), matchesPattern)) {
|
|
1700
1682
|
const choice = await promptWriteBlock(ctx, filePath);
|
|
1701
1683
|
if (choice === 'abort') {
|
|
1702
1684
|
return {
|
|
@@ -1742,7 +1724,6 @@ export function createLandstripIntegration(
|
|
|
1742
1724
|
const muted = (s: string) => theme.fg('muted', s);
|
|
1743
1725
|
const accent = (s: string) => theme.fg('accent', s);
|
|
1744
1726
|
const text = (s: string) => theme.fg('text', s);
|
|
1745
|
-
const borderFg = (s: string) => theme.fg('border', s);
|
|
1746
1727
|
|
|
1747
1728
|
function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
|
|
1748
1729
|
if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
|
|
@@ -1755,29 +1736,16 @@ export function createLandstripIntegration(
|
|
|
1755
1736
|
return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
|
|
1756
1737
|
}
|
|
1757
1738
|
|
|
1758
|
-
function makeRow(content: string, innerW: number, border: string): string {
|
|
1759
|
-
const line = truncateToWidth(content, innerW);
|
|
1760
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
1761
|
-
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
1739
|
return {
|
|
1765
1740
|
render(width: number): string[] {
|
|
1766
1741
|
const innerW = Math.max(1, width - 4);
|
|
1767
|
-
const
|
|
1768
|
-
const row = (content: string) => makeRow(content, innerW, border);
|
|
1742
|
+
const row = (content = '') => boxRow(theme, width, content);
|
|
1769
1743
|
const lines: string[] = [];
|
|
1770
1744
|
const status = sandboxStatus();
|
|
1771
1745
|
const toggleValue = config.enabled
|
|
1772
1746
|
? theme.fg('success', 'enabled')
|
|
1773
1747
|
: theme.fg('warning', 'disabled');
|
|
1774
1748
|
|
|
1775
|
-
function topBorder(titleText: string): string {
|
|
1776
|
-
const title = accent(` ${titleText} `);
|
|
1777
|
-
const fill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
1778
|
-
return `${borderFg('╭─')}${title}${fill}${borderFg('─╮')}`;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
1749
|
function section(titleText: string, detail?: string): void {
|
|
1782
1750
|
lines.push(row(''));
|
|
1783
1751
|
lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
|
|
@@ -1792,7 +1760,7 @@ export function createLandstripIntegration(
|
|
|
1792
1760
|
return text(truncateToWidth(value, Math.max(10, maxWidth)));
|
|
1793
1761
|
}
|
|
1794
1762
|
|
|
1795
|
-
lines.push(
|
|
1763
|
+
lines.push(boxTop(theme, width, 'Sandbox'));
|
|
1796
1764
|
|
|
1797
1765
|
const statusDot = theme.fg(status.color, '●');
|
|
1798
1766
|
const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 28)));
|
|
@@ -1834,9 +1802,7 @@ export function createLandstripIntegration(
|
|
|
1834
1802
|
`${dim('t')} ${muted('toggle persisted setting')} ${dim('esc')} ${muted('close')}`,
|
|
1835
1803
|
),
|
|
1836
1804
|
);
|
|
1837
|
-
lines.push(
|
|
1838
|
-
`${borderFg('╰')}${borderFg('─'.repeat(Math.max(0, width - 2)))}${borderFg('╯')}`,
|
|
1839
|
-
);
|
|
1805
|
+
lines.push(boxBottom(theme, width));
|
|
1840
1806
|
|
|
1841
1807
|
return lines;
|
|
1842
1808
|
},
|