@dcl-regenesislabs/opendcl 0.1.4-22555336781.commit-12e2bc1 → 0.1.4-22626562583.commit-09a1d03

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 CHANGED
@@ -32,7 +32,7 @@ The result: **more creators building more scenes, faster.**
32
32
  - **Integrated commands** — `/init` to scaffold, `/preview` to launch the dev server, `/tasks` to manage running processes, `/review` to audit code
33
33
  - **TypeScript validation** — catches type errors immediately after writing code
34
34
  - **Free asset catalogs** — 2,700+ Creator Hub 3D models, 900+ CC0-licensed models, and 50 audio files the agent proactively suggests when building scenes
35
- - **Permission gate** — prompts for confirmation before destructive bash commands or writes to sensitive files
35
+ - **Permission gate** — prompts for confirmation before destructive bash commands, writes to sensitive files, or any file access outside the working directory
36
36
  - **Compact tool output** — write shows path + size instead of file content, read shows a 5-line preview instead of 10
37
37
  - **Session persistence** — pick up where you left off across sessions
38
38
 
@@ -7,25 +7,53 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
10
- import { classifyBashCommand, classifyFilePath } from "./utils.js";
10
+ import { classifyBashCommand, classifyFilePath, isOutsideCwd, OUTSIDE_CWD_REASON } from "./utils.js";
11
+ import { resolve } from "node:path";
11
12
 
