failproofai 0.0.2-beta.3 → 0.0.2-beta.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +3 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07k6eu-._.js → [root-of-the-server]__00_.atk._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kfv9fw._.js → [root-of-the-server]__0gw4qdj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{0p_fpyfmmohnx.js → 040il49xqyq~j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t94r_mk0s7e4.js → 0d-hv1uc827s6.js} +2 -2
- package/.next/standalone/.next/static/chunks/{139~00zc9.u7s.js → 0ezymnwrt2x6i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{01haq0a3zrx0v.js → 0gleuaabeolm~.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0.jo.465b6_k..js → 0issdwvmb81z_.js} +1 -1
- package/.next/standalone/.next/static/chunks/0jf9lx3rkmqx_.css +1 -0
- package/.next/standalone/.next/static/chunks/{0mq7ze1vkeo1p.js → 0odv81fzkn6u~.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0qwyj3m400l_g.js → 10uhv8kh~ad6m.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0a6xi1a8f_qlp.js → 14ee68i9dy9b3.js} +1 -1
- package/.next/standalone/app/components/session-hooks-panel.tsx +14 -1
- package/.next/standalone/app/policies/hooks-client.tsx +14 -1
- package/.next/standalone/components/navbar.tsx +5 -0
- package/.next/standalone/dist/cli.mjs +54 -21
- package/.next/standalone/next.config.ts +5 -3
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/.next/standalone/src/hooks/builtin-policies.ts +54 -5
- package/.next/standalone/src/hooks/handler.ts +1 -0
- package/.next/standalone/src/hooks/hook-activity-store.ts +6 -1
- package/.next/standalone/src/hooks/policy-evaluator.ts +16 -13
- package/dist/cli.mjs +54 -21
- package/package.json +1 -1
- package/src/hooks/builtin-policies.ts +54 -5
- package/src/hooks/handler.ts +1 -0
- package/src/hooks/hook-activity-store.ts +6 -1
- package/src/hooks/policy-evaluator.ts +16 -13
- package/.next/standalone/.next/static/chunks/08f78tecvx61l.css +0 -1
- /package/.next/standalone/.next/static/{7fR022u1Sj-s5MfKO1q9Y → p7b7Yk0VOBDjbtr1aHDyV}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7fR022u1Sj-s5MfKO1q9Y → p7b7Yk0VOBDjbtr1aHDyV}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7fR022u1Sj-s5MfKO1q9Y → p7b7Yk0VOBDjbtr1aHDyV}/_ssgManifest.js +0 -0
|
@@ -496,7 +496,8 @@ function rmTargetIsAllowed(cmd, allowPaths) {
|
|
|
496
496
|
if (rmIdx < 0)
|
|
497
497
|
continue;
|
|
498
498
|
const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
|
|
499
|
-
|
|
499
|
+
const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
|
|
500
|
+
if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some((f) => /^--recursive$/i.test(f)))
|
|
500
501
|
continue;
|
|
501
502
|
const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
|
|
502
503
|
for (const target of pathArgs) {
|
|
@@ -521,7 +522,10 @@ function blockRmRf(ctx) {
|
|
|
521
522
|
if (ctx.toolName !== "Bash")
|
|
522
523
|
return allow();
|
|
523
524
|
const cmd = getCommand(ctx);
|
|
524
|
-
const hasDestructivePath =
|
|
525
|
+
const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
|
|
526
|
+
const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
|
|
527
|
+
return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
|
|
528
|
+
});
|
|
525
529
|
if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
|
|
526
530
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
527
531
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
@@ -531,7 +535,10 @@ function blockRmRf(ctx) {
|
|
|
531
535
|
if (hasDestructivePath && /\brm\b/.test(cmd)) {
|
|
532
536
|
const tokens = parseArgvTokens(cmd);
|
|
533
537
|
const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
|
|
534
|
-
|
|
538
|
+
const longFlags = tokens.filter((t) => /^--/.test(t));
|
|
539
|
+
const hasRecursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
|
|
540
|
+
const hasForce = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
|
|
541
|
+
if (hasRecursive && hasForce) {
|
|
535
542
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
536
543
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
537
544
|
return allow();
|
|
@@ -847,6 +854,20 @@ function requirePrBeforeStop(ctx) {
|
|
|
847
854
|
const branch = getCurrentBranch(cwd);
|
|
848
855
|
if (!branch || branch === "HEAD")
|
|
849
856
|
return allow("Detached HEAD, skipping PR check.");
|
|
857
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
858
|
+
if (branch === baseBranch) {
|
|
859
|
+
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
863
|
+
if (!ahead) {
|
|
864
|
+
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
865
|
+
}
|
|
866
|
+
const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
867
|
+
if (!diff) {
|
|
868
|
+
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
869
|
+
}
|
|
870
|
+
} catch {}
|
|
850
871
|
let prJson;
|
|
851
872
|
try {
|
|
852
873
|
prJson = execSync("gh pr view --json number,url,state", {
|
|
@@ -939,7 +960,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
939
960
|
SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
|
|
940
961
|
PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
|
|
941
962
|
ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
942
|
-
ECHO_ENV_RE = /echo\s
|
|
963
|
+
ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
943
964
|
EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
944
965
|
PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
945
966
|
PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
@@ -950,7 +971,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
950
971
|
SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
|
|
951
972
|
PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
952
973
|
RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
953
|
-
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
|
|
974
|
+
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
954
975
|
PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
955
976
|
FORCE_PUSH_RE = /(?:--force|-f\b)/;
|
|
956
977
|
SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
@@ -1272,7 +1293,14 @@ var init_builtin_policies = __esm(() => {
|
|
|
1272
1293
|
match: { events: ["Stop"] },
|
|
1273
1294
|
defaultEnabled: false,
|
|
1274
1295
|
category: "Workflow",
|
|
1275
|
-
beta: true
|
|
1296
|
+
beta: true,
|
|
1297
|
+
params: {
|
|
1298
|
+
baseBranch: {
|
|
1299
|
+
type: "string",
|
|
1300
|
+
description: "Base branch to compare against (default: main)",
|
|
1301
|
+
default: "main"
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1276
1304
|
},
|
|
1277
1305
|
{
|
|
1278
1306
|
name: "require-ci-green-before-stop",
|
|
@@ -1304,7 +1332,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1304
1332
|
};
|
|
1305
1333
|
let instructPolicyName = null;
|
|
1306
1334
|
let instructReason = null;
|
|
1307
|
-
const
|
|
1335
|
+
const allowEntries = [];
|
|
1308
1336
|
for (const policy of policies) {
|
|
1309
1337
|
const schema = POLICY_PARAMS_MAP.get(policy.name);
|
|
1310
1338
|
let ctx;
|
|
@@ -1365,7 +1393,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1365
1393
|
return {
|
|
1366
1394
|
exitCode: 2,
|
|
1367
1395
|
stdout: "",
|
|
1368
|
-
stderr:
|
|
1396
|
+
stderr: reason,
|
|
1369
1397
|
policyName: policy.name,
|
|
1370
1398
|
reason,
|
|
1371
1399
|
decision: "deny"
|
|
@@ -1377,7 +1405,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1377
1405
|
hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
|
|
1378
1406
|
}
|
|
1379
1407
|
if (result.decision === "allow" && result.reason) {
|
|
1380
|
-
|
|
1408
|
+
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
1381
1409
|
}
|
|
1382
1410
|
}
|
|
1383
1411
|
if (instructPolicyName && instructReason) {
|
|
@@ -1406,16 +1434,16 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1406
1434
|
decision: "instruct"
|
|
1407
1435
|
};
|
|
1408
1436
|
}
|
|
1409
|
-
if (
|
|
1410
|
-
const combined =
|
|
1437
|
+
if (allowEntries.length > 0) {
|
|
1438
|
+
const combined = allowEntries.map((e) => e.reason).join(`
|
|
1411
1439
|
`);
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1440
|
+
const policyNames = allowEntries.map((e) => e.policyName);
|
|
1441
|
+
const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
|
|
1442
|
+
const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
|
|
1443
|
+
const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
|
|
1444
|
+
`);
|
|
1445
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + `
|
|
1446
|
+
`, policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1419
1447
|
}
|
|
1420
1448
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
1421
1449
|
}
|
|
@@ -1711,7 +1739,11 @@ function updateStats(entry) {
|
|
|
1711
1739
|
s.totalEvents += 1;
|
|
1712
1740
|
if (entry.decision === "deny")
|
|
1713
1741
|
s.denyCount += 1;
|
|
1714
|
-
if (entry.
|
|
1742
|
+
if (entry.policyNames && entry.policyNames.length > 0) {
|
|
1743
|
+
for (const name of entry.policyNames) {
|
|
1744
|
+
s.policyMap[name] = (s.policyMap[name] ?? 0) + 1;
|
|
1745
|
+
}
|
|
1746
|
+
} else if (entry.policyName) {
|
|
1715
1747
|
s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
|
|
1716
1748
|
}
|
|
1717
1749
|
const tmpPath = join3(storeDir, `stats.json.${process.pid}.tmp`);
|
|
@@ -1731,7 +1763,7 @@ var init_hook_activity_store = __esm(() => {
|
|
|
1731
1763
|
});
|
|
1732
1764
|
|
|
1733
1765
|
// package.json
|
|
1734
|
-
var version2 = "0.0.2-beta.
|
|
1766
|
+
var version2 = "0.0.2-beta.5";
|
|
1735
1767
|
var init_package = () => {};
|
|
1736
1768
|
|
|
1737
1769
|
// src/posthog-key.ts
|
|
@@ -1940,6 +1972,7 @@ async function handleHookEvent(eventType) {
|
|
|
1940
1972
|
eventType,
|
|
1941
1973
|
toolName: parsed.tool_name ?? null,
|
|
1942
1974
|
policyName: result.policyName,
|
|
1975
|
+
policyNames: result.policyNames,
|
|
1943
1976
|
decision: result.decision,
|
|
1944
1977
|
reason: result.reason,
|
|
1945
1978
|
durationMs,
|
|
@@ -2957,7 +2990,7 @@ import { realpathSync as realpathSync2 } from "fs";
|
|
|
2957
2990
|
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
2958
2991
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2959
2992
|
// package.json
|
|
2960
|
-
var version = "0.0.2-beta.
|
|
2993
|
+
var version = "0.0.2-beta.5";
|
|
2961
2994
|
|
|
2962
2995
|
// bin/failproofai.mjs
|
|
2963
2996
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
2
6
|
|
|
3
7
|
const allowedDevOrigins = process.env.FAILPROOFAI_ALLOWED_DEV_ORIGINS
|
|
4
8
|
? process.env.FAILPROOFAI_ALLOWED_DEV_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean)
|
|
@@ -19,9 +23,7 @@ const nextConfig: NextConfig = {
|
|
|
19
23
|
return config;
|
|
20
24
|
},
|
|
21
25
|
env: {
|
|
22
|
-
|
|
23
|
-
// Note: Only use this if you need it on the client side
|
|
24
|
-
// For server-side only, you can access it via process.env.CLAUDE_PROJECTS_PATH
|
|
26
|
+
NEXT_PUBLIC_APP_VERSION: pkg.version,
|
|
25
27
|
},
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.5",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
|
|
|
9
9
|
const hostname = process.env.HOSTNAME || '0.0.0.0'
|
|
10
10
|
|
|
11
11
|
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
|
|
12
|
-
const nextConfig = {"env":{},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
|
|
12
|
+
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.2-beta.5"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
|
|
13
13
|
|
|
14
14
|
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
|
|
15
15
|
|
|
@@ -86,7 +86,7 @@ const PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm
|
|
|
86
86
|
|
|
87
87
|
// protectEnvVars
|
|
88
88
|
const ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
89
|
-
const ECHO_ENV_RE = /echo\s
|
|
89
|
+
const ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
90
90
|
const EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
91
91
|
const PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
92
92
|
const PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
@@ -103,7 +103,7 @@ const PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
|
103
103
|
const RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
104
104
|
|
|
105
105
|
// blockCurlPipeSh
|
|
106
|
-
const CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
|
|
106
|
+
const CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
107
107
|
const PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
108
108
|
|
|
109
109
|
// blockForcePush
|
|
@@ -440,7 +440,8 @@ function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
|
|
|
440
440
|
if (rmIdx < 0) continue;
|
|
441
441
|
// Only validate recursive rm segments — non-recursive rm has no catastrophic-deletion risk
|
|
442
442
|
const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
|
|
443
|
-
|
|
443
|
+
const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
|
|
444
|
+
if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some(f => /^--recursive$/i.test(f))) continue;
|
|
444
445
|
const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
|
|
445
446
|
for (const target of pathArgs) {
|
|
446
447
|
const normalized = target.replace(/\/\*$/, "").replace(/\/+$/, "") || "/";
|
|
@@ -465,7 +466,10 @@ function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
|
|
|
465
466
|
function blockRmRf(ctx: PolicyContext): PolicyResult {
|
|
466
467
|
if (ctx.toolName !== "Bash") return allow();
|
|
467
468
|
const cmd = getCommand(ctx);
|
|
468
|
-
const hasDestructivePath =
|
|
469
|
+
const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
|
|
470
|
+
const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
|
|
471
|
+
return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
|
|
472
|
+
});
|
|
469
473
|
|
|
470
474
|
// Combined flags in one token: rm -rf /, rm -fr /
|
|
471
475
|
if (hasDestructivePath && (
|
|
@@ -481,7 +485,10 @@ function blockRmRf(ctx: PolicyContext): PolicyResult {
|
|
|
481
485
|
if (hasDestructivePath && /\brm\b/.test(cmd)) {
|
|
482
486
|
const tokens = parseArgvTokens(cmd);
|
|
483
487
|
const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
|
|
484
|
-
|
|
488
|
+
const longFlags = tokens.filter((t) => /^--/.test(t));
|
|
489
|
+
const hasRecursive = /r/i.test(shortFlags) || longFlags.some(f => /^--recursive$/i.test(f));
|
|
490
|
+
const hasForce = /f/.test(shortFlags) || longFlags.some(f => /^--force$/i.test(f));
|
|
491
|
+
if (hasRecursive && hasForce) {
|
|
485
492
|
const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
|
|
486
493
|
if (rmTargetIsAllowed(cmd, allowPaths)) return allow();
|
|
487
494
|
return deny("Catastrophic deletion blocked");
|
|
@@ -891,6 +898,41 @@ function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
891
898
|
const branch = getCurrentBranch(cwd);
|
|
892
899
|
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping PR check.");
|
|
893
900
|
|
|
901
|
+
const baseBranch = (ctx.params?.baseBranch as string) ?? "main";
|
|
902
|
+
|
|
903
|
+
// If on the base branch itself, no PR is needed
|
|
904
|
+
if (branch === baseBranch) {
|
|
905
|
+
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Check if branch has diverged from base in any meaningful way
|
|
909
|
+
try {
|
|
910
|
+
const ahead = execFileSync(
|
|
911
|
+
"git",
|
|
912
|
+
["log", `origin/${baseBranch}..HEAD`, "--oneline"],
|
|
913
|
+
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
914
|
+
).trim();
|
|
915
|
+
|
|
916
|
+
if (!ahead) {
|
|
917
|
+
// No commits ahead — branch is fully merged (regular merge / fast-forward)
|
|
918
|
+
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Commits exist but might be from a squash-merged PR.
|
|
922
|
+
// Check actual file diff — if trees are identical, work is already in main.
|
|
923
|
+
const diff = execFileSync(
|
|
924
|
+
"git",
|
|
925
|
+
["diff", "--stat", `origin/${baseBranch}`, "HEAD"],
|
|
926
|
+
{ cwd, encoding: "utf8", timeout: 5000 },
|
|
927
|
+
).trim();
|
|
928
|
+
|
|
929
|
+
if (!diff) {
|
|
930
|
+
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
931
|
+
}
|
|
932
|
+
} catch {
|
|
933
|
+
// origin/{baseBranch} ref missing or git error — fall through to gh pr view
|
|
934
|
+
}
|
|
935
|
+
|
|
894
936
|
// Check if a PR exists for this branch
|
|
895
937
|
let prJson: string;
|
|
896
938
|
try {
|
|
@@ -1282,6 +1324,13 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
|
1282
1324
|
defaultEnabled: false,
|
|
1283
1325
|
category: "Workflow",
|
|
1284
1326
|
beta: true,
|
|
1327
|
+
params: {
|
|
1328
|
+
baseBranch: {
|
|
1329
|
+
type: "string",
|
|
1330
|
+
description: "Base branch to compare against (default: main)",
|
|
1331
|
+
default: "main",
|
|
1332
|
+
},
|
|
1333
|
+
} satisfies PolicyParamsSchema,
|
|
1285
1334
|
},
|
|
1286
1335
|
{
|
|
1287
1336
|
name: "require-ci-green-before-stop",
|
|
@@ -134,6 +134,7 @@ export async function handleHookEvent(eventType: string): Promise<number> {
|
|
|
134
134
|
eventType,
|
|
135
135
|
toolName: (parsed.tool_name as string) ?? null,
|
|
136
136
|
policyName: result.policyName,
|
|
137
|
+
policyNames: result.policyNames,
|
|
137
138
|
decision: result.decision,
|
|
138
139
|
reason: result.reason,
|
|
139
140
|
durationMs,
|
|
@@ -43,6 +43,7 @@ export interface HookActivityEntry {
|
|
|
43
43
|
eventType: string;
|
|
44
44
|
toolName: string | null;
|
|
45
45
|
policyName: string | null;
|
|
46
|
+
policyNames?: string[];
|
|
46
47
|
decision: "allow" | "deny" | "instruct";
|
|
47
48
|
reason: string | null;
|
|
48
49
|
durationMs: number;
|
|
@@ -186,7 +187,11 @@ function updateStats(entry: HookActivityEntry): void {
|
|
|
186
187
|
const s = readStoredStats();
|
|
187
188
|
s.totalEvents += 1;
|
|
188
189
|
if (entry.decision === "deny") s.denyCount += 1;
|
|
189
|
-
if (entry.
|
|
190
|
+
if (entry.policyNames && entry.policyNames.length > 0) {
|
|
191
|
+
for (const name of entry.policyNames) {
|
|
192
|
+
s.policyMap[name] = (s.policyMap[name] ?? 0) + 1;
|
|
193
|
+
}
|
|
194
|
+
} else if (entry.policyName) {
|
|
190
195
|
s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
|
|
191
196
|
}
|
|
192
197
|
// Write atomically: write to a PID-unique temp file then rename — prevents partial reads.
|
|
@@ -13,6 +13,7 @@ export interface EvaluationResult {
|
|
|
13
13
|
stdout: string;
|
|
14
14
|
stderr: string;
|
|
15
15
|
policyName: string | null;
|
|
16
|
+
policyNames?: string[];
|
|
16
17
|
reason: string | null;
|
|
17
18
|
decision: "allow" | "deny" | "instruct";
|
|
18
19
|
}
|
|
@@ -51,8 +52,8 @@ export async function evaluatePolicies(
|
|
|
51
52
|
let instructPolicyName: string | null = null;
|
|
52
53
|
let instructReason: string | null = null;
|
|
53
54
|
|
|
54
|
-
// Track informational messages from allow decisions
|
|
55
|
-
const
|
|
55
|
+
// Track informational messages from allow decisions (with policy attribution)
|
|
56
|
+
const allowEntries: Array<{ policyName: string; reason: string }> = [];
|
|
56
57
|
|
|
57
58
|
for (const policy of policies) {
|
|
58
59
|
// Inject params: merge policyParams[policy.name] over schema defaults
|
|
@@ -123,7 +124,7 @@ export async function evaluatePolicies(
|
|
|
123
124
|
return {
|
|
124
125
|
exitCode: 2,
|
|
125
126
|
stdout: "",
|
|
126
|
-
stderr:
|
|
127
|
+
stderr: reason,
|
|
127
128
|
policyName: policy.name,
|
|
128
129
|
reason,
|
|
129
130
|
decision: "deny",
|
|
@@ -139,7 +140,7 @@ export async function evaluatePolicies(
|
|
|
139
140
|
|
|
140
141
|
// Accumulate informational messages from allow decisions
|
|
141
142
|
if (result.decision === "allow" && result.reason) {
|
|
142
|
-
|
|
143
|
+
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -175,15 +176,17 @@ export async function evaluatePolicies(
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// All policies allowed — pass along any informational messages
|
|
178
|
-
if (
|
|
179
|
-
const combined =
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
if (allowEntries.length > 0) {
|
|
180
|
+
const combined = allowEntries.map((e) => e.reason).join("\n");
|
|
181
|
+
const policyNames = allowEntries.map((e) => e.policyName);
|
|
182
|
+
const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
|
|
183
|
+
const response = supportsHookSpecificOutput
|
|
184
|
+
? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } }
|
|
185
|
+
: { reason: combined };
|
|
186
|
+
const stderrMsg = allowEntries
|
|
187
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
188
|
+
.join("\n");
|
|
189
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + "\n", policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
187
190
|
}
|
|
188
191
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
189
192
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -496,7 +496,8 @@ function rmTargetIsAllowed(cmd, allowPaths) {
|
|
|
496
496
|
if (rmIdx < 0)
|
|
497
497
|
continue;
|
|
498
498
|
const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
|
|
499
|
-
|
|
499
|
+
const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
|
|
500
|
+
if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some((f) => /^--recursive$/i.test(f)))
|
|
500
501
|
continue;
|
|
501
502
|
const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
|
|
502
503
|
for (const target of pathArgs) {
|
|
@@ -521,7 +522,10 @@ function blockRmRf(ctx) {
|
|
|
521
522
|
if (ctx.toolName !== "Bash")
|
|
522
523
|
return allow();
|
|
523
524
|
const cmd = getCommand(ctx);
|
|
524
|
-
const hasDestructivePath =
|
|
525
|
+
const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
|
|
526
|
+
const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
|
|
527
|
+
return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
|
|
528
|
+
});
|
|
525
529
|
if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
|
|
526
530
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
527
531
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
@@ -531,7 +535,10 @@ function blockRmRf(ctx) {
|
|
|
531
535
|
if (hasDestructivePath && /\brm\b/.test(cmd)) {
|
|
532
536
|
const tokens = parseArgvTokens(cmd);
|
|
533
537
|
const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
|
|
534
|
-
|
|
538
|
+
const longFlags = tokens.filter((t) => /^--/.test(t));
|
|
539
|
+
const hasRecursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
|
|
540
|
+
const hasForce = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
|
|
541
|
+
if (hasRecursive && hasForce) {
|
|
535
542
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
536
543
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
537
544
|
return allow();
|
|
@@ -847,6 +854,20 @@ function requirePrBeforeStop(ctx) {
|
|
|
847
854
|
const branch = getCurrentBranch(cwd);
|
|
848
855
|
if (!branch || branch === "HEAD")
|
|
849
856
|
return allow("Detached HEAD, skipping PR check.");
|
|
857
|
+
const baseBranch = ctx.params?.baseBranch ?? "main";
|
|
858
|
+
if (branch === baseBranch) {
|
|
859
|
+
return allow(`On base branch "${baseBranch}", skipping PR check.`);
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
863
|
+
if (!ahead) {
|
|
864
|
+
return allow(`No commits ahead of origin/${baseBranch}, skipping PR check.`);
|
|
865
|
+
}
|
|
866
|
+
const diff = execFileSync("git", ["diff", "--stat", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
867
|
+
if (!diff) {
|
|
868
|
+
return allow(`No file changes compared to origin/${baseBranch}, skipping PR check.`);
|
|
869
|
+
}
|
|
870
|
+
} catch {}
|
|
850
871
|
let prJson;
|
|
851
872
|
try {
|
|
852
873
|
prJson = execSync("gh pr view --json number,url,state", {
|
|
@@ -939,7 +960,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
939
960
|
SCHEMA_ALTER_RE = /\bALTER\s+TABLE\b[\s\S]*\b(?:DROP\s+COLUMN|ADD\s+COLUMN|RENAME\s+(?:COLUMN|TO)|MODIFY\s+COLUMN)\b/i;
|
|
940
961
|
PUBLISH_CMD_RE = /(?:npm\s+publish|bun\s+publish|pnpm\s+publish|yarn\s+npm\s+publish|twine\s+upload|poetry\s+publish|cargo\s+publish|gem\s+push)\b/;
|
|
941
962
|
ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
942
|
-
ECHO_ENV_RE = /echo\s
|
|
963
|
+
ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
943
964
|
EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
944
965
|
PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
945
966
|
PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
@@ -950,7 +971,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
950
971
|
SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
|
|
951
972
|
PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
952
973
|
RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
953
|
-
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
|
|
974
|
+
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
954
975
|
PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
955
976
|
FORCE_PUSH_RE = /(?:--force|-f\b)/;
|
|
956
977
|
SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
@@ -1272,7 +1293,14 @@ var init_builtin_policies = __esm(() => {
|
|
|
1272
1293
|
match: { events: ["Stop"] },
|
|
1273
1294
|
defaultEnabled: false,
|
|
1274
1295
|
category: "Workflow",
|
|
1275
|
-
beta: true
|
|
1296
|
+
beta: true,
|
|
1297
|
+
params: {
|
|
1298
|
+
baseBranch: {
|
|
1299
|
+
type: "string",
|
|
1300
|
+
description: "Base branch to compare against (default: main)",
|
|
1301
|
+
default: "main"
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1276
1304
|
},
|
|
1277
1305
|
{
|
|
1278
1306
|
name: "require-ci-green-before-stop",
|
|
@@ -1304,7 +1332,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1304
1332
|
};
|
|
1305
1333
|
let instructPolicyName = null;
|
|
1306
1334
|
let instructReason = null;
|
|
1307
|
-
const
|
|
1335
|
+
const allowEntries = [];
|
|
1308
1336
|
for (const policy of policies) {
|
|
1309
1337
|
const schema = POLICY_PARAMS_MAP.get(policy.name);
|
|
1310
1338
|
let ctx;
|
|
@@ -1365,7 +1393,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1365
1393
|
return {
|
|
1366
1394
|
exitCode: 2,
|
|
1367
1395
|
stdout: "",
|
|
1368
|
-
stderr:
|
|
1396
|
+
stderr: reason,
|
|
1369
1397
|
policyName: policy.name,
|
|
1370
1398
|
reason,
|
|
1371
1399
|
decision: "deny"
|
|
@@ -1377,7 +1405,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1377
1405
|
hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
|
|
1378
1406
|
}
|
|
1379
1407
|
if (result.decision === "allow" && result.reason) {
|
|
1380
|
-
|
|
1408
|
+
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
1381
1409
|
}
|
|
1382
1410
|
}
|
|
1383
1411
|
if (instructPolicyName && instructReason) {
|
|
@@ -1406,16 +1434,16 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1406
1434
|
decision: "instruct"
|
|
1407
1435
|
};
|
|
1408
1436
|
}
|
|
1409
|
-
if (
|
|
1410
|
-
const combined =
|
|
1437
|
+
if (allowEntries.length > 0) {
|
|
1438
|
+
const combined = allowEntries.map((e) => e.reason).join(`
|
|
1411
1439
|
`);
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1440
|
+
const policyNames = allowEntries.map((e) => e.policyName);
|
|
1441
|
+
const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
|
|
1442
|
+
const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
|
|
1443
|
+
const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
|
|
1444
|
+
`);
|
|
1445
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + `
|
|
1446
|
+
`, policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1419
1447
|
}
|
|
1420
1448
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
1421
1449
|
}
|
|
@@ -1711,7 +1739,11 @@ function updateStats(entry) {
|
|
|
1711
1739
|
s.totalEvents += 1;
|
|
1712
1740
|
if (entry.decision === "deny")
|
|
1713
1741
|
s.denyCount += 1;
|
|
1714
|
-
if (entry.
|
|
1742
|
+
if (entry.policyNames && entry.policyNames.length > 0) {
|
|
1743
|
+
for (const name of entry.policyNames) {
|
|
1744
|
+
s.policyMap[name] = (s.policyMap[name] ?? 0) + 1;
|
|
1745
|
+
}
|
|
1746
|
+
} else if (entry.policyName) {
|
|
1715
1747
|
s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
|
|
1716
1748
|
}
|
|
1717
1749
|
const tmpPath = join3(storeDir, `stats.json.${process.pid}.tmp`);
|
|
@@ -1731,7 +1763,7 @@ var init_hook_activity_store = __esm(() => {
|
|
|
1731
1763
|
});
|
|
1732
1764
|
|
|
1733
1765
|
// package.json
|
|
1734
|
-
var version2 = "0.0.2-beta.
|
|
1766
|
+
var version2 = "0.0.2-beta.5";
|
|
1735
1767
|
var init_package = () => {};
|
|
1736
1768
|
|
|
1737
1769
|
// src/posthog-key.ts
|
|
@@ -1940,6 +1972,7 @@ async function handleHookEvent(eventType) {
|
|
|
1940
1972
|
eventType,
|
|
1941
1973
|
toolName: parsed.tool_name ?? null,
|
|
1942
1974
|
policyName: result.policyName,
|
|
1975
|
+
policyNames: result.policyNames,
|
|
1943
1976
|
decision: result.decision,
|
|
1944
1977
|
reason: result.reason,
|
|
1945
1978
|
durationMs,
|
|
@@ -2957,7 +2990,7 @@ import { realpathSync as realpathSync2 } from "fs";
|
|
|
2957
2990
|
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
2958
2991
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2959
2992
|
// package.json
|
|
2960
|
-
var version = "0.0.2-beta.
|
|
2993
|
+
var version = "0.0.2-beta.5";
|
|
2961
2994
|
|
|
2962
2995
|
// bin/failproofai.mjs
|
|
2963
2996
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.5",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|