enigma-cli 1.63.6 → 1.64.1
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/assets/memory/AGENTS.md +1 -1
- package/assets/memory/CLAUDE.md +1 -1
- package/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/SKILL.md +14 -1
- package/assets/skills/backend-policy/skill.json +5 -5
- package/assets/skills/ci-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/email-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +4 -1
- package/assets/skills/frontend-policy/skill.json +4 -4
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/SKILL.md +2 -0
- package/assets/skills/security-policy/skill.json +4 -4
- package/assets/skills/skill-creator/skill.json +1 -1
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/SKILL.md +1 -0
- package/assets/skills/testing-policy/skill.json +4 -4
- package/assets/skills/validation-policy/skill.json +1 -1
- package/bin/checksums.json +4 -4
- package/dist/guard.js +0 -0
- package/dist/guardrails.js +194 -24
- package/dist/post-edit.js +241 -68
- package/package.json +1 -1
package/dist/guardrails.js
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/guardrails.ts
|
|
4
|
-
import { homedir } from "os";
|
|
4
|
+
import { homedir as homedir2 } from "os";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { execFileSync } from "child_process";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
// src/private-terms.ts
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
function readPrivateTerms() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = JSON.parse(readFileSync(join(process.env.ENIGMA_CONFIG_HOME || homedir(), ".enigma-guard.json"), "utf8"));
|
|
15
|
+
if (!Array.isArray(raw.privateTerms)) return [];
|
|
16
|
+
return raw.privateTerms.filter((t) => typeof t === "string" && t.trim().length >= 3);
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function privateTermMatcher(terms) {
|
|
22
|
+
const anyCase = (ch) => {
|
|
23
|
+
const lo = ch.toLowerCase(), up = ch.toUpperCase();
|
|
24
|
+
return lo === up ? ch.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&") : `[${lo}${up}]`;
|
|
25
|
+
};
|
|
26
|
+
const parts = terms.map((t) => t.trim().split(/[\s_-]+/).filter(Boolean).map((w) => [...w].map(anyCase).join("")).join("[\\s_-]*")).filter(Boolean);
|
|
27
|
+
if (!parts.length) return null;
|
|
28
|
+
return new RegExp(`(?:(?<![\\p{L}\\p{N}])|(?<=\\p{Ll})(?=\\p{Lu}))(?:${parts.join("|")})(?!\\p{Ll})`, "u");
|
|
29
|
+
}
|
|
30
|
+
function hasPrivateTerm(line, re) {
|
|
31
|
+
return re.test(line.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, " "));
|
|
32
|
+
}
|
|
33
|
+
function privateTermLines(text, re) {
|
|
34
|
+
const out = [];
|
|
35
|
+
text.split("\n").forEach((line, i) => {
|
|
36
|
+
if (hasPrivateTerm(line, re)) out.push(i + 1);
|
|
37
|
+
});
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/guardrails.ts
|
|
42
|
+
import { dirname, join as join2, resolve, sep } from "path";
|
|
43
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync2, readdirSync, writeFileSync, statSync, existsSync } from "fs";
|
|
9
44
|
var COMMENT_LINE = /^\s*(\/\/|#|\*|--|<!--|\{?\/\*)/;
|
|
10
45
|
var SKELETON_GUARD_SRC = "\\bif\\s*\\(\\s*(isLoading|isPending|isFetching|loading|pending)\\s*\\)\\s*return\\s+(null\\b|<\\s*\\w*(Spinner|Loader|Loading|CircularProgress)\\b)";
|
|
11
46
|
var SKELETON_SIGNAL_SRC = "skeleton|animate-pulse|shimmer|Suspense|ContentLoader|content-loader|<\\s*Placeholder";
|
|
@@ -1410,6 +1445,35 @@ var BUILTIN_RULES = [
|
|
|
1410
1445
|
severity: "block",
|
|
1411
1446
|
skill: "frontend-policy"
|
|
1412
1447
|
},
|
|
1448
|
+
{
|
|
1449
|
+
id: "fe-raw-img-over-component",
|
|
1450
|
+
label: "A project with an image component uses it",
|
|
1451
|
+
files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro"],
|
|
1452
|
+
excludeFiles: [
|
|
1453
|
+
"*.test.*",
|
|
1454
|
+
"*.spec.*",
|
|
1455
|
+
"**/tests/**",
|
|
1456
|
+
"**/__tests__/**",
|
|
1457
|
+
"**/fixtures/**",
|
|
1458
|
+
"*.stories.*",
|
|
1459
|
+
"**/dist/**",
|
|
1460
|
+
"**/build/**",
|
|
1461
|
+
"**/node_modules/**",
|
|
1462
|
+
"**/vendor/**",
|
|
1463
|
+
"**/emails/**",
|
|
1464
|
+
"dist/**",
|
|
1465
|
+
"build/**",
|
|
1466
|
+
"node_modules/**",
|
|
1467
|
+
"vendor/**",
|
|
1468
|
+
"emails/**"
|
|
1469
|
+
],
|
|
1470
|
+
scope: "file",
|
|
1471
|
+
stage: "diff",
|
|
1472
|
+
fileCheck: "fe-raw-img-over-component",
|
|
1473
|
+
message: "A raw <img> in a project that already has an image component. Every content image goes through ONE component, so loading, sizing, alt text, lazy decoding and the lightbox stay the same everywhere and a fix lands once; an <img> per page is a second, drifting copy of it. Use the component named here. Mark the line `enigma:allow-raw-img` where the element must stay raw - an email template, a tracking pixel, or markup the component cannot render (frontend-policy).",
|
|
1474
|
+
severity: "block",
|
|
1475
|
+
skill: "frontend-policy"
|
|
1476
|
+
},
|
|
1413
1477
|
{
|
|
1414
1478
|
id: "fe-toast-hand-rolled",
|
|
1415
1479
|
label: "Toasts come from the primitive",
|
|
@@ -2220,6 +2284,35 @@ var BUILTIN_RULES = [
|
|
|
2220
2284
|
severity: "block",
|
|
2221
2285
|
skill: "security-policy"
|
|
2222
2286
|
},
|
|
2287
|
+
{
|
|
2288
|
+
id: "sec-private-term",
|
|
2289
|
+
label: "Real people's data stays out of the repository",
|
|
2290
|
+
files: ["**"],
|
|
2291
|
+
excludeFiles: [
|
|
2292
|
+
"**/node_modules/**",
|
|
2293
|
+
"**/dist/**",
|
|
2294
|
+
"**/build/**",
|
|
2295
|
+
"**/vendor/**",
|
|
2296
|
+
"**/.next/**",
|
|
2297
|
+
"node_modules/**",
|
|
2298
|
+
"dist/**",
|
|
2299
|
+
"build/**",
|
|
2300
|
+
"vendor/**",
|
|
2301
|
+
"*.lock",
|
|
2302
|
+
"*.lockb",
|
|
2303
|
+
"package-lock.json",
|
|
2304
|
+
"*.min.js",
|
|
2305
|
+
"*.map"
|
|
2306
|
+
],
|
|
2307
|
+
scope: "file",
|
|
2308
|
+
// DIFF stage like sec-operator-env-leak: it fires on the line being added, the moment
|
|
2309
|
+
// before the value reaches the history for good, and never on an old file it did not write.
|
|
2310
|
+
stage: "diff",
|
|
2311
|
+
fileCheck: "sec-private-term",
|
|
2312
|
+
message: "This line carries one of your private terms - the real name, handle, server or community name of you or of the people you serve (the list in ~/.enigma-guard.json, `enigma config guard-private-terms`). Real data never goes into a tracked file, and tests, fixtures, examples and docs use ANONYMOUS data: invent the values (`Player One`, `alice@example.test`, `Test Server`) and, when a real log or payload shape matters, reproduce the shape with invented values instead of pasting the capture. Being shown a real value to work with is permission to use it, never to publish it (security-policy, testing-policy).",
|
|
2313
|
+
severity: "block",
|
|
2314
|
+
skill: "security-policy"
|
|
2315
|
+
},
|
|
2223
2316
|
// The injection class. Every one but sec-unsafe-deserialization is DIFF stage for the reason the
|
|
2224
2317
|
// stage exists: each has a measured backlog in real repositories (the counts are in guardrails.md),
|
|
2225
2318
|
// so at the edit stage it would block unrelated edits to a file that already carried the shape.
|
|
@@ -2689,7 +2782,7 @@ var PROJECT_CHECKS = {
|
|
|
2689
2782
|
"ts-relational-no-prisma": (root) => {
|
|
2690
2783
|
const pkg = readPkgDeps(root);
|
|
2691
2784
|
if (!pkg) return false;
|
|
2692
|
-
const hasTs = "typescript" in pkg || existsSync(
|
|
2785
|
+
const hasTs = "typescript" in pkg || existsSync(join2(root, "tsconfig.json"));
|
|
2693
2786
|
if (!hasTs) return false;
|
|
2694
2787
|
const relational = ["typeorm", "sequelize", "knex", "drizzle-orm", "pg", "mysql", "mysql2", "better-sqlite3", "@mikro-orm/core"];
|
|
2695
2788
|
if (!relational.some((d) => d in pkg)) return false;
|
|
@@ -2726,6 +2819,7 @@ var FILE_CHECKS = {
|
|
|
2726
2819
|
"ts-alias-deep-relative": (content, file) => deepRelativeImports(content, file),
|
|
2727
2820
|
"ts-alias-paths": (content, file) => missingPathAlias(content, file),
|
|
2728
2821
|
"sec-operator-env-leak": (content) => operatorHomePathLeak(content),
|
|
2822
|
+
"sec-private-term": (content) => privateTermLeak(content),
|
|
2729
2823
|
"fe-new-password-affordance-on-signin": (content) => newPasswordAffordanceOnSignIn(content),
|
|
2730
2824
|
"fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
|
|
2731
2825
|
"fe-selection-hand-rolled": (content) => handRolledSelection(content),
|
|
@@ -2735,6 +2829,7 @@ var FILE_CHECKS = {
|
|
|
2735
2829
|
"sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
|
|
2736
2830
|
"fe-select-hand-rolled": (content) => handRolledSelect(content),
|
|
2737
2831
|
"fe-native-select-over-primitive": (content) => nativeSelectOverPrimitive(content),
|
|
2832
|
+
"fe-raw-img-over-component": (content, file) => rawImgOverComponent(content, file),
|
|
2738
2833
|
"fe-toast-hand-rolled": (content) => handRolledToast(content),
|
|
2739
2834
|
"fe-palette-hand-rolled": (content) => handRolledPalette(content),
|
|
2740
2835
|
"sec-sql-built-from-values": (content, file) => sqlBuiltFromValues(content, file),
|
|
@@ -2796,7 +2891,7 @@ function applyFixes(file, findings, stage = "edit") {
|
|
|
2796
2891
|
if (!fixable.length) return { fixed: [], remaining: findings };
|
|
2797
2892
|
let content;
|
|
2798
2893
|
try {
|
|
2799
|
-
content =
|
|
2894
|
+
content = readFileSync2(file, "utf8");
|
|
2800
2895
|
} catch {
|
|
2801
2896
|
return { fixed: [], remaining: findings };
|
|
2802
2897
|
}
|
|
@@ -3203,6 +3298,70 @@ function nativeSelectOverPrimitive(content) {
|
|
|
3203
3298
|
}
|
|
3204
3299
|
return [];
|
|
3205
3300
|
}
|
|
3301
|
+
var RAW_IMG = /<img\b/;
|
|
3302
|
+
var ALLOW_RAW_IMG = /enigma:allow-raw-img/;
|
|
3303
|
+
var DECORATIVE_IMG = /alt=(?:""|''|\{["']{2}\})|\.svg\b|\b(?:logo|icon|flag|favicon|sprite)/i;
|
|
3304
|
+
var IMAGE_COMPONENT_FILE = /(?:^|\/)(?:image|img|picture)\.(?:tsx|jsx|vue|svelte|astro)$/i;
|
|
3305
|
+
var DEFINES_IMAGE = /\b(?:function|const|class)\s+(?:Image|Img|Picture)\b/;
|
|
3306
|
+
var LOCAL_IMAGE_COMPONENTS = ["src/components", "components", "src/components/ui", "components/ui", "src/lib/components", "app/components"];
|
|
3307
|
+
var imageComponentCache = /* @__PURE__ */ new Map();
|
|
3308
|
+
function projectImageComponent(root) {
|
|
3309
|
+
const cached = imageComponentCache.get(root);
|
|
3310
|
+
if (cached !== void 0) return cached;
|
|
3311
|
+
let deps = {};
|
|
3312
|
+
try {
|
|
3313
|
+
deps = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8")).dependencies ?? {};
|
|
3314
|
+
} catch {
|
|
3315
|
+
}
|
|
3316
|
+
let found = null;
|
|
3317
|
+
if ("@enigmax/primitives" in deps) found = "`Image` from @enigmax/primitives (`enigma add image`)";
|
|
3318
|
+
else if ("next" in deps) found = "`Image` from next/image";
|
|
3319
|
+
else if ("astro" in deps) found = "`Image` from astro:assets";
|
|
3320
|
+
else if ("@nuxt/image" in deps) found = "`NuxtImg` from @nuxt/image";
|
|
3321
|
+
else {
|
|
3322
|
+
for (const dir of LOCAL_IMAGE_COMPONENTS) {
|
|
3323
|
+
let names = [];
|
|
3324
|
+
try {
|
|
3325
|
+
names = readdirSync(join2(root, dir));
|
|
3326
|
+
} catch {
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
const hit = names.find((n) => /^(?:image|img|picture)\.(?:tsx|jsx|vue|svelte|astro)$/i.test(n));
|
|
3330
|
+
if (hit) {
|
|
3331
|
+
found = `the project's own \`${dir}/${hit}\``;
|
|
3332
|
+
break;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
imageComponentCache.set(root, found);
|
|
3337
|
+
return found;
|
|
3338
|
+
}
|
|
3339
|
+
function imgElementAt(lines, start) {
|
|
3340
|
+
const parts = [];
|
|
3341
|
+
for (let i = start; i < lines.length && i < start + 30; i++) {
|
|
3342
|
+
const text = i === start ? lines[i].slice(lines[i].search(RAW_IMG)) : lines[i];
|
|
3343
|
+
parts.push(text);
|
|
3344
|
+
if (/(?<!=)>/.test(text)) break;
|
|
3345
|
+
}
|
|
3346
|
+
return parts.join("\n");
|
|
3347
|
+
}
|
|
3348
|
+
function rawImgOverComponent(content, file) {
|
|
3349
|
+
const norm = file.replace(/\\/g, "/");
|
|
3350
|
+
if (!RAW_IMG.test(content) || IMAGE_COMPONENT_FILE.test(norm) || DEFINES_IMAGE.test(content)) return [];
|
|
3351
|
+
const root = findProjectRoot(file);
|
|
3352
|
+
const component = root ? projectImageComponent(root) : null;
|
|
3353
|
+
if (!component) return [];
|
|
3354
|
+
const lines = content.split("\n");
|
|
3355
|
+
const hits = [];
|
|
3356
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3357
|
+
const line = lines[i];
|
|
3358
|
+
if (!RAW_IMG.test(line) || COMMENT_LINE.test(line) || allowedAt(lines, i, ALLOW_RAW_IMG)) continue;
|
|
3359
|
+
const element = imgElementAt(lines, i);
|
|
3360
|
+
if (DECORATIVE_IMG.test(element) || ALLOW_RAW_IMG.test(element)) continue;
|
|
3361
|
+
hits.push({ line: i + 1, detail: `this project already has ${component}` });
|
|
3362
|
+
}
|
|
3363
|
+
return hits;
|
|
3364
|
+
}
|
|
3206
3365
|
var TOAST_QUEUE = /\bset(?:Toasts?|Snackbars?)\b|\btoasts\.(?:map|filter|slice)\b|\btoastQueue\b/;
|
|
3207
3366
|
var TOAST_TIMER = /setTimeout\(/;
|
|
3208
3367
|
var TOAST_MITIGATED = /@enigmax\/primitives|createNotifications|useNotifications|<Toaster|ToastT\b|ToasterProps\b|["']sonner["']|react-hot-toast|react-toastify|notistack|@radix-ui|@mui\/material|@chakra-ui|@mantine|antd|enigma:allow-hand-rolled-toast/;
|
|
@@ -3307,11 +3466,11 @@ function nearestTsconfig(file) {
|
|
|
3307
3466
|
return cached;
|
|
3308
3467
|
}
|
|
3309
3468
|
seen.push(dir);
|
|
3310
|
-
const candidate =
|
|
3469
|
+
const candidate = join2(dir, "tsconfig.json");
|
|
3311
3470
|
if (existsSync(candidate)) {
|
|
3312
3471
|
let found = null;
|
|
3313
3472
|
try {
|
|
3314
|
-
found = { dir, text:
|
|
3473
|
+
found = { dir, text: readFileSync2(candidate, "utf8") };
|
|
3315
3474
|
} catch {
|
|
3316
3475
|
found = null;
|
|
3317
3476
|
}
|
|
@@ -3374,7 +3533,7 @@ function deepRelativeImports(content, file) {
|
|
|
3374
3533
|
function missingPathAlias(content, file) {
|
|
3375
3534
|
if (/["'](?:paths|extends)["']\s*:/.test(content)) return [];
|
|
3376
3535
|
const dir = dirname(resolve(file));
|
|
3377
|
-
const src = ["src", "app", "lib"].find((d) => existsSync(
|
|
3536
|
+
const src = ["src", "app", "lib"].find((d) => existsSync(join2(dir, d)));
|
|
3378
3537
|
if (!src) return [];
|
|
3379
3538
|
const anchor = content.split("\n").findIndex((l) => /["']compilerOptions["']/.test(l));
|
|
3380
3539
|
return [{ line: anchor === -1 ? 1 : anchor + 1, detail: `no alias for ./${src}` }];
|
|
@@ -3383,7 +3542,7 @@ var NAMED_IMPORT = /^import[ \t]+(?:[\w$]+[ \t]*,[ \t]*)?(?:type[ \t]+)?\{([^}]*
|
|
|
3383
3542
|
var GENERIC_ACCOUNTS = /* @__PURE__ */ new Set(["runner", "root", "ubuntu", "debian", "vagrant", "node", "user", "users", "developer", "ec2-user", "codespace", "gitpod", "jenkins", "circleci", "travis", "docker", "app"]);
|
|
3384
3543
|
function operatorHome() {
|
|
3385
3544
|
const env = process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME;
|
|
3386
|
-
return env?.trim() ||
|
|
3545
|
+
return env?.trim() || homedir2();
|
|
3387
3546
|
}
|
|
3388
3547
|
function operatorHomePathLeak(content) {
|
|
3389
3548
|
const re = operatorHomeRegex("i");
|
|
@@ -3395,6 +3554,15 @@ function operatorHomePathLeak(content) {
|
|
|
3395
3554
|
}
|
|
3396
3555
|
return out;
|
|
3397
3556
|
}
|
|
3557
|
+
var privateTermCache;
|
|
3558
|
+
function privateTermLeak(content) {
|
|
3559
|
+
const terms = readPrivateTerms();
|
|
3560
|
+
const key = terms.join("\n");
|
|
3561
|
+
if (privateTermCache?.key !== key) privateTermCache = { key, re: privateTermMatcher(terms) };
|
|
3562
|
+
const re = privateTermCache.re;
|
|
3563
|
+
if (re === null) return [];
|
|
3564
|
+
return privateTermLines(content, re).map((line) => ({ line, detail: "a private term from your list" }));
|
|
3565
|
+
}
|
|
3398
3566
|
function redactOperatorHome(text) {
|
|
3399
3567
|
const re = operatorHomeRegex("gi");
|
|
3400
3568
|
return re === null ? text : text.replace(re, "~");
|
|
@@ -3798,7 +3966,7 @@ function wideNamedImports(content, max) {
|
|
|
3798
3966
|
}
|
|
3799
3967
|
function readPkgDeps(root) {
|
|
3800
3968
|
try {
|
|
3801
|
-
const pkg = JSON.parse(
|
|
3969
|
+
const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
|
|
3802
3970
|
return { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies };
|
|
3803
3971
|
} catch {
|
|
3804
3972
|
return null;
|
|
@@ -3820,7 +3988,7 @@ function globRe(glob, ignoreCase = false) {
|
|
|
3820
3988
|
return re;
|
|
3821
3989
|
}
|
|
3822
3990
|
function guardrailsConfigPath() {
|
|
3823
|
-
return process.env.ENIGMA_GUARDRAILS_CONFIG ||
|
|
3991
|
+
return process.env.ENIGMA_GUARDRAILS_CONFIG || join2(homedir2(), ".enigma-guardrails.json");
|
|
3824
3992
|
}
|
|
3825
3993
|
function isValidRule(r) {
|
|
3826
3994
|
const x = r;
|
|
@@ -3834,7 +4002,7 @@ function loadRules() {
|
|
|
3834
4002
|
let disabled = [];
|
|
3835
4003
|
let custom = [];
|
|
3836
4004
|
try {
|
|
3837
|
-
const raw = JSON.parse(
|
|
4005
|
+
const raw = JSON.parse(readFileSync2(guardrailsConfigPath(), "utf8"));
|
|
3838
4006
|
if (Array.isArray(raw.disabled)) disabled = raw.disabled.filter((s) => typeof s === "string");
|
|
3839
4007
|
if (Array.isArray(raw.rules)) custom = raw.rules.filter(isValidRule);
|
|
3840
4008
|
} catch {
|
|
@@ -3845,10 +4013,10 @@ function loadRules() {
|
|
|
3845
4013
|
function findProjectRoot(file) {
|
|
3846
4014
|
let dir = dirname(resolve(file));
|
|
3847
4015
|
for (let i = 0; i < 40; i++) {
|
|
3848
|
-
const isProj = existsSync(
|
|
4016
|
+
const isProj = existsSync(join2(dir, "package.json")) || existsSync(join2(dir, ".enigma.json"));
|
|
3849
4017
|
let hasGit = false;
|
|
3850
4018
|
try {
|
|
3851
|
-
hasGit = statSync(
|
|
4019
|
+
hasGit = statSync(join2(dir, ".git")).isDirectory();
|
|
3852
4020
|
} catch {
|
|
3853
4021
|
}
|
|
3854
4022
|
if (isProj || hasGit) return dir;
|
|
@@ -3914,7 +4082,7 @@ function formatFindings(findings) {
|
|
|
3914
4082
|
function checkPath(file, stage = "edit") {
|
|
3915
4083
|
let content;
|
|
3916
4084
|
try {
|
|
3917
|
-
content =
|
|
4085
|
+
content = readFileSync2(file, "utf8");
|
|
3918
4086
|
} catch {
|
|
3919
4087
|
return [];
|
|
3920
4088
|
}
|
|
@@ -3953,7 +4121,7 @@ function repairableDiffFindings(file) {
|
|
|
3953
4121
|
if (!changed) return [];
|
|
3954
4122
|
let content;
|
|
3955
4123
|
try {
|
|
3956
|
-
content =
|
|
4124
|
+
content = readFileSync2(file, "utf8");
|
|
3957
4125
|
} catch {
|
|
3958
4126
|
return [];
|
|
3959
4127
|
}
|
|
@@ -3967,7 +4135,7 @@ function repairableDiffFindings(file) {
|
|
|
3967
4135
|
function runGuardrailsHook(payload) {
|
|
3968
4136
|
let file;
|
|
3969
4137
|
try {
|
|
3970
|
-
file = JSON.parse(payload ??
|
|
4138
|
+
file = JSON.parse(payload ?? readFileSync2(0, "utf8"))?.tool_input?.file_path;
|
|
3971
4139
|
} catch {
|
|
3972
4140
|
}
|
|
3973
4141
|
if (!file || typeof file !== "string") return 0;
|
|
@@ -4354,16 +4522,16 @@ function pathJoinedFromInput(content, file) {
|
|
|
4354
4522
|
if (!python && /^\s*["']use client["']/m.test(content)) return [];
|
|
4355
4523
|
if (PATH_CONTAINED.test(content)) return [];
|
|
4356
4524
|
const input = python ? REQUEST_INPUT_PY : REQUEST_INPUT_JS;
|
|
4357
|
-
const
|
|
4525
|
+
const join3 = python ? PATH_JOIN_PY : PATH_JOIN_JS;
|
|
4358
4526
|
const lines = content.split("\n");
|
|
4359
4527
|
const names = namesFromInput(lines, input);
|
|
4360
4528
|
const out = [];
|
|
4361
4529
|
for (let i = 0; i < lines.length; i++) {
|
|
4362
4530
|
const text = lines[i];
|
|
4363
4531
|
if (auditSkipLine(text)) continue;
|
|
4364
|
-
|
|
4532
|
+
join3.lastIndex = 0;
|
|
4365
4533
|
let m;
|
|
4366
|
-
while (m =
|
|
4534
|
+
while (m = join3.exec(text)) {
|
|
4367
4535
|
const parts = (m[1] ?? m[2] ?? "").split(",").slice(1).map((s) => s.trim());
|
|
4368
4536
|
if (parts.some((p) => input.test(p) || names.has(p) || names.has(p.replace(/^`\$\{(\w+)\}.*`$/, "$1")))) {
|
|
4369
4537
|
out.push({ line: i + 1, detail: text.trim().slice(0, 120) });
|
|
@@ -4730,7 +4898,7 @@ var LEDGER_MAX_BYTES = 512 * 1024;
|
|
|
4730
4898
|
var LEDGER_KEEP = 2e3;
|
|
4731
4899
|
var LEDGER_KEEP_REPLY = 200;
|
|
4732
4900
|
function ledgerPath() {
|
|
4733
|
-
return process.env.ENIGMA_GUARDRAILS_LOG ||
|
|
4901
|
+
return process.env.ENIGMA_GUARDRAILS_LOG || join2(homedir2(), ".enigma", "guardrail-log.jsonl");
|
|
4734
4902
|
}
|
|
4735
4903
|
function ledgerKey(rule, outcome, file, line) {
|
|
4736
4904
|
return JSON.stringify([rule, outcome, file, line ?? null]);
|
|
@@ -4766,7 +4934,7 @@ function recordFindings(findings, outcome, stage = "edit") {
|
|
|
4766
4934
|
} catch {
|
|
4767
4935
|
}
|
|
4768
4936
|
if (size > LEDGER_MAX_BYTES) {
|
|
4769
|
-
const kept = rotateLedger(
|
|
4937
|
+
const kept = rotateLedger(readFileSync2(path, "utf8"));
|
|
4770
4938
|
writeFileSync(path, kept.length ? `${kept.join("\n")}
|
|
4771
4939
|
` : "");
|
|
4772
4940
|
}
|
|
@@ -4801,7 +4969,7 @@ function rotateLedger(text) {
|
|
|
4801
4969
|
function eachLedgerEntry(sinceDays, visit) {
|
|
4802
4970
|
let text;
|
|
4803
4971
|
try {
|
|
4804
|
-
text =
|
|
4972
|
+
text = readFileSync2(ledgerPath(), "utf8");
|
|
4805
4973
|
} catch {
|
|
4806
4974
|
return;
|
|
4807
4975
|
}
|
|
@@ -4871,7 +5039,7 @@ function runGuardrailsScan(all) {
|
|
|
4871
5039
|
for (const file of files) {
|
|
4872
5040
|
let content;
|
|
4873
5041
|
try {
|
|
4874
|
-
content =
|
|
5042
|
+
content = readFileSync2(file, "utf8");
|
|
4875
5043
|
} catch {
|
|
4876
5044
|
continue;
|
|
4877
5045
|
}
|
|
@@ -4947,9 +5115,11 @@ export {
|
|
|
4947
5115
|
pageAwaitWithoutBoundary,
|
|
4948
5116
|
pathJoinedFromInput,
|
|
4949
5117
|
poweredByBuzzword,
|
|
5118
|
+
privateTermLeak,
|
|
4950
5119
|
purpleGradient,
|
|
4951
5120
|
queryInLoop,
|
|
4952
5121
|
radixContentWithoutPortal,
|
|
5122
|
+
rawImgOverComponent,
|
|
4953
5123
|
readLedger,
|
|
4954
5124
|
readReplyLedger,
|
|
4955
5125
|
recordFindings,
|