failproofai 0.0.2-beta.2 → 0.0.2-beta.4
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/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]__0103jwf._.js → [root-of-the-server]__05ib_c3._.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]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ovwjau._.js → [root-of-the-server]__0od~yp1._.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 +5 -5
- 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 +1 -1
- 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/{0sm1iqi3m~xiz.js → 03qghe4e2_.ul.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jrzwsyo7wo26.js → 0_eej2~ju.yds.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tl2f-3yc.rqc.js → 0bc69j4t8njpq.js} +1 -1
- package/.next/standalone/.next/static/chunks/0nnxt7uoz_cvj.css +1 -0
- package/.next/standalone/.next/static/chunks/{0uftmw5od9kdz.js → 0qaojcc.nvqd8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{001k0zayn2o.s.js → 0xg4wy053mmhs.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pdd7~yp8ytu6.js → 12mgwr8gh_kqo.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0tbr0o7vwc~-s.js → 134~t05vpu75e.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wtcha31~i7rm.js → 17veghz_js0u3.js} +1 -1
- package/.next/standalone/Dockerfile.docs +12 -0
- package/.next/standalone/README.md +59 -46
- package/.next/standalone/app/components/session-hooks-panel.tsx +31 -6
- package/.next/standalone/app/policies/hooks-client.tsx +31 -6
- package/.next/standalone/dist/cli.mjs +234 -27
- package/.next/standalone/dist/index.js +2 -2
- package/.next/standalone/docs/{architecture.md → architecture.mdx} +40 -23
- package/.next/standalone/docs/{built-in-policies.md → built-in-policies.mdx} +134 -12
- package/.next/standalone/docs/cli/dashboard.mdx +28 -0
- package/.next/standalone/docs/cli/environment-variables.mdx +34 -0
- package/.next/standalone/docs/cli/hook.mdx +30 -0
- package/.next/standalone/docs/cli/install-policies.mdx +48 -0
- package/.next/standalone/docs/cli/list-policies.mdx +31 -0
- package/.next/standalone/docs/cli/remove-policies.mdx +44 -0
- package/.next/standalone/docs/cli/version.mdx +12 -0
- package/.next/standalone/docs/{configuration.md → configuration.mdx} +16 -16
- package/.next/standalone/docs/{custom-hooks.md → custom-policies.mdx} +80 -42
- package/.next/standalone/docs/{dashboard.md → dashboard.mdx} +26 -29
- package/.next/standalone/docs/docs.json +31 -4
- package/.next/standalone/docs/examples.mdx +253 -0
- package/.next/standalone/docs/for-agents.mdx +38 -0
- package/.next/standalone/docs/getting-started.mdx +134 -0
- package/.next/standalone/docs/introduction.mdx +57 -0
- package/.next/standalone/docs/logo/dark.svg +21 -0
- package/.next/standalone/docs/logo/light.svg +21 -0
- package/.next/standalone/docs/{package-aliases.md → package-aliases.mdx} +5 -5
- package/.next/standalone/docs/{testing.md → testing.mdx} +11 -11
- package/.next/standalone/package.json +6 -9
- package/.next/standalone/scripts/publish-aliases.mjs +4 -2
- package/.next/standalone/skills-lock.json +10 -0
- package/.next/standalone/src/hooks/builtin-policies.ts +271 -25
- 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 +23 -2
- package/.next/standalone/src/hooks/policy-helpers.ts +2 -2
- package/.next/standalone/vitest.config.e2e.mts +3 -0
- package/.next/standalone/vitest.config.mts +3 -0
- package/README.md +59 -46
- package/dist/cli.mjs +234 -27
- package/dist/index.js +2 -2
- package/package.json +6 -9
- package/scripts/publish-aliases.mjs +4 -2
- package/src/hooks/builtin-policies.ts +271 -25
- package/src/hooks/handler.ts +1 -0
- package/src/hooks/hook-activity-store.ts +6 -1
- package/src/hooks/policy-evaluator.ts +23 -2
- package/src/hooks/policy-helpers.ts +2 -2
- package/.next/standalone/.next/static/chunks/15jpradyu_531.css +0 -1
- package/.next/standalone/docs/cli-reference.md +0 -175
- package/.next/standalone/docs/getting-started.md +0 -128
- package/.next/standalone/docs/introduction.md +0 -47
- /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → N7CmoNJD3b7hE1pCaP_Gs}/_ssgManifest.js +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -179,8 +179,8 @@ var init_hooks_config = __esm(() => {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
// src/hooks/policy-helpers.ts
|
|
182
|
-
function allow() {
|
|
183
|
-
return { decision: "allow" };
|
|
182
|
+
function allow(reason) {
|
|
183
|
+
return reason ? { decision: "allow", reason } : { decision: "allow" };
|
|
184
184
|
}
|
|
185
185
|
function deny(reason) {
|
|
186
186
|
return { decision: "deny", reason };
|
|
@@ -248,7 +248,7 @@ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILP
|
|
|
248
248
|
// src/hooks/builtin-policies.ts
|
|
249
249
|
import { resolve as resolve2, join as join2 } from "node:path";
|
|
250
250
|
import { readFile, writeFile } from "node:fs/promises";
|
|
251
|
-
import { execSync } from "node:child_process";
|
|
251
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
252
252
|
import { homedir as homedir3 } from "node:os";
|
|
253
253
|
function isClaudeInternalPath(resolved2) {
|
|
254
254
|
const claudeDir = join2(homedir3(), ".claude");
|
|
@@ -266,6 +266,22 @@ function getFilePath(ctx) {
|
|
|
266
266
|
function parseArgvTokens(cmd) {
|
|
267
267
|
return cmd.trim().split(/\s+/).map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
268
268
|
}
|
|
269
|
+
function getCurrentBranch(cwd) {
|
|
270
|
+
try {
|
|
271
|
+
let branch = gitBranchCache.get(cwd);
|
|
272
|
+
if (branch === undefined) {
|
|
273
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
274
|
+
cwd,
|
|
275
|
+
encoding: "utf8",
|
|
276
|
+
timeout: 3000
|
|
277
|
+
}).trim();
|
|
278
|
+
gitBranchCache.set(cwd, branch);
|
|
279
|
+
}
|
|
280
|
+
return branch || null;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
269
285
|
function matchesAllowedPattern(cmd, pattern) {
|
|
270
286
|
const cmdTokens = parseArgvTokens(cmd);
|
|
271
287
|
const patTokens = parseArgvTokens(pattern);
|
|
@@ -480,7 +496,8 @@ function rmTargetIsAllowed(cmd, allowPaths) {
|
|
|
480
496
|
if (rmIdx < 0)
|
|
481
497
|
continue;
|
|
482
498
|
const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
|
|
483
|
-
|
|
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)))
|
|
484
501
|
continue;
|
|
485
502
|
const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
|
|
486
503
|
for (const target of pathArgs) {
|
|
@@ -505,7 +522,10 @@ function blockRmRf(ctx) {
|
|
|
505
522
|
if (ctx.toolName !== "Bash")
|
|
506
523
|
return allow();
|
|
507
524
|
const cmd = getCommand(ctx);
|
|
508
|
-
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
|
+
});
|
|
509
529
|
if (hasDestructivePath && (/rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) || /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd))) {
|
|
510
530
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
511
531
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
@@ -515,7 +535,10 @@ function blockRmRf(ctx) {
|
|
|
515
535
|
if (hasDestructivePath && /\brm\b/.test(cmd)) {
|
|
516
536
|
const tokens = parseArgvTokens(cmd);
|
|
517
537
|
const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
|
|
518
|
-
|
|
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) {
|
|
519
542
|
const allowPaths = ctx.params?.allowPaths ?? [];
|
|
520
543
|
if (rmTargetIsAllowed(cmd, allowPaths))
|
|
521
544
|
return allow();
|
|
@@ -653,22 +676,12 @@ function blockWorkOnMain(ctx) {
|
|
|
653
676
|
const cwd = ctx.session?.cwd;
|
|
654
677
|
if (!cwd)
|
|
655
678
|
return allow();
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (branch === undefined) {
|
|
659
|
-
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
660
|
-
cwd,
|
|
661
|
-
encoding: "utf8",
|
|
662
|
-
timeout: 3000
|
|
663
|
-
}).trim();
|
|
664
|
-
gitBranchCache.set(cwd, branch);
|
|
665
|
-
}
|
|
666
|
-
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
667
|
-
if (protectedBranches.includes(branch)) {
|
|
668
|
-
return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
|
|
669
|
-
}
|
|
670
|
-
} catch {
|
|
679
|
+
const branch = getCurrentBranch(cwd);
|
|
680
|
+
if (!branch)
|
|
671
681
|
return allow();
|
|
682
|
+
const protectedBranches = ctx.params?.protectedBranches ?? ["main", "master"];
|
|
683
|
+
if (protectedBranches.includes(branch)) {
|
|
684
|
+
return deny(`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`);
|
|
672
685
|
}
|
|
673
686
|
return allow();
|
|
674
687
|
}
|
|
@@ -767,6 +780,137 @@ function warnBackgroundProcess(ctx) {
|
|
|
767
780
|
}
|
|
768
781
|
return allow();
|
|
769
782
|
}
|
|
783
|
+
function requireCommitBeforeStop(ctx) {
|
|
784
|
+
const cwd = ctx.session?.cwd;
|
|
785
|
+
if (!cwd)
|
|
786
|
+
return allow("No working directory available, skipping commit check.");
|
|
787
|
+
try {
|
|
788
|
+
const status = execSync("git status --porcelain", {
|
|
789
|
+
cwd,
|
|
790
|
+
encoding: "utf8",
|
|
791
|
+
timeout: 5000
|
|
792
|
+
}).trim();
|
|
793
|
+
if (status.length > 0) {
|
|
794
|
+
return deny("You have uncommitted changes in the working directory. Commit all changes before stopping.");
|
|
795
|
+
}
|
|
796
|
+
return allow("All changes are committed.");
|
|
797
|
+
} catch {
|
|
798
|
+
return allow("Not a git repository, skipping commit check.");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function requirePushBeforeStop(ctx) {
|
|
802
|
+
const cwd = ctx.session?.cwd;
|
|
803
|
+
if (!cwd)
|
|
804
|
+
return allow("No working directory available, skipping push check.");
|
|
805
|
+
try {
|
|
806
|
+
const remotes = execSync("git remote", {
|
|
807
|
+
cwd,
|
|
808
|
+
encoding: "utf8",
|
|
809
|
+
timeout: 3000
|
|
810
|
+
}).trim();
|
|
811
|
+
if (!remotes)
|
|
812
|
+
return allow("No git remote configured, skipping push check.");
|
|
813
|
+
const remote = ctx.params?.remote ?? "origin";
|
|
814
|
+
const branch = getCurrentBranch(cwd);
|
|
815
|
+
if (!branch || branch === "HEAD")
|
|
816
|
+
return allow("Detached HEAD, skipping push check.");
|
|
817
|
+
let hasTracking = false;
|
|
818
|
+
try {
|
|
819
|
+
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
820
|
+
cwd,
|
|
821
|
+
encoding: "utf8",
|
|
822
|
+
timeout: 3000
|
|
823
|
+
});
|
|
824
|
+
hasTracking = true;
|
|
825
|
+
} catch {}
|
|
826
|
+
if (!hasTracking) {
|
|
827
|
+
return deny(`Branch "${branch}" has not been pushed to remote "${remote}". ` + `Push your branch with: git push -u ${remote} ${branch}`);
|
|
828
|
+
}
|
|
829
|
+
const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
|
|
830
|
+
cwd,
|
|
831
|
+
encoding: "utf8",
|
|
832
|
+
timeout: 5000
|
|
833
|
+
}).trim();
|
|
834
|
+
if (unpushed.length > 0) {
|
|
835
|
+
const commitCount = unpushed.split(`
|
|
836
|
+
`).length;
|
|
837
|
+
return deny(`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` + `Push your changes with: git push`);
|
|
838
|
+
}
|
|
839
|
+
return allow(`All commits pushed to "${remote}".`);
|
|
840
|
+
} catch {
|
|
841
|
+
return allow("Could not check push status, skipping.");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function requirePrBeforeStop(ctx) {
|
|
845
|
+
const cwd = ctx.session?.cwd;
|
|
846
|
+
if (!cwd)
|
|
847
|
+
return allow("No working directory available, skipping PR check.");
|
|
848
|
+
try {
|
|
849
|
+
try {
|
|
850
|
+
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
851
|
+
} catch {
|
|
852
|
+
return allow("GitHub CLI (gh) not installed, skipping PR check.");
|
|
853
|
+
}
|
|
854
|
+
const branch = getCurrentBranch(cwd);
|
|
855
|
+
if (!branch || branch === "HEAD")
|
|
856
|
+
return allow("Detached HEAD, skipping PR check.");
|
|
857
|
+
let prJson;
|
|
858
|
+
try {
|
|
859
|
+
prJson = execSync("gh pr view --json number,url,state", {
|
|
860
|
+
cwd,
|
|
861
|
+
encoding: "utf8",
|
|
862
|
+
timeout: 15000
|
|
863
|
+
}).trim();
|
|
864
|
+
} catch {
|
|
865
|
+
return deny(`No pull request found for branch "${branch}". ` + `Create one with: gh pr create`);
|
|
866
|
+
}
|
|
867
|
+
const pr = JSON.parse(prJson);
|
|
868
|
+
if (pr.state === "OPEN") {
|
|
869
|
+
return allow(`PR #${pr.number} exists: ${pr.url}`);
|
|
870
|
+
}
|
|
871
|
+
return deny(`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Create a new PR with: gh pr create`);
|
|
872
|
+
} catch {
|
|
873
|
+
return allow("Could not check PR status, skipping.");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function requireCiGreenBeforeStop(ctx) {
|
|
877
|
+
const cwd = ctx.session?.cwd;
|
|
878
|
+
if (!cwd)
|
|
879
|
+
return allow("No working directory available, skipping CI check.");
|
|
880
|
+
try {
|
|
881
|
+
try {
|
|
882
|
+
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
883
|
+
} catch {
|
|
884
|
+
return allow("GitHub CLI (gh) not installed, skipping CI check.");
|
|
885
|
+
}
|
|
886
|
+
const branch = getCurrentBranch(cwd);
|
|
887
|
+
if (!branch || branch === "HEAD")
|
|
888
|
+
return allow("Detached HEAD, skipping CI check.");
|
|
889
|
+
const runsJson = execFileSync("gh", ["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"], {
|
|
890
|
+
cwd,
|
|
891
|
+
encoding: "utf8",
|
|
892
|
+
timeout: 15000
|
|
893
|
+
}).trim();
|
|
894
|
+
if (!runsJson || runsJson === "[]")
|
|
895
|
+
return allow(`No CI runs found for branch "${branch}".`);
|
|
896
|
+
const runs = JSON.parse(runsJson);
|
|
897
|
+
if (runs.length === 0)
|
|
898
|
+
return allow(`No CI runs found for branch "${branch}".`);
|
|
899
|
+
const failing = runs.filter((r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped");
|
|
900
|
+
if (failing.length > 0) {
|
|
901
|
+
const names = failing.map((r) => `"${r.name}"`).join(", ");
|
|
902
|
+
return deny(`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`);
|
|
903
|
+
}
|
|
904
|
+
const pending = runs.filter((r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting");
|
|
905
|
+
if (pending.length > 0) {
|
|
906
|
+
const names = pending.map((r) => `"${r.name}"`).join(", ");
|
|
907
|
+
return deny(`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`);
|
|
908
|
+
}
|
|
909
|
+
return allow(`All CI checks passed on branch "${branch}".`);
|
|
910
|
+
} catch {
|
|
911
|
+
return allow("Could not check CI status, skipping.");
|
|
912
|
+
}
|
|
913
|
+
}
|
|
770
914
|
function registerBuiltinPolicies(enabledNames) {
|
|
771
915
|
const enabledSet = new Set(enabledNames);
|
|
772
916
|
for (const policy of BUILTIN_POLICIES) {
|
|
@@ -802,7 +946,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
802
946
|
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;
|
|
803
947
|
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/;
|
|
804
948
|
ENV_PRINTENV_RE = /(?:^|\s|;|&&|\|\|)(?:env|printenv)(?:\s|$|;|&&|\|)/;
|
|
805
|
-
ECHO_ENV_RE = /echo\s
|
|
949
|
+
ECHO_ENV_RE = /echo\s+.*\$\{?[A-Za-z_]/;
|
|
806
950
|
EXPORT_RE = /(?:^|\s|;|&&|\|\|)export\s+\w+/;
|
|
807
951
|
PS_ENV_VAR_RE = /\$env:[A-Za-z_]/i;
|
|
808
952
|
PS_CHILDITEM_ENV_RE = /(?:Get-ChildItem|dir|gci|ls)\s+Env:/i;
|
|
@@ -813,7 +957,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
813
957
|
SUDO_RE = /(?:^|;|&&|\|\|)\s*sudo\s/;
|
|
814
958
|
PS_ELEVATION_RE = /Start-Process\s+.*-Verb\s+RunAs/i;
|
|
815
959
|
RUNAS_RE = /(?:^|;|&&|\|\|)\s*runas\s/i;
|
|
816
|
-
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh)/;
|
|
960
|
+
CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|fish|ash)\b/;
|
|
817
961
|
PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
|
|
818
962
|
FORCE_PUSH_RE = /(?:--force|-f\b)/;
|
|
819
963
|
SECRET_FILE_RE = /\.(?:pem|key)$/;
|
|
@@ -1102,6 +1246,49 @@ var init_builtin_policies = __esm(() => {
|
|
|
1102
1246
|
match: { events: ["PreToolUse"] },
|
|
1103
1247
|
defaultEnabled: false,
|
|
1104
1248
|
category: "AI Behavior"
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
name: "require-commit-before-stop",
|
|
1252
|
+
description: "Require all changes to be committed before Claude stops",
|
|
1253
|
+
fn: requireCommitBeforeStop,
|
|
1254
|
+
match: { events: ["Stop"] },
|
|
1255
|
+
defaultEnabled: false,
|
|
1256
|
+
category: "Workflow",
|
|
1257
|
+
beta: true
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
name: "require-push-before-stop",
|
|
1261
|
+
description: "Require all commits to be pushed to remote before Claude stops",
|
|
1262
|
+
fn: requirePushBeforeStop,
|
|
1263
|
+
match: { events: ["Stop"] },
|
|
1264
|
+
defaultEnabled: false,
|
|
1265
|
+
category: "Workflow",
|
|
1266
|
+
beta: true,
|
|
1267
|
+
params: {
|
|
1268
|
+
remote: {
|
|
1269
|
+
type: "string",
|
|
1270
|
+
description: "Remote name to push to (default: origin)",
|
|
1271
|
+
default: "origin"
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
name: "require-pr-before-stop",
|
|
1277
|
+
description: "Require a pull request to exist for the current branch before Claude stops",
|
|
1278
|
+
fn: requirePrBeforeStop,
|
|
1279
|
+
match: { events: ["Stop"] },
|
|
1280
|
+
defaultEnabled: false,
|
|
1281
|
+
category: "Workflow",
|
|
1282
|
+
beta: true
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "require-ci-green-before-stop",
|
|
1286
|
+
description: "Require CI checks to pass on the current branch before Claude stops",
|
|
1287
|
+
fn: requireCiGreenBeforeStop,
|
|
1288
|
+
match: { events: ["Stop"] },
|
|
1289
|
+
defaultEnabled: false,
|
|
1290
|
+
category: "Workflow",
|
|
1291
|
+
beta: true
|
|
1105
1292
|
}
|
|
1106
1293
|
];
|
|
1107
1294
|
});
|
|
@@ -1124,6 +1311,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1124
1311
|
};
|
|
1125
1312
|
let instructPolicyName = null;
|
|
1126
1313
|
let instructReason = null;
|
|
1314
|
+
const allowEntries = [];
|
|
1127
1315
|
for (const policy of policies) {
|
|
1128
1316
|
const schema = POLICY_PARAMS_MAP.get(policy.name);
|
|
1129
1317
|
let ctx;
|
|
@@ -1184,7 +1372,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1184
1372
|
return {
|
|
1185
1373
|
exitCode: 2,
|
|
1186
1374
|
stdout: "",
|
|
1187
|
-
stderr:
|
|
1375
|
+
stderr: reason,
|
|
1188
1376
|
policyName: policy.name,
|
|
1189
1377
|
reason,
|
|
1190
1378
|
decision: "deny"
|
|
@@ -1195,6 +1383,9 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1195
1383
|
instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
|
|
1196
1384
|
hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
|
|
1197
1385
|
}
|
|
1386
|
+
if (result.decision === "allow" && result.reason) {
|
|
1387
|
+
allowEntries.push({ policyName: policy.name, reason: result.reason });
|
|
1388
|
+
}
|
|
1198
1389
|
}
|
|
1199
1390
|
if (instructPolicyName && instructReason) {
|
|
1200
1391
|
if (eventType === "Stop") {
|
|
@@ -1222,6 +1413,17 @@ async function evaluatePolicies(eventType, payload, session, config) {
|
|
|
1222
1413
|
decision: "instruct"
|
|
1223
1414
|
};
|
|
1224
1415
|
}
|
|
1416
|
+
if (allowEntries.length > 0) {
|
|
1417
|
+
const combined = allowEntries.map((e) => e.reason).join(`
|
|
1418
|
+
`);
|
|
1419
|
+
const policyNames = allowEntries.map((e) => e.policyName);
|
|
1420
|
+
const supportsHookSpecificOutput = eventType === "PreToolUse" || eventType === "PostToolUse" || eventType === "UserPromptSubmit";
|
|
1421
|
+
const response = supportsHookSpecificOutput ? { hookSpecificOutput: { hookEventName: eventType, additionalContext: `Note from failproofai: ${combined}` } } : { reason: combined };
|
|
1422
|
+
const stderrMsg = allowEntries.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`).join(`
|
|
1423
|
+
`);
|
|
1424
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: stderrMsg + `
|
|
1425
|
+
`, policyName: policyNames[0], policyNames, reason: combined, decision: "allow" };
|
|
1426
|
+
}
|
|
1225
1427
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
1226
1428
|
}
|
|
1227
1429
|
var POLICY_PARAMS_MAP;
|
|
@@ -1516,7 +1718,11 @@ function updateStats(entry) {
|
|
|
1516
1718
|
s.totalEvents += 1;
|
|
1517
1719
|
if (entry.decision === "deny")
|
|
1518
1720
|
s.denyCount += 1;
|
|
1519
|
-
if (entry.
|
|
1721
|
+
if (entry.policyNames && entry.policyNames.length > 0) {
|
|
1722
|
+
for (const name of entry.policyNames) {
|
|
1723
|
+
s.policyMap[name] = (s.policyMap[name] ?? 0) + 1;
|
|
1724
|
+
}
|
|
1725
|
+
} else if (entry.policyName) {
|
|
1520
1726
|
s.policyMap[entry.policyName] = (s.policyMap[entry.policyName] ?? 0) + 1;
|
|
1521
1727
|
}
|
|
1522
1728
|
const tmpPath = join3(storeDir, `stats.json.${process.pid}.tmp`);
|
|
@@ -1536,7 +1742,7 @@ var init_hook_activity_store = __esm(() => {
|
|
|
1536
1742
|
});
|
|
1537
1743
|
|
|
1538
1744
|
// package.json
|
|
1539
|
-
var version2 = "0.0.2-beta.
|
|
1745
|
+
var version2 = "0.0.2-beta.4";
|
|
1540
1746
|
var init_package = () => {};
|
|
1541
1747
|
|
|
1542
1748
|
// src/posthog-key.ts
|
|
@@ -1745,6 +1951,7 @@ async function handleHookEvent(eventType) {
|
|
|
1745
1951
|
eventType,
|
|
1746
1952
|
toolName: parsed.tool_name ?? null,
|
|
1747
1953
|
policyName: result.policyName,
|
|
1954
|
+
policyNames: result.policyNames,
|
|
1748
1955
|
decision: result.decision,
|
|
1749
1956
|
reason: result.reason,
|
|
1750
1957
|
durationMs,
|
|
@@ -2762,7 +2969,7 @@ import { realpathSync as realpathSync2 } from "fs";
|
|
|
2762
2969
|
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
2763
2970
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2764
2971
|
// package.json
|
|
2765
|
-
var version = "0.0.2-beta.
|
|
2972
|
+
var version = "0.0.2-beta.4";
|
|
2766
2973
|
|
|
2767
2974
|
// bin/failproofai.mjs
|
|
2768
2975
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
package/dist/index.js
CHANGED
|
@@ -69,8 +69,8 @@ function clearCustomHooks() {
|
|
|
69
69
|
g[REGISTRY_KEY] = [];
|
|
70
70
|
}
|
|
71
71
|
// src/hooks/policy-helpers.ts
|
|
72
|
-
function allow() {
|
|
73
|
-
return { decision: "allow" };
|
|
72
|
+
function allow(reason) {
|
|
73
|
+
return reason ? { decision: "allow", reason } : { decision: "allow" };
|
|
74
74
|
}
|
|
75
75
|
function deny(reason) {
|
|
76
76
|
return { decision: "deny", reason };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.2-beta.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"
|
|
7
7
|
},
|
|
@@ -40,17 +40,14 @@
|
|
|
40
40
|
"claude-agents-sdk",
|
|
41
41
|
"anthropic",
|
|
42
42
|
"ai-agent",
|
|
43
|
-
"
|
|
44
|
-
"agent-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"agent-reliability",
|
|
44
|
+
"agent-monitoring",
|
|
45
|
+
"autonomous-agent",
|
|
46
|
+
"failure-prevention",
|
|
47
47
|
"developer-tools",
|
|
48
48
|
"devtools",
|
|
49
49
|
"cli",
|
|
50
50
|
"local-first",
|
|
51
|
-
"monitoring",
|
|
52
|
-
"debugging",
|
|
53
|
-
"tracing",
|
|
54
51
|
"hooks",
|
|
55
52
|
"policies"
|
|
56
53
|
],
|
|
@@ -8,6 +8,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
const rootPkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
9
9
|
const VERSION = rootPkg.version;
|
|
10
10
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
11
|
+
const distTagIdx = process.argv.indexOf('--dist-tag');
|
|
12
|
+
const DIST_TAG = distTagIdx !== -1 ? process.argv[distTagIdx + 1] : (VERSION.includes('-') ? 'beta' : 'latest');
|
|
11
13
|
|
|
12
14
|
const ALIASES = [
|
|
13
15
|
// Formatting variants — no "ai", or hyphen/underscore separators
|
|
@@ -54,7 +56,7 @@ for (const name of ALIASES) {
|
|
|
54
56
|
cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
|
|
55
57
|
|
|
56
58
|
if (DRY_RUN) {
|
|
57
|
-
console.log(`[dry-run] Would publish ${name}@${VERSION}`);
|
|
59
|
+
console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
|
|
58
60
|
console.log(JSON.stringify(pkg, null, 2));
|
|
59
61
|
console.log('---');
|
|
60
62
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -63,7 +65,7 @@ for (const name of ALIASES) {
|
|
|
63
65
|
|
|
64
66
|
console.log(`Publishing ${name}@${VERSION}...`);
|
|
65
67
|
try {
|
|
66
|
-
execSync(
|
|
68
|
+
execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
67
69
|
console.log(`Done: ${name}`);
|
|
68
70
|
} catch (err) {
|
|
69
71
|
const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
|