@rjshrjndrn/pi-sandbox 0.1.0 → 0.1.2
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/README.md +1 -1
- package/package.json +11 -3
- package/src/boundary.ts +39 -10
- package/tests/boundary.test.ts +26 -22
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rjshrjndrn/pi-sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Filesystem boundary enforcement for pi — prompts before the agent escapes your project",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"sandbox",
|
|
9
|
+
"filesystem",
|
|
10
|
+
"permissions"
|
|
11
|
+
],
|
|
6
12
|
"license": "MIT",
|
|
7
13
|
"type": "module",
|
|
8
14
|
"pi": {
|
|
9
|
-
"extensions": [
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./extensions"
|
|
17
|
+
]
|
|
10
18
|
},
|
|
11
19
|
"peerDependencies": {
|
|
12
20
|
"@mariozechner/pi-coding-agent": "*",
|
package/src/boundary.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
2
|
+
import { execSync } from "node:child_process"
|
|
2
3
|
import { state } from "./state.js"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Detect the project filesystem boundary.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Tries multiple methods to find the git worktree root:
|
|
9
|
+
* 1. pi.exec() (preferred, uses pi's exec infrastructure)
|
|
10
|
+
* 2. child_process.execSync() (fallback, direct OS call)
|
|
11
|
+
* 3. Falls back to `cwd` if git is not available or not a git repo.
|
|
9
12
|
*/
|
|
10
13
|
export async function detectBoundary(
|
|
11
14
|
pi: ExtensionAPI,
|
|
12
15
|
cwd: string
|
|
13
16
|
): Promise<{ boundary: string; source: "git" | "cwd" }> {
|
|
17
|
+
// Try pi.exec first
|
|
14
18
|
try {
|
|
15
19
|
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], {
|
|
16
20
|
timeout: 5000,
|
|
@@ -20,8 +24,24 @@ export async function detectBoundary(
|
|
|
20
24
|
return { boundary: toplevel, source: "git" }
|
|
21
25
|
}
|
|
22
26
|
} catch {
|
|
23
|
-
//
|
|
27
|
+
// pi.exec may not be available in all contexts — try direct exec
|
|
24
28
|
}
|
|
29
|
+
|
|
30
|
+
// Fallback: direct child_process
|
|
31
|
+
try {
|
|
32
|
+
const toplevel = execSync("git rev-parse --show-toplevel", {
|
|
33
|
+
cwd,
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
+
}).trim()
|
|
38
|
+
if (toplevel.length > 0) {
|
|
39
|
+
return { boundary: toplevel, source: "git" }
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// git not available or not a git repo
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
return { boundary: cwd, source: "cwd" }
|
|
26
46
|
}
|
|
27
47
|
|
|
@@ -30,13 +50,22 @@ export async function detectBoundary(
|
|
|
30
50
|
*/
|
|
31
51
|
export function registerBoundaryDetection(pi: ExtensionAPI) {
|
|
32
52
|
pi.on("session_start", async (_event, ctx) => {
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
try {
|
|
54
|
+
const { boundary, source } = await detectBoundary(pi, ctx.cwd)
|
|
55
|
+
state.boundary = boundary
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
const label =
|
|
58
|
+
source === "git"
|
|
59
|
+
? `🔒 pi-sandbox: boundary set to ${boundary} (git worktree)`
|
|
60
|
+
: `🔒 pi-sandbox: boundary set to ${boundary} (cwd fallback)`
|
|
61
|
+
ctx.ui.notify(label, "info")
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Last resort: use cwd as boundary
|
|
64
|
+
state.boundary = ctx.cwd
|
|
65
|
+
ctx.ui.notify(
|
|
66
|
+
`🔒 pi-sandbox: boundary set to ${ctx.cwd} (error fallback: ${err})`,
|
|
67
|
+
"warning"
|
|
68
|
+
)
|
|
69
|
+
}
|
|
41
70
|
})
|
|
42
71
|
}
|
package/tests/boundary.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ function mockPiExecThrows() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe("detectBoundary", () => {
|
|
17
|
-
it("returns git worktree root when
|
|
17
|
+
it("returns git worktree root when pi.exec succeeds", async () => {
|
|
18
18
|
const pi = mockPi({
|
|
19
19
|
stdout: "/Users/test/project\n",
|
|
20
20
|
stderr: "",
|
|
@@ -26,46 +26,50 @@ describe("detectBoundary", () => {
|
|
|
26
26
|
expect(result.source).toBe("git")
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
it("
|
|
29
|
+
it("trims whitespace from git output", async () => {
|
|
30
30
|
const pi = mockPi({
|
|
31
|
-
stdout: "",
|
|
32
|
-
stderr: "
|
|
33
|
-
code:
|
|
31
|
+
stdout: " /Users/test/project \n",
|
|
32
|
+
stderr: "",
|
|
33
|
+
code: 0,
|
|
34
34
|
killed: false,
|
|
35
35
|
})
|
|
36
|
-
const result = await detectBoundary(pi, "/Users/test/
|
|
37
|
-
expect(result.boundary).toBe("/Users/test/
|
|
38
|
-
expect(result.source).toBe("
|
|
36
|
+
const result = await detectBoundary(pi, "/Users/test/project/src")
|
|
37
|
+
expect(result.boundary).toBe("/Users/test/project")
|
|
38
|
+
expect(result.source).toBe("git")
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it("falls back to
|
|
41
|
+
it("falls back to execSync when pi.exec fails", async () => {
|
|
42
|
+
// pi.exec throws, but we're in a real git repo so execSync will find it
|
|
42
43
|
const pi = mockPiExecThrows()
|
|
43
|
-
const result = await detectBoundary(pi,
|
|
44
|
-
|
|
45
|
-
expect(result.source).toBe("
|
|
44
|
+
const result = await detectBoundary(pi, process.cwd())
|
|
45
|
+
// Should still detect git via execSync fallback
|
|
46
|
+
expect(result.source).toBe("git")
|
|
47
|
+
expect(result.boundary.length).toBeGreaterThan(0)
|
|
46
48
|
})
|
|
47
49
|
|
|
48
|
-
it("falls back to cwd
|
|
50
|
+
it("falls back to cwd in a non-git directory", async () => {
|
|
49
51
|
const pi = mockPi({
|
|
50
52
|
stdout: "",
|
|
51
|
-
stderr: "",
|
|
52
|
-
code:
|
|
53
|
+
stderr: "fatal: not a git repository",
|
|
54
|
+
code: 128,
|
|
53
55
|
killed: false,
|
|
54
56
|
})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
// Use /tmp which is not a git repo
|
|
58
|
+
const result = await detectBoundary(pi, "/tmp")
|
|
59
|
+
expect(result.boundary).toBe("/tmp")
|
|
57
60
|
expect(result.source).toBe("cwd")
|
|
58
61
|
})
|
|
59
62
|
|
|
60
|
-
it("
|
|
63
|
+
it("falls back to cwd when pi.exec returns empty stdout", async () => {
|
|
61
64
|
const pi = mockPi({
|
|
62
|
-
stdout: "
|
|
65
|
+
stdout: "",
|
|
63
66
|
stderr: "",
|
|
64
67
|
code: 0,
|
|
65
68
|
killed: false,
|
|
66
69
|
})
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(result.
|
|
70
|
+
// execSync will also fail in /tmp
|
|
71
|
+
const result = await detectBoundary(pi, "/tmp")
|
|
72
|
+
expect(result.boundary).toBe("/tmp")
|
|
73
|
+
expect(result.source).toBe("cwd")
|
|
70
74
|
})
|
|
71
75
|
})
|