@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 +1 -1
- package/extensions/permissions/index.ts +56 -25
- package/extensions/permissions/utils.ts +15 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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):
|
|
22
|
+
function denyResult(reason: string): BlockResult {
|
|
20
23
|
return { block: true, reason: `User denied: ${reason}` };
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
const
|
|
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
|
-
|
|
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
|
|
80
|
+
if (!reason) return;
|
|
62
81
|
|
|
63
|
-
if (
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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": "
|
|
69
|
+
"commit": "09a1d037c3a42a9041ce7baccd6d22411cb365ed"
|
|
70
70
|
}
|