pi-landstrip 0.3.1 → 0.3.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 +230 -169
- package/landstrip.d.ts +3 -0
- package/package.json +3 -2
- package/sandbox.json +1 -0
package/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
|
+
/// <reference path="./landstrip.d.ts" />
|
|
5
|
+
|
|
4
6
|
import type {
|
|
5
7
|
AgentToolResult,
|
|
6
8
|
AgentToolUpdateCallback,
|
|
@@ -45,6 +47,7 @@ interface SandboxFilesystemConfig {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
interface SandboxNetworkConfig {
|
|
50
|
+
allowNetwork: boolean;
|
|
48
51
|
allowLocalBinding: boolean;
|
|
49
52
|
allowAllUnixSockets: boolean;
|
|
50
53
|
allowUnixSockets: string[];
|
|
@@ -60,10 +63,11 @@ interface SandboxConfig {
|
|
|
60
63
|
|
|
61
64
|
interface LandstripPolicy {
|
|
62
65
|
network: {
|
|
66
|
+
allowNetwork: boolean;
|
|
63
67
|
allowLocalBinding: boolean;
|
|
64
68
|
allowAllUnixSockets: boolean;
|
|
65
69
|
allowUnixSockets: string[];
|
|
66
|
-
httpProxyPort
|
|
70
|
+
httpProxyPort?: number;
|
|
67
71
|
};
|
|
68
72
|
filesystem: SandboxFilesystemConfig;
|
|
69
73
|
}
|
|
@@ -76,12 +80,13 @@ interface LandstripErrorResponse {
|
|
|
76
80
|
message: string;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
const LANDSTRIP_VERSION = [0, 9,
|
|
83
|
+
const LANDSTRIP_VERSION = [0, 9, 7] as const;
|
|
80
84
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
81
85
|
|
|
82
86
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
83
87
|
enabled: true,
|
|
84
88
|
network: {
|
|
89
|
+
allowNetwork: false,
|
|
85
90
|
allowLocalBinding: false,
|
|
86
91
|
allowAllUnixSockets: false,
|
|
87
92
|
allowUnixSockets: [],
|
|
@@ -593,16 +598,33 @@ function pipeSockets(client: Socket, upstream: Socket, initialData?: Buffer): vo
|
|
|
593
598
|
upstream.pipe(client);
|
|
594
599
|
}
|
|
595
600
|
|
|
601
|
+
type LandstripBashTool = ReturnType<typeof createBashToolDefinition>;
|
|
602
|
+
|
|
603
|
+
export interface LandstripIntegrationOptions {
|
|
604
|
+
registerBashTool?: boolean;
|
|
605
|
+
cwd?: string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export interface LandstripIntegration {
|
|
609
|
+
createBashTool(cwd: string, ctx?: ExtensionContext): LandstripBashTool;
|
|
610
|
+
register(pi: ExtensionAPI): void;
|
|
611
|
+
}
|
|
612
|
+
|
|
596
613
|
export default function (pi: ExtensionAPI) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
614
|
+
createLandstripIntegration().register(pi);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function createLandstripIntegration(
|
|
618
|
+
options: LandstripIntegrationOptions = {},
|
|
619
|
+
): LandstripIntegration {
|
|
620
|
+
const shouldRegisterBashTool = options.registerBashTool ?? true;
|
|
621
|
+
const localCwd = options.cwd ?? process.cwd();
|
|
602
622
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
623
|
+
function createPlainBashTool(cwd: string): LandstripBashTool {
|
|
624
|
+
return createBashToolDefinition(cwd, {
|
|
625
|
+
shellPath: SettingsManager.create(cwd).getShellPath(),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
606
628
|
|
|
607
629
|
let sandboxEnabled = false;
|
|
608
630
|
let sandboxReady = false;
|
|
@@ -675,15 +697,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
675
697
|
return true;
|
|
676
698
|
}
|
|
677
699
|
|
|
678
|
-
function buildLandstripPolicy(cwd: string, proxyPort: number): LandstripPolicy {
|
|
700
|
+
function buildLandstripPolicy(cwd: string, proxyPort: number | null): LandstripPolicy {
|
|
679
701
|
const config = loadConfig(cwd);
|
|
680
702
|
|
|
681
703
|
return {
|
|
682
704
|
network: {
|
|
705
|
+
allowNetwork: config.network.allowNetwork,
|
|
683
706
|
allowLocalBinding: config.network.allowLocalBinding,
|
|
684
707
|
allowAllUnixSockets: config.network.allowAllUnixSockets,
|
|
685
708
|
allowUnixSockets: config.network.allowUnixSockets,
|
|
686
|
-
httpProxyPort: proxyPort,
|
|
709
|
+
...(proxyPort !== null ? { httpProxyPort: proxyPort } : {}),
|
|
687
710
|
},
|
|
688
711
|
filesystem: {
|
|
689
712
|
denyRead: config.filesystem.denyRead,
|
|
@@ -694,7 +717,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
717
|
};
|
|
695
718
|
}
|
|
696
719
|
|
|
697
|
-
function writePolicyFile(cwd: string, proxyPort: number): { dir: string; path: string } {
|
|
720
|
+
function writePolicyFile(cwd: string, proxyPort: number | null): { dir: string; path: string } {
|
|
698
721
|
const dir = mkdtempSync(join(tmpdir(), 'pi-landstrip-'));
|
|
699
722
|
const path = join(dir, 'policy.json');
|
|
700
723
|
writeFileSync(
|
|
@@ -838,9 +861,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
838
861
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
839
862
|
if (!existsSync(cwd)) throw new Error(`Working directory does not exist: ${cwd}`);
|
|
840
863
|
|
|
841
|
-
const { shell, args } = getShellConfig(
|
|
842
|
-
const
|
|
843
|
-
const
|
|
864
|
+
const { shell, args } = getShellConfig(SettingsManager.create(cwd).getShellPath());
|
|
865
|
+
const config = loadConfig(cwd);
|
|
866
|
+
const allowNetwork = config.network.allowNetwork;
|
|
867
|
+
const proxy = allowNetwork ? null : await startProxy(ctx, cwd);
|
|
868
|
+
const proxyPort = proxy ? proxy.port : null;
|
|
869
|
+
const policy = writePolicyFile(cwd, proxyPort);
|
|
844
870
|
const landstripArgs = ['-p', policy.path, shell, ...args, command];
|
|
845
871
|
|
|
846
872
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -853,13 +879,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
853
879
|
cleaned = true;
|
|
854
880
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
855
881
|
signal?.removeEventListener('abort', onAbort);
|
|
856
|
-
void proxy
|
|
882
|
+
void proxy?.stop();
|
|
857
883
|
rmSync(policy.dir, { recursive: true, force: true });
|
|
858
884
|
};
|
|
859
885
|
|
|
860
886
|
const child = spawn(binaryPath(), landstripArgs, {
|
|
861
887
|
cwd,
|
|
862
|
-
env: proxyEnv(env, proxy
|
|
888
|
+
env: allowNetwork ? { ...process.env, ...env } : proxyEnv(env, proxy!.port),
|
|
863
889
|
detached: true,
|
|
864
890
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
865
891
|
});
|
|
@@ -919,11 +945,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
919
945
|
ctx: ExtensionContext,
|
|
920
946
|
): Promise<AgentToolResult<BashToolDetails | undefined>> {
|
|
921
947
|
let landstripStderr = '';
|
|
922
|
-
const sandboxedBash = createBashToolDefinition(
|
|
948
|
+
const sandboxedBash = createBashToolDefinition(ctx.cwd, {
|
|
923
949
|
operations: createLandstripBashOps(ctx, (data) => {
|
|
924
950
|
landstripStderr += data.toString('utf8');
|
|
925
951
|
}),
|
|
926
|
-
shellPath:
|
|
952
|
+
shellPath: SettingsManager.create(ctx.cwd).getShellPath(),
|
|
927
953
|
});
|
|
928
954
|
|
|
929
955
|
const run = () => sandboxedBash.execute(id, params, signal, onUpdate, ctx);
|
|
@@ -986,6 +1012,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
986
1012
|
}
|
|
987
1013
|
|
|
988
1014
|
function warnIfAllDomainsAllowed(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1015
|
+
if (config.network.allowNetwork) {
|
|
1016
|
+
ctx.ui.notify('Network sandbox is disabled because network.allowNetwork is true.', 'warning');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
989
1019
|
if (!allowsAllDomains(config.network.allowedDomains)) return;
|
|
990
1020
|
ctx.ui.notify(
|
|
991
1021
|
'Network sandbox allows all domains because network.allowedDomains contains "*".',
|
|
@@ -994,9 +1024,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
994
1024
|
}
|
|
995
1025
|
|
|
996
1026
|
function enableStatus(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
997
|
-
const networkLabel =
|
|
998
|
-
? '
|
|
999
|
-
:
|
|
1027
|
+
const networkLabel = config.network.allowNetwork
|
|
1028
|
+
? 'unrestricted'
|
|
1029
|
+
: allowsAllDomains(config.network.allowedDomains)
|
|
1030
|
+
? 'all domains'
|
|
1031
|
+
: `${config.network.allowedDomains.length} domains`;
|
|
1000
1032
|
ctx.ui.setStatus(
|
|
1001
1033
|
'sandbox',
|
|
1002
1034
|
ctx.ui.theme.fg(
|
|
@@ -1030,7 +1062,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1030
1062
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1031
1063
|
sandboxEnabled = false;
|
|
1032
1064
|
sandboxReady = false;
|
|
1033
|
-
ctx.ui.notify(`landstrip 0.9.
|
|
1065
|
+
ctx.ui.notify(`landstrip 0.9.7 or newer is required; found: ${version}`, 'error');
|
|
1034
1066
|
return false;
|
|
1035
1067
|
}
|
|
1036
1068
|
|
|
@@ -1041,178 +1073,207 @@ export default function (pi: ExtensionAPI) {
|
|
|
1041
1073
|
return true;
|
|
1042
1074
|
}
|
|
1043
1075
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
label: 'bash (landstrip)',
|
|
1047
|
-
async execute(id, params, signal, onUpdate, ctx) {
|
|
1048
|
-
if (!sandboxEnabled || !sandboxReady)
|
|
1049
|
-
return localBash.execute(id, params, signal, onUpdate, ctx);
|
|
1076
|
+
function createBashTool(cwd: string, ctx?: ExtensionContext): LandstripBashTool {
|
|
1077
|
+
const localBash = createPlainBashTool(cwd);
|
|
1050
1078
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
|
|
1064
|
-
exitCode: 1,
|
|
1065
|
-
cancelled: false,
|
|
1066
|
-
truncated: false,
|
|
1067
|
-
},
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return { operations: createLandstripBashOps(ctx) };
|
|
1072
|
-
});
|
|
1079
|
+
return {
|
|
1080
|
+
...localBash,
|
|
1081
|
+
label: 'bash (landstrip)',
|
|
1082
|
+
async execute(id, params, signal, onUpdate, callCtx) {
|
|
1083
|
+
const effectiveCtx = callCtx ?? ctx;
|
|
1084
|
+
if (!sandboxEnabled || !sandboxReady || !effectiveCtx)
|
|
1085
|
+
return localBash.execute(id, params, signal, onUpdate, effectiveCtx);
|
|
1086
|
+
|
|
1087
|
+
return runBashWithOptionalRetry(id, params, signal, onUpdate, effectiveCtx);
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1073
1091
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1092
|
+
function register(pi: ExtensionAPI): void {
|
|
1093
|
+
const maybePi = pi as ExtensionAPI & {
|
|
1094
|
+
getFlag?: (name: string) => unknown;
|
|
1095
|
+
registerCommand?: ExtensionAPI['registerCommand'];
|
|
1096
|
+
registerFlag?: ExtensionAPI['registerFlag'];
|
|
1097
|
+
};
|
|
1076
1098
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1099
|
+
maybePi.registerFlag?.('no-sandbox', {
|
|
1100
|
+
description: 'Disable landstrip sandboxing for bash commands',
|
|
1101
|
+
type: 'boolean',
|
|
1102
|
+
default: false,
|
|
1103
|
+
});
|
|
1079
1104
|
|
|
1080
|
-
|
|
1105
|
+
if (shouldRegisterBashTool) pi.registerTool(createBashTool(localCwd));
|
|
1081
1106
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
block: true,
|
|
1087
|
-
reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1107
|
+
pi.on('user_bash', async (event, ctx) => {
|
|
1108
|
+
if (!sandboxEnabled || !sandboxReady) return;
|
|
1109
|
+
const config = loadConfig(ctx.cwd);
|
|
1110
|
+
if (!config.enabled) return;
|
|
1091
1111
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const choice = await promptReadBlock(ctx, filePath);
|
|
1096
|
-
if (choice === 'abort') {
|
|
1112
|
+
if (!config.network.allowNetwork) {
|
|
1113
|
+
const blockedDomain = await preflightCommandDomains(event.command, ctx);
|
|
1114
|
+
if (blockedDomain) {
|
|
1097
1115
|
return {
|
|
1098
|
-
|
|
1099
|
-
|
|
1116
|
+
result: {
|
|
1117
|
+
output: `Blocked: "${blockedDomain}" is not allowed by the sandbox. Use /sandbox to review your config.`,
|
|
1118
|
+
exitCode: 1,
|
|
1119
|
+
cancelled: false,
|
|
1120
|
+
truncated: false,
|
|
1121
|
+
},
|
|
1100
1122
|
};
|
|
1101
1123
|
}
|
|
1102
|
-
await applyReadChoice(choice, filePath, ctx.cwd);
|
|
1103
1124
|
}
|
|
1104
|
-
}
|
|
1105
1125
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1126
|
+
return { operations: createLandstripBashOps(ctx) };
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
1130
|
+
if (!sandboxEnabled) return;
|
|
1131
|
+
|
|
1132
|
+
const config = loadConfig(ctx.cwd);
|
|
1133
|
+
if (!config.enabled) return;
|
|
1134
|
+
|
|
1135
|
+
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1108
1136
|
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1137
|
+
if (sandboxReady && isToolCallEventType('bash', event)) {
|
|
1138
|
+
if (!config.network.allowNetwork) {
|
|
1139
|
+
const blockedDomain = await preflightCommandDomains(event.input.command, ctx);
|
|
1140
|
+
if (blockedDomain) {
|
|
1141
|
+
return {
|
|
1142
|
+
block: true,
|
|
1143
|
+
reason: `Network access to "${blockedDomain}" is blocked by the sandbox.`,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1116
1147
|
}
|
|
1117
1148
|
|
|
1118
|
-
if (
|
|
1119
|
-
const
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1149
|
+
if (isToolCallEventType('read', event)) {
|
|
1150
|
+
const filePath = canonicalizePath(event.input.path);
|
|
1151
|
+
if (!matchesPattern(filePath, getEffectiveAllowRead(ctx.cwd))) {
|
|
1152
|
+
const choice = await promptReadBlock(ctx, filePath);
|
|
1153
|
+
if (choice === 'abort') {
|
|
1154
|
+
return {
|
|
1155
|
+
block: true,
|
|
1156
|
+
reason: `Sandbox: read access denied for "${filePath}"`,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
await applyReadChoice(choice, filePath, ctx.cwd);
|
|
1125
1160
|
}
|
|
1126
|
-
await applyWriteChoice(choice, filePath, ctx.cwd);
|
|
1127
1161
|
}
|
|
1128
|
-
}
|
|
1129
|
-
});
|
|
1130
1162
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1163
|
+
if (isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
|
|
1164
|
+
const filePath = canonicalizePath((event.input as { path: string }).path);
|
|
1133
1165
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1166
|
+
if (matchesPattern(filePath, config.filesystem.denyWrite)) {
|
|
1167
|
+
return {
|
|
1168
|
+
block: true,
|
|
1169
|
+
reason:
|
|
1170
|
+
`Sandbox: write access denied for "${filePath}" (in denyWrite). ` +
|
|
1171
|
+
`To change this, edit denyWrite in:\n ${projectPath}\n ${globalPath}`,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1140
1174
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1175
|
+
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(ctx.cwd), matchesPattern)) {
|
|
1176
|
+
const choice = await promptWriteBlock(ctx, filePath);
|
|
1177
|
+
if (choice === 'abort') {
|
|
1178
|
+
return {
|
|
1179
|
+
block: true,
|
|
1180
|
+
reason: `Sandbox: write access denied for "${filePath}" (not in allowWrite)`,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
await applyWriteChoice(choice, filePath, ctx.cwd);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1148
1187
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1188
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
1189
|
+
const noSandbox = maybePi.getFlag?.('no-sandbox') as boolean;
|
|
1151
1190
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
ctx.ui.notify('Sandbox is already enabled', 'info');
|
|
1191
|
+
if (noSandbox) {
|
|
1192
|
+
sandboxEnabled = false;
|
|
1193
|
+
sandboxReady = false;
|
|
1194
|
+
ctx.ui.notify('Sandbox disabled via --no-sandbox', 'warning');
|
|
1157
1195
|
return;
|
|
1158
1196
|
}
|
|
1159
1197
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
description: 'Disable the landstrip sandbox for this session',
|
|
1166
|
-
handler: async (_args, ctx) => {
|
|
1167
|
-
if (!sandboxEnabled) {
|
|
1168
|
-
ctx.ui.notify('Sandbox is already disabled', 'info');
|
|
1198
|
+
const config = loadConfig(ctx.cwd);
|
|
1199
|
+
if (!config.enabled) {
|
|
1200
|
+
sandboxEnabled = false;
|
|
1201
|
+
sandboxReady = false;
|
|
1202
|
+
ctx.ui.notify('Sandbox disabled via config', 'info');
|
|
1169
1203
|
return;
|
|
1170
1204
|
}
|
|
1171
1205
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
ctx.ui.setStatus('sandbox', '');
|
|
1175
|
-
ctx.ui.notify('Sandbox disabled', 'info');
|
|
1176
|
-
},
|
|
1177
|
-
});
|
|
1206
|
+
enableSandbox(ctx);
|
|
1207
|
+
});
|
|
1178
1208
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1209
|
+
maybePi.registerCommand?.('sandbox-enable', {
|
|
1210
|
+
description: 'Enable the landstrip sandbox for this session',
|
|
1211
|
+
handler: async (_args, ctx) => {
|
|
1212
|
+
if (sandboxEnabled) {
|
|
1213
|
+
ctx.ui.notify('Sandbox is already enabled', 'info');
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1186
1216
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
'',
|
|
1202
|
-
'
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1217
|
+
if (enableSandbox(ctx)) ctx.ui.notify('Sandbox enabled', 'info');
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
maybePi.registerCommand?.('sandbox-disable', {
|
|
1222
|
+
description: 'Disable the landstrip sandbox for this session',
|
|
1223
|
+
handler: async (_args, ctx) => {
|
|
1224
|
+
if (!sandboxEnabled) {
|
|
1225
|
+
ctx.ui.notify('Sandbox is already disabled', 'info');
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
sandboxEnabled = false;
|
|
1230
|
+
sandboxReady = false;
|
|
1231
|
+
ctx.ui.setStatus('sandbox', '');
|
|
1232
|
+
ctx.ui.notify('Sandbox disabled', 'info');
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
maybePi.registerCommand?.('sandbox', {
|
|
1237
|
+
description: 'Show sandbox configuration',
|
|
1238
|
+
handler: async (_args, ctx) => {
|
|
1239
|
+
if (!sandboxEnabled) {
|
|
1240
|
+
ctx.ui.notify('Sandbox is disabled', 'info');
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const config = loadConfig(ctx.cwd);
|
|
1245
|
+
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1246
|
+
const lines = [
|
|
1247
|
+
'Sandbox Configuration',
|
|
1248
|
+
` Project config: ${projectPath}`,
|
|
1249
|
+
` Global config: ${globalPath}`,
|
|
1250
|
+
` landstrip: ${binaryPath()}`,
|
|
1251
|
+
'',
|
|
1252
|
+
`Network (bash ${config.network.allowNetwork ? 'unrestricted' : 'through HTTP proxy'}):`,
|
|
1253
|
+
` Allow network: ${config.network.allowNetwork}`,
|
|
1254
|
+
` Allowed domains: ${config.network.allowedDomains.join(', ') || '(none)'}`,
|
|
1255
|
+
` Denied domains: ${config.network.deniedDomains.join(', ') || '(none)'}`,
|
|
1256
|
+
...(sessionAllowedDomains.length > 0
|
|
1257
|
+
? [` Session allowed: ${sessionAllowedDomains.join(', ')}`]
|
|
1258
|
+
: []),
|
|
1259
|
+
'',
|
|
1260
|
+
'Filesystem (bash + read/write/edit tools):',
|
|
1261
|
+
` Deny Read: ${config.filesystem.denyRead.join(', ') || '(none)'}`,
|
|
1262
|
+
` Allow Read: ${config.filesystem.allowRead.join(', ') || '(none)'}`,
|
|
1263
|
+
` Allow Write: ${config.filesystem.allowWrite.join(', ') || '(none)'}`,
|
|
1264
|
+
` Deny Write: ${config.filesystem.denyWrite.join(', ') || '(none)'}`,
|
|
1265
|
+
...(sessionAllowedReadPaths.length > 0
|
|
1266
|
+
? [` Session read: ${sessionAllowedReadPaths.join(', ')}`]
|
|
1267
|
+
: []),
|
|
1268
|
+
...(sessionAllowedWritePaths.length > 0
|
|
1269
|
+
? [` Session write: ${sessionAllowedWritePaths.join(', ')}`]
|
|
1270
|
+
: []),
|
|
1271
|
+
];
|
|
1272
|
+
|
|
1273
|
+
ctx.ui.notify(lines.join('\n'), 'info');
|
|
1274
|
+
},
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return { createBashTool, register };
|
|
1218
1279
|
}
|
package/landstrip.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Landlock-based sandboxing for pi with interactive permission prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"index.ts",
|
|
18
|
+
"landstrip.d.ts",
|
|
18
19
|
"README.md",
|
|
19
20
|
"sandbox.json"
|
|
20
21
|
],
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
33
|
-
"@jarkkojs/landstrip": "^0.9.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.9.7"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|