@swifty.js/swifty 0.0.21 → 0.0.22
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/dist/{agent-ZCMBUWLZ.js → agent-5T7KNC7V.js} +1 -1
- package/dist/{anthropic-4ZVG7AMA.js → anthropic-P2R4GW6F.js} +1 -1
- package/dist/{chunk-Y5WGBAGP.js → chunk-HJIGA37J.js} +1 -1
- package/dist/{chunk-DDBH5AEN.js → chunk-L73IHJF4.js} +53 -53
- package/dist/{chunk-S6ZADC7C.js → chunk-NEAR6YPZ.js} +1 -1
- package/dist/{chunk-MUPVOATV.js → chunk-NLNH3IRT.js} +1 -1
- package/dist/{chunk-6E6UA7MT.js → chunk-YQQVB6VB.js} +9 -8
- package/dist/lib/agent-2AGRYN3R.js +9 -0
- package/dist/lib/anthropic-GFWP3ORB.js +22 -0
- package/dist/lib/bwrap-QRGPUP4J.js +6 -0
- package/dist/lib/checker-IIXJXQCB.js +19 -0
- package/dist/lib/chunk-6GOWRPYS.js +339 -0
- package/dist/lib/chunk-7URDLWQN.js +426 -0
- package/dist/lib/chunk-C7IHCJZ3.js +849 -0
- package/dist/lib/chunk-EJLGB2EJ.js +377 -0
- package/dist/lib/chunk-GHF2PSEW.js +8 -0
- package/dist/lib/chunk-GNBXECZN.js +243 -0
- package/dist/lib/chunk-HK2Z6WP4.js +223 -0
- package/dist/lib/chunk-LUEP4JMF.js +549 -0
- package/dist/lib/chunk-ORSYNBMM.js +38 -0
- package/dist/lib/chunk-RR2CZ6CY.js +598 -0
- package/dist/lib/chunk-UHVO63Y7.js +1246 -0
- package/dist/lib/chunk-UMFNTXKA.js +88 -0
- package/dist/lib/chunk-VUD72RTY.js +39 -0
- package/dist/lib/glob.wasm +0 -0
- package/dist/lib/index.d.ts +5350 -0
- package/dist/lib/index.js +12002 -0
- package/dist/lib/openai-HXRMPNB7.js +15 -0
- package/dist/lib/seatbelt-FT5IY73W.js +6 -0
- package/dist/lib/tool-filter-VF7TZRE5.js +19 -0
- package/dist/main.js +225 -225
- package/dist/{openai-HXD52MZB.js → openai-TVUUHRG7.js} +1 -1
- package/dist/{server-VMHWEO2Y.js → server-ZTLHJWEQ.js} +14 -14
- package/package.json +14 -2
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mcpCallPermissionContent
|
|
3
|
+
} from "./chunk-GNBXECZN.js";
|
|
4
|
+
import {
|
|
5
|
+
createChildLogger,
|
|
6
|
+
strArg
|
|
7
|
+
} from "./chunk-EJLGB2EJ.js";
|
|
8
|
+
import {
|
|
9
|
+
MCP_CALL_TOOL_NAME
|
|
10
|
+
} from "./chunk-GHF2PSEW.js";
|
|
11
|
+
|
|
12
|
+
// src/permissions/checker.ts
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
14
|
+
import { homedir, tmpdir } from "os";
|
|
15
|
+
import { join, resolve, dirname } from "path";
|
|
16
|
+
import yaml from "js-yaml";
|
|
17
|
+
import z, { parse } from "zod";
|
|
18
|
+
var log = createChildLogger({ module: "permissions" });
|
|
19
|
+
var DANGEROUS_PATTERNS = [];
|
|
20
|
+
var SAFE_PREFIXES = [
|
|
21
|
+
"ls",
|
|
22
|
+
"pwd",
|
|
23
|
+
"echo",
|
|
24
|
+
"cat",
|
|
25
|
+
"head",
|
|
26
|
+
"tail",
|
|
27
|
+
"wc",
|
|
28
|
+
"date",
|
|
29
|
+
"whoami",
|
|
30
|
+
"uname",
|
|
31
|
+
"hostname",
|
|
32
|
+
"which",
|
|
33
|
+
"type",
|
|
34
|
+
"file",
|
|
35
|
+
"git status",
|
|
36
|
+
"git log",
|
|
37
|
+
"git diff",
|
|
38
|
+
"git branch",
|
|
39
|
+
"git show",
|
|
40
|
+
"git rev-parse",
|
|
41
|
+
"git remote",
|
|
42
|
+
"bun test",
|
|
43
|
+
"bun run",
|
|
44
|
+
"npm test",
|
|
45
|
+
"npm run",
|
|
46
|
+
"go test",
|
|
47
|
+
"go build",
|
|
48
|
+
"go vet",
|
|
49
|
+
"python -c",
|
|
50
|
+
"node -e"
|
|
51
|
+
];
|
|
52
|
+
var CONTENT_FIELDS = {
|
|
53
|
+
Bash: "command",
|
|
54
|
+
PowerShell: "command",
|
|
55
|
+
ReadFile: "file_path",
|
|
56
|
+
WriteFile: "file_path",
|
|
57
|
+
EditFile: "file_path",
|
|
58
|
+
Glob: "pattern",
|
|
59
|
+
Grep: "pattern"
|
|
60
|
+
};
|
|
61
|
+
var DEFAULT_DENY_WRITE = [
|
|
62
|
+
".swifty/config.yaml",
|
|
63
|
+
".swifty/permissions.local.yaml",
|
|
64
|
+
".swifty/skills/"
|
|
65
|
+
];
|
|
66
|
+
function extractContent(toolName, args) {
|
|
67
|
+
if (toolName === MCP_CALL_TOOL_NAME) {
|
|
68
|
+
return mcpCallPermissionContent(strArg(args, "server", ""), strArg(args, "tool", ""));
|
|
69
|
+
}
|
|
70
|
+
const field = CONTENT_FIELDS[toolName];
|
|
71
|
+
if (!field) {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
const v = args[field];
|
|
75
|
+
return typeof v === "string" ? v : "";
|
|
76
|
+
}
|
|
77
|
+
var PathSandbox = class {
|
|
78
|
+
allowedRoots;
|
|
79
|
+
denyWritePaths;
|
|
80
|
+
projectDir;
|
|
81
|
+
constructor(projectDir) {
|
|
82
|
+
this.projectDir = resolve(projectDir);
|
|
83
|
+
this.allowedRoots = [this.projectDir, tmpdir()];
|
|
84
|
+
this.denyWritePaths = DEFAULT_DENY_WRITE.map((p) => join(this.projectDir, p));
|
|
85
|
+
}
|
|
86
|
+
addRoot(root) {
|
|
87
|
+
this.allowedRoots.push(resolve(root));
|
|
88
|
+
}
|
|
89
|
+
// Add custom deny-write paths
|
|
90
|
+
addDenyWrite(path) {
|
|
91
|
+
this.denyWritePaths.push(resolve(path));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check whether a path is in the deny-write list.
|
|
95
|
+
* denyWrite has the highest priority — even if the path is within an allowed root, writes are still denied.
|
|
96
|
+
*/
|
|
97
|
+
checkDenyWrite(filePath) {
|
|
98
|
+
const absolute = resolve(filePath);
|
|
99
|
+
for (const denied of this.denyWritePaths) {
|
|
100
|
+
if (absolute.startsWith(denied)) {
|
|
101
|
+
return {
|
|
102
|
+
effect: "deny",
|
|
103
|
+
reason: `Path ${filePath} is in deny-write list`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
check(filePath) {
|
|
110
|
+
const absolute = resolve(filePath);
|
|
111
|
+
for (const root of this.allowedRoots) {
|
|
112
|
+
if (absolute.startsWith(root)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
effect: "deny",
|
|
118
|
+
reason: `Path ${filePath} is outside allowed directories`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function globMatch(pattern, content) {
|
|
123
|
+
const re = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
124
|
+
try {
|
|
125
|
+
return new RegExp(re).test(content);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log.error({ err }, "permissions operation failed");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
var RULE_RE = /^(\w+)\((.+)\)$/;
|
|
132
|
+
function loadRulesFile(path) {
|
|
133
|
+
let data;
|
|
134
|
+
try {
|
|
135
|
+
data = readFileSync(path, "utf-8");
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.code !== "ENOENT") {
|
|
138
|
+
log.error({ err }, "permissions operation failed");
|
|
139
|
+
}
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const YamlEntrySchema = z.object({
|
|
143
|
+
rule: z.string().optional(),
|
|
144
|
+
effect: z.string().optional()
|
|
145
|
+
});
|
|
146
|
+
let yamlData;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = yaml.load(data);
|
|
149
|
+
yamlData = parse(z.array(YamlEntrySchema), parsed);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log.error({ err }, "permissions operation failed");
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const rules = [];
|
|
155
|
+
for (const entry of yamlData) {
|
|
156
|
+
if (entry.effect !== "allow" && entry.effect !== "deny" && entry.effect !== "ask") {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const m = RULE_RE.exec((entry.rule ?? "").trim());
|
|
160
|
+
if (!m) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
rules.push({ tool: m[1], pattern: m[2], effect: entry.effect });
|
|
164
|
+
}
|
|
165
|
+
return rules;
|
|
166
|
+
}
|
|
167
|
+
function evaluateRules(rules, toolName, content) {
|
|
168
|
+
let hit = null;
|
|
169
|
+
for (const r of rules) {
|
|
170
|
+
if (r.tool !== toolName && r.tool !== "*") {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!globMatch(r.pattern, content)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (r.effect === "deny") {
|
|
177
|
+
return "deny";
|
|
178
|
+
}
|
|
179
|
+
if (r.effect === "ask") {
|
|
180
|
+
hit = "ask";
|
|
181
|
+
} else {
|
|
182
|
+
hit ??= "allow";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return hit;
|
|
186
|
+
}
|
|
187
|
+
var RuleEngine = class {
|
|
188
|
+
userPath;
|
|
189
|
+
projectPath;
|
|
190
|
+
localPath;
|
|
191
|
+
cache = /* @__PURE__ */ new Map();
|
|
192
|
+
constructor(workDir) {
|
|
193
|
+
this.userPath = join(homedir(), ".swifty", "permissions.yaml");
|
|
194
|
+
this.projectPath = join(workDir, ".swifty", "permissions.yaml");
|
|
195
|
+
this.localPath = join(workDir, ".swifty", "permissions.local.yaml");
|
|
196
|
+
}
|
|
197
|
+
// Read a single rules file; skips disk I/O and parsing on cache hit.
|
|
198
|
+
rulesFor(path) {
|
|
199
|
+
let st;
|
|
200
|
+
try {
|
|
201
|
+
st = statSync(path, { bigint: true });
|
|
202
|
+
} catch {
|
|
203
|
+
this.cache.delete(path);
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const cached = this.cache.get(path);
|
|
207
|
+
if (cached?.mtimeNs === st.mtimeNs && cached.size === st.size) {
|
|
208
|
+
return cached.rules;
|
|
209
|
+
}
|
|
210
|
+
const rules = loadRulesFile(path);
|
|
211
|
+
this.cache.set(path, { mtimeNs: st.mtimeNs, size: st.size, rules });
|
|
212
|
+
return rules;
|
|
213
|
+
}
|
|
214
|
+
// Return the merged snapshot of all three rules files. Reuses the previous
|
|
215
|
+
// parse result when files are unchanged; re-reads only on change, so edits
|
|
216
|
+
// take effect on the next evaluation without redundant parsing. One snapshot
|
|
217
|
+
// is taken per tool call and shared across sub-command checks.
|
|
218
|
+
snapshot() {
|
|
219
|
+
return [this.userPath, this.projectPath, this.localPath].flatMap((p) => this.rulesFor(p));
|
|
220
|
+
}
|
|
221
|
+
// Take a snapshot then adjudicate: reuses the previous parse result when
|
|
222
|
+
// files are unchanged; a freshly written "allow always" rule takes effect
|
|
223
|
+
// immediately. Priority is deny > ask > allow regardless of which layer or
|
|
224
|
+
// line a rule resides on, so a deny cannot be overridden by an allow from
|
|
225
|
+
// another layer. Returns null when no rule matches.
|
|
226
|
+
evaluate(toolName, content) {
|
|
227
|
+
return evaluateRules(this.snapshot(), toolName, content);
|
|
228
|
+
}
|
|
229
|
+
// Persists a rule to the project-local YAML file in the `Tool(pattern)`
|
|
230
|
+
// format so "allow always" survives a restart.
|
|
231
|
+
appendLocalRule(rule) {
|
|
232
|
+
mkdirSync(dirname(this.localPath), { recursive: true });
|
|
233
|
+
const rules = loadRulesFile(this.localPath);
|
|
234
|
+
const exists = rules.some(
|
|
235
|
+
(r) => r.tool === rule.tool && r.pattern === rule.pattern && r.effect === rule.effect
|
|
236
|
+
);
|
|
237
|
+
if (exists) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
rules.push(rule);
|
|
241
|
+
const entries = rules.map((r) => ({
|
|
242
|
+
rule: `${r.tool}(${r.pattern})`,
|
|
243
|
+
effect: r.effect
|
|
244
|
+
}));
|
|
245
|
+
writeFileSync(this.localPath, yaml.dump(entries), "utf-8");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
function detectDangerous(command) {
|
|
249
|
+
for (const p of DANGEROUS_PATTERNS) {
|
|
250
|
+
if (p.re.test(command)) {
|
|
251
|
+
return p.reason;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
function isSafeCommand(command) {
|
|
257
|
+
const trimmed = command.trim();
|
|
258
|
+
if (trimmed.includes(">") || trimmed.includes("|") || trimmed.includes(";") || trimmed.includes("&&") || trimmed.includes("$(") || trimmed.includes("`")) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return SAFE_PREFIXES.some(
|
|
262
|
+
(prefix) => trimmed === prefix || trimmed.startsWith(prefix + " ") || trimmed.startsWith(prefix + " ")
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
function modeDecide(mode, category) {
|
|
266
|
+
switch (mode) {
|
|
267
|
+
case "bypassPermissions":
|
|
268
|
+
return "allow";
|
|
269
|
+
case "plan":
|
|
270
|
+
return category === "read" ? "allow" : "ask";
|
|
271
|
+
case "acceptEdits":
|
|
272
|
+
return category === "command" ? "ask" : "allow";
|
|
273
|
+
case "default":
|
|
274
|
+
default:
|
|
275
|
+
return category === "read" ? "allow" : "ask";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
var PermissionChecker = class {
|
|
279
|
+
mode;
|
|
280
|
+
planFilePath = "";
|
|
281
|
+
// Sandbox mode: when enabled, command-category tools run through OS sandbox isolation, with optional auto-allow
|
|
282
|
+
sandboxEnabled = false;
|
|
283
|
+
sandboxAutoAllow = false;
|
|
284
|
+
sandbox;
|
|
285
|
+
ruleEngine;
|
|
286
|
+
constructor(workDir, mode = "default") {
|
|
287
|
+
this.mode = mode;
|
|
288
|
+
this.sandbox = new PathSandbox(workDir);
|
|
289
|
+
this.ruleEngine = new RuleEngine(workDir);
|
|
290
|
+
}
|
|
291
|
+
check(toolName, category, args) {
|
|
292
|
+
const content = extractContent(toolName, args);
|
|
293
|
+
let snapshot = null;
|
|
294
|
+
const rules = () => snapshot ??= this.ruleEngine.snapshot();
|
|
295
|
+
if (this.mode === "plan" && (toolName === "WriteFile" || toolName === "EditFile")) {
|
|
296
|
+
const path = strArg(args, "file_path", "");
|
|
297
|
+
if (path.includes(".swifty/plans/")) {
|
|
298
|
+
return {
|
|
299
|
+
effect: "allow",
|
|
300
|
+
reason: "Plan file write allowed in plan mode"
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (category === "command" && isSafeCommand(content)) {
|
|
305
|
+
return { effect: "allow", reason: "Safe read-only command" };
|
|
306
|
+
}
|
|
307
|
+
const dangerReason = category === "command" ? detectDangerous(content) : "";
|
|
308
|
+
if (dangerReason) {
|
|
309
|
+
return {
|
|
310
|
+
effect: "deny",
|
|
311
|
+
reason: `Dangerous command blocked: ${dangerReason}`
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (this.sandboxEnabled && this.sandboxAutoAllow && toolName === "Bash") {
|
|
315
|
+
const subcommands = strArg(args, "command").split(/\s*(?:&&|\|\||[;|])\s*/).map((s) => s.trim()).filter(Boolean);
|
|
316
|
+
let hasAsk = false;
|
|
317
|
+
for (const sub of subcommands) {
|
|
318
|
+
const ruleResult = evaluateRules(rules(), toolName, sub);
|
|
319
|
+
if (ruleResult === "deny") {
|
|
320
|
+
return { effect: "deny", reason: "Permission rule: deny" };
|
|
321
|
+
}
|
|
322
|
+
if (ruleResult === "ask") {
|
|
323
|
+
hasAsk = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (hasAsk) {
|
|
327
|
+
return {
|
|
328
|
+
effect: "ask",
|
|
329
|
+
reason: "Permission rule: ask (sandbox does not override)"
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
effect: "allow",
|
|
334
|
+
reason: "Sandbox auto-allow: OS sandbox active"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const filePath = strArg(args, "file_path", strArg(args, "path", ""));
|
|
338
|
+
if ((category === "read" || category === "write") && filePath) {
|
|
339
|
+
if (category === "write") {
|
|
340
|
+
const denyDecision = this.sandbox.checkDenyWrite(filePath);
|
|
341
|
+
if (denyDecision) {
|
|
342
|
+
return denyDecision;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const sandboxDecision = this.sandbox.check(filePath);
|
|
346
|
+
if (sandboxDecision && this.mode !== "bypassPermissions") {
|
|
347
|
+
const ruleEffect2 = this.ruleEngine.evaluate(toolName, content);
|
|
348
|
+
if (ruleEffect2) {
|
|
349
|
+
return {
|
|
350
|
+
effect: ruleEffect2,
|
|
351
|
+
reason: `Permission rule: ${ruleEffect2}`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { effect: "ask", reason: sandboxDecision.reason };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const ruleEffect = evaluateRules(rules(), toolName, content);
|
|
358
|
+
if (ruleEffect) {
|
|
359
|
+
return { effect: ruleEffect, reason: `Permission rule: ${ruleEffect}` };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
effect: modeDecide(this.mode, category),
|
|
363
|
+
reason: `Mode: ${this.mode}`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// Allow an extra directory outside the project root. When a background agent
|
|
367
|
+
// needs read/write access to user-level data (e.g., user-level memory dir),
|
|
368
|
+
// the caller declares it explicitly; the sandbox baseline stays at the project root.
|
|
369
|
+
allowExtraRoot(path) {
|
|
370
|
+
this.sandbox.addRoot(path);
|
|
371
|
+
}
|
|
372
|
+
// Persist a scoped "allow always" rule.
|
|
373
|
+
// - File path: parent directory + `/*` (a directory path uses itself + `/*`).
|
|
374
|
+
// - Command: first 1-2 words + `*` so it allows that command family.
|
|
375
|
+
allowAlways(toolName, args) {
|
|
376
|
+
const content = extractContent(toolName, args);
|
|
377
|
+
const isFilePath = toolName === "ReadFile" || toolName === "WriteFile" || toolName === "EditFile";
|
|
378
|
+
let pattern;
|
|
379
|
+
if (isFilePath && content) {
|
|
380
|
+
const abs = resolve(content);
|
|
381
|
+
let isDir = false;
|
|
382
|
+
try {
|
|
383
|
+
isDir = statSync(abs).isDirectory();
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
pattern = join(isDir ? abs : dirname(abs), "*");
|
|
387
|
+
} else {
|
|
388
|
+
const words = content.trim().split(/\s+/).slice(0, 2);
|
|
389
|
+
pattern = words.join(" ") + "*";
|
|
390
|
+
}
|
|
391
|
+
this.ruleEngine.appendLocalRule({
|
|
392
|
+
tool: toolName,
|
|
393
|
+
pattern,
|
|
394
|
+
effect: "allow"
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Generate a human-readable description of the tool action for display in HITL confirmation dialogs.
|
|
399
|
+
* Prioritizes extracting fields defined in contentFields (e.g., command, file_path);
|
|
400
|
+
* falls back to a key:value summary of parameters if no match is found.
|
|
401
|
+
*/
|
|
402
|
+
describeToolAction(toolName, args) {
|
|
403
|
+
const content = extractContent(toolName, args);
|
|
404
|
+
if (content) {
|
|
405
|
+
return content;
|
|
406
|
+
}
|
|
407
|
+
const parts = [];
|
|
408
|
+
for (const [k, v] of Object.entries(args)) {
|
|
409
|
+
let s = String(v);
|
|
410
|
+
if (s.length > 80) {
|
|
411
|
+
s = s.slice(0, 80) + "...";
|
|
412
|
+
}
|
|
413
|
+
parts.push(`${k}: ${s}`);
|
|
414
|
+
}
|
|
415
|
+
return parts.join(", ");
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
extractContent,
|
|
421
|
+
PathSandbox,
|
|
422
|
+
evaluateRules,
|
|
423
|
+
RuleEngine,
|
|
424
|
+
isSafeCommand,
|
|
425
|
+
PermissionChecker
|
|
426
|
+
};
|