12
- function blockResult(reason: string, detail: string): { block: true; reason: string } {
13
+ type BlockResult = { block: true; reason: string };
14
+
15
+ function blockResult(reason: string, detail: string): BlockResult {
13
16
  return {
14
17
  block: true,
15
18
  reason: `Blocked: ${reason}\n${detail}\nUse --no-permissions to allow in non-interactive mode.`,
16
19
  };
17
20
  }
18
21
 
19
- function denyResult(reason: string): { block: true; reason: string } {
22
+ function denyResult(reason: string): BlockResult {
20
23
  return { block: true, reason: `User denied: ${reason}` };
21
24
  }
22
25
 
23
- const ALLOW = "Allow";
24
- const ALWAYS = "Always allow";
25
- const DENY = "Deny";
26
+ const CHOICES = ["Allow", "Always allow", "Deny"] as const;
26
27
 
27
28
  const extension: ExtensionFactory = (pi) => {
28
29
  const sessionAllow = new Set<string>();
30
+ const allowedPaths = new Set<string>();
31
+
32
+ function isPathAllowed(resolvedPath: string): boolean {
33
+ for (const allowed of allowedPaths) {
34
+ if (resolvedPath === allowed || resolvedPath.startsWith(allowed + "/")) return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * Prompts the user for confirmation and handles their choice.
41
+ * Returns a BlockResult to deny, or undefined to allow.
42
+ */
43
+ async function promptOrBlock(
44
+ ctx: { hasUI: boolean; ui: { select: (title: string, options: string[]) => Promise<string | null> } },
45
+ reason: string,
46
+ detail: string,
47
+ onAlways: () => void,
48
+ ): Promise<BlockResult | undefined> {
49
+ if (!ctx.hasUI) return blockResult(reason, detail);
50
+
51
+ const choice = await ctx.ui.select(`${reason}\n${detail}`, [...CHOICES]);
52
+
53
+ if (choice === "Always allow") { onAlways(); return; }
54
+ if (choice === "Allow") return;
55
+ return denyResult(reason);
56
+ }
29
57
 
30
58
  pi.registerFlag("no-permissions", {
31
59
  description: "Disable permission gate (skip confirmation prompts for dangerous operations)",
@@ -43,33 +71,36 @@ const extension: ExtensionFactory = (pi) => {
43
71
  const reason = classifyBashCommand(command);
44
72
  if (!reason || sessionAllow.has(reason)) return;
45
73
 
46
- if (!ctx.hasUI) return blockResult(reason, `Command: ${command}`);
47
-
48
- const choice = await ctx.ui.select(
49
- `${reason}\nCommand: ${command}`,
50
- [ALLOW, ALWAYS, DENY],
51
- );
52
-
53
- if (choice === ALWAYS) { sessionAllow.add(reason); return; }
54
- if (choice === ALLOW) return;
55
- return denyResult(reason);
74
+ return promptOrBlock(ctx, reason, `Command: ${command}`, () => sessionAllow.add(reason));
56
75
  }
57
76
 
58
77
  if (toolName === "write" || toolName === "edit") {
59
78
  const filePath = (event.input as { path?: string }).path ?? "";
60
79
  const reason = filePath ? classifyFilePath(filePath, ctx.cwd) : null;
61
- if (!reason || sessionAllow.has(reason)) return;
80
+ if (!reason) return;
62
81
 
63
- if (!ctx.hasUI) return blockResult(reason, `Path: ${filePath}`);
82
+ if (reason === OUTSIDE_CWD_REASON) {
83
+ const resolved = resolve(ctx.cwd, filePath);
84
+ if (isPathAllowed(resolved)) return;
85
+
86
+ return promptOrBlock(ctx, reason, `File: ${filePath}`, () => allowedPaths.add(resolved));
87
+ }
88
+
89
+ if (sessionAllow.has(reason)) return;
90
+
91
+ return promptOrBlock(ctx, reason, `Path: ${filePath}`, () => sessionAllow.add(reason));
92
+ }
93
+
94
+ if (toolName === "read" || toolName === "grep" || toolName === "find" || toolName === "ls") {
95
+ const filePath = (event.input as { path?: string }).path ?? "";
96
+ if (!filePath) return;
64
97
 
65
- const choice = await ctx.ui.select(
66
- `${reason}\nFile: ${filePath}`,
67
- [ALLOW, ALWAYS, DENY],
68
- );
98
+ const resolved = resolve(ctx.cwd, filePath);
99
+ const reason = isOutsideCwd(filePath, ctx.cwd);
100
+ if (!reason) return;
101
+ if (isPathAllowed(resolved)) return;
69
102
 
70
- if (choice === ALWAYS) { sessionAllow.add(reason); return; }
71
- if (choice === ALLOW) return;
72
- return denyResult(reason);
103
+ return promptOrBlock(ctx, reason, `Path: ${filePath}`, () => allowedPaths.add(resolved));
73
104
  }
74
105
  });
75
106
  };
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { resolve, relative } from "node:path";
7
7
 
8
+ export const OUTSIDE_CWD_REASON = "Accesses path outside working directory";
9
+
8
10
  interface DenylistEntry {
9
11
  pattern: RegExp;
10
12
  reason: string;
@@ -75,8 +77,20 @@ export function classifyFilePath(filePath: string, projectRoot: string): string
75
77
  const rel = relative(projectRoot, resolved);
76
78
 
77
79
  if (rel.startsWith("..")) {
78
- return "File is outside the project root";
80
+ return OUTSIDE_CWD_REASON;
79
81
  }
80
82
 
81
83
  return findMatchingReason(SENSITIVE_FILE_PATTERNS, filePath);
82
84
  }
85
+
86
+ /**
87
+ * Returns a reason string if the file path resolves outside the given cwd,
88
+ * or null if inside (or empty).
89
+ */
90
+ export function isOutsideCwd(filePath: string, cwd: string): string | null {
91
+ if (!filePath) return null;
92
+ const resolved = resolve(cwd, filePath);
93
+ const rel = relative(cwd, resolved);
94
+ if (rel.startsWith("..")) return OUTSIDE_CWD_REASON;
95
+ return null;
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl-regenesislabs/opendcl",
3
- "version": "0.1.4-22555336781.commit-12e2bc1",
3
+ "version": "0.1.4-22626562583.commit-09a1d03",
4
4
  "description": "AI coding assistant for Decentraland SDK7 scene development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,5 +66,5 @@
66
66
  "prompts/",
67
67
  "context/"
68
68
  ],
69
- "commit": "12e2bc1a70db6216feafebf6564faa40515c72c0"
69
+ "commit": "09a1d037c3a42a9041ce7baccd6d22411cb365ed"
70
70
  }