failproofai 0.0.2-beta.2 → 0.0.2-beta.3
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]__07k6eu-._.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]__0kfv9fw._.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/{0uftmw5od9kdz.js → 0.jo.465b6_k..js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wtcha31~i7rm.js → 01haq0a3zrx0v.js} +1 -1
- package/.next/standalone/.next/static/chunks/08f78tecvx61l.css +1 -0
- package/.next/standalone/.next/static/chunks/{0tl2f-3yc.rqc.js → 0a6xi1a8f_qlp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tbr0o7vwc~-s.js → 0mq7ze1vkeo1p.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0sm1iqi3m~xiz.js → 0p_fpyfmmohnx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pdd7~yp8ytu6.js → 0qwyj3m400l_g.js} +1 -1
- package/.next/standalone/.next/static/chunks/{001k0zayn2o.s.js → 0t94r_mk0s7e4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jrzwsyo7wo26.js → 139~00zc9.u7s.js} +1 -1
- package/.next/standalone/Dockerfile.docs +12 -0
- package/.next/standalone/README.md +59 -46
- package/.next/standalone/dist/cli.mjs +215 -20
- 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 +259 -20
- package/.next/standalone/src/hooks/policy-evaluator.ts +19 -1
- 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 +215 -20
- package/dist/index.js +2 -2
- package/package.json +6 -9
- package/scripts/publish-aliases.mjs +4 -2
- package/src/hooks/builtin-policies.ts +259 -20
- package/src/hooks/policy-evaluator.ts +19 -1
- 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 → 7fR022u1Sj-s5MfKO1q9Y}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → 7fR022u1Sj-s5MfKO1q9Y}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JksWDLwDoPy6bcczVWlff → 7fR022u1Sj-s5MfKO1q9Y}/_ssgManifest.js +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
5
|
import { readFile, writeFile, stat, open } from "node:fs/promises";
|
|
6
|
-
import { execSync } from "node:child_process";
|
|
6
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import type { BuiltinPolicyDefinition, PolicyContext, PolicyResult, PolicyParamsSchema } from "./policy-types";
|
|
9
9
|
import { allow, deny, instruct } from "./policy-helpers";
|
|
@@ -145,12 +145,29 @@ const TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
|
|
|
145
145
|
const DISOWN_RE = /\bdisown\b/;
|
|
146
146
|
const BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
|
|
147
147
|
|
|
148
|
-
//
|
|
148
|
+
// Caches the current branch per cwd to avoid repeated execSync calls.
|
|
149
149
|
// Trade-off: if the user switches branches externally mid-session, the cache serves
|
|
150
150
|
// the stale value until the process restarts. This is acceptable since branch switches
|
|
151
151
|
// during an active Claude session are rare.
|
|
152
152
|
const gitBranchCache = new Map<string, string>();
|
|
153
153
|
|
|
154
|
+
function getCurrentBranch(cwd: string): string | null {
|
|
155
|
+
try {
|
|
156
|
+
let branch = gitBranchCache.get(cwd);
|
|
157
|
+
if (branch === undefined) {
|
|
158
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
159
|
+
cwd,
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
timeout: 3000,
|
|
162
|
+
}).trim();
|
|
163
|
+
gitBranchCache.set(cwd, branch);
|
|
164
|
+
}
|
|
165
|
+
return branch || null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
/**
|
|
155
172
|
* Check if a command matches an allow pattern using token-by-token comparison.
|
|
156
173
|
* The "*" token is a wildcard. Extra command tokens beyond the pattern are allowed,
|
|
@@ -627,24 +644,14 @@ function blockWorkOnMain(ctx: PolicyContext): PolicyResult {
|
|
|
627
644
|
const cwd = ctx.session?.cwd;
|
|
628
645
|
if (!cwd) return allow();
|
|
629
646
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
gitBranchCache.set(cwd, branch);
|
|
639
|
-
}
|
|
640
|
-
const protectedBranches = ((ctx.params?.protectedBranches ?? ["main", "master"]) as string[]);
|
|
641
|
-
if (protectedBranches.includes(branch)) {
|
|
642
|
-
return deny(
|
|
643
|
-
`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`,
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
} catch {
|
|
647
|
-
return allow();
|
|
647
|
+
const branch = getCurrentBranch(cwd);
|
|
648
|
+
if (!branch) return allow();
|
|
649
|
+
|
|
650
|
+
const protectedBranches = ((ctx.params?.protectedBranches ?? ["main", "master"]) as string[]);
|
|
651
|
+
if (protectedBranches.includes(branch)) {
|
|
652
|
+
return deny(
|
|
653
|
+
`Git ${cmd.match(/git\s+(\S+)/)?.[1] ?? "operation"} on ${branch} is blocked. Create a feature branch first.`,
|
|
654
|
+
);
|
|
648
655
|
}
|
|
649
656
|
return allow();
|
|
650
657
|
}
|
|
@@ -786,6 +793,195 @@ function warnBackgroundProcess(ctx: PolicyContext): PolicyResult {
|
|
|
786
793
|
return allow();
|
|
787
794
|
}
|
|
788
795
|
|
|
796
|
+
// -- Workflow (Stop event) policies --
|
|
797
|
+
|
|
798
|
+
function requireCommitBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
799
|
+
const cwd = ctx.session?.cwd;
|
|
800
|
+
if (!cwd) return allow("No working directory available, skipping commit check.");
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
const status = execSync("git status --porcelain", {
|
|
804
|
+
cwd,
|
|
805
|
+
encoding: "utf8",
|
|
806
|
+
timeout: 5000,
|
|
807
|
+
}).trim();
|
|
808
|
+
|
|
809
|
+
if (status.length > 0) {
|
|
810
|
+
return deny(
|
|
811
|
+
"You have uncommitted changes in the working directory. Commit all changes before stopping.",
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return allow("All changes are committed.");
|
|
815
|
+
} catch {
|
|
816
|
+
return allow("Not a git repository, skipping commit check.");
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function requirePushBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
821
|
+
const cwd = ctx.session?.cwd;
|
|
822
|
+
if (!cwd) return allow("No working directory available, skipping push check.");
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const remotes = execSync("git remote", {
|
|
826
|
+
cwd,
|
|
827
|
+
encoding: "utf8",
|
|
828
|
+
timeout: 3000,
|
|
829
|
+
}).trim();
|
|
830
|
+
|
|
831
|
+
if (!remotes) return allow("No git remote configured, skipping push check.");
|
|
832
|
+
|
|
833
|
+
const remote = (ctx.params?.remote as string) ?? "origin";
|
|
834
|
+
|
|
835
|
+
const branch = getCurrentBranch(cwd);
|
|
836
|
+
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping push check.");
|
|
837
|
+
|
|
838
|
+
// Check if remote tracking branch exists
|
|
839
|
+
let hasTracking = false;
|
|
840
|
+
try {
|
|
841
|
+
execFileSync("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
842
|
+
cwd,
|
|
843
|
+
encoding: "utf8",
|
|
844
|
+
timeout: 3000,
|
|
845
|
+
});
|
|
846
|
+
hasTracking = true;
|
|
847
|
+
} catch {
|
|
848
|
+
// Remote tracking branch does not exist
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (!hasTracking) {
|
|
852
|
+
return deny(
|
|
853
|
+
`Branch "${branch}" has not been pushed to remote "${remote}". ` +
|
|
854
|
+
`Push your branch with: git push -u ${remote} ${branch}`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Check for unpushed commits
|
|
859
|
+
const unpushed = execFileSync("git", ["log", `${remote}/${branch}..HEAD`, "--oneline"], {
|
|
860
|
+
cwd,
|
|
861
|
+
encoding: "utf8",
|
|
862
|
+
timeout: 5000,
|
|
863
|
+
}).trim();
|
|
864
|
+
|
|
865
|
+
if (unpushed.length > 0) {
|
|
866
|
+
const commitCount = unpushed.split("\n").length;
|
|
867
|
+
return deny(
|
|
868
|
+
`You have ${commitCount} unpushed commit${commitCount > 1 ? "s" : ""} on branch "${branch}". ` +
|
|
869
|
+
`Push your changes with: git push`,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return allow(`All commits pushed to "${remote}".`);
|
|
874
|
+
} catch {
|
|
875
|
+
return allow("Could not check push status, skipping.");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function requirePrBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
880
|
+
const cwd = ctx.session?.cwd;
|
|
881
|
+
if (!cwd) return allow("No working directory available, skipping PR check.");
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
// Check if gh CLI is available
|
|
885
|
+
try {
|
|
886
|
+
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
887
|
+
} catch {
|
|
888
|
+
return allow("GitHub CLI (gh) not installed, skipping PR check.");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const branch = getCurrentBranch(cwd);
|
|
892
|
+
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping PR check.");
|
|
893
|
+
|
|
894
|
+
// Check if a PR exists for this branch
|
|
895
|
+
let prJson: string;
|
|
896
|
+
try {
|
|
897
|
+
prJson = execSync("gh pr view --json number,url,state", {
|
|
898
|
+
cwd,
|
|
899
|
+
encoding: "utf8",
|
|
900
|
+
timeout: 15000,
|
|
901
|
+
}).trim();
|
|
902
|
+
} catch {
|
|
903
|
+
// gh pr view exits non-zero when no PR exists
|
|
904
|
+
return deny(
|
|
905
|
+
`No pull request found for branch "${branch}". ` +
|
|
906
|
+
`Create one with: gh pr create`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const pr = JSON.parse(prJson) as { number: number; url: string; state: string };
|
|
911
|
+
|
|
912
|
+
if (pr.state === "OPEN") {
|
|
913
|
+
return allow(`PR #${pr.number} exists: ${pr.url}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return deny(
|
|
917
|
+
`Pull request for branch "${branch}" is ${pr.state.toLowerCase()}. Create a new PR with: gh pr create`,
|
|
918
|
+
);
|
|
919
|
+
} catch {
|
|
920
|
+
return allow("Could not check PR status, skipping.");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
925
|
+
const cwd = ctx.session?.cwd;
|
|
926
|
+
if (!cwd) return allow("No working directory available, skipping CI check.");
|
|
927
|
+
|
|
928
|
+
try {
|
|
929
|
+
// Check if gh CLI is available
|
|
930
|
+
try {
|
|
931
|
+
execSync("gh --version", { cwd, encoding: "utf8", timeout: 3000 });
|
|
932
|
+
} catch {
|
|
933
|
+
return allow("GitHub CLI (gh) not installed, skipping CI check.");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const branch = getCurrentBranch(cwd);
|
|
937
|
+
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
|
|
938
|
+
|
|
939
|
+
const runsJson = execFileSync(
|
|
940
|
+
"gh",
|
|
941
|
+
["run", "list", "--branch", branch, "--limit", "5", "--json", "status,conclusion,name"],
|
|
942
|
+
{
|
|
943
|
+
cwd,
|
|
944
|
+
encoding: "utf8",
|
|
945
|
+
timeout: 15000,
|
|
946
|
+
},
|
|
947
|
+
).trim();
|
|
948
|
+
|
|
949
|
+
if (!runsJson || runsJson === "[]") return allow(`No CI runs found for branch "${branch}".`);
|
|
950
|
+
|
|
951
|
+
const runs = JSON.parse(runsJson) as Array<{
|
|
952
|
+
status: string;
|
|
953
|
+
conclusion: string;
|
|
954
|
+
name: string;
|
|
955
|
+
}>;
|
|
956
|
+
|
|
957
|
+
if (runs.length === 0) return allow(`No CI runs found for branch "${branch}".`);
|
|
958
|
+
|
|
959
|
+
const failing = runs.filter(
|
|
960
|
+
(r) => r.status === "completed" && r.conclusion !== "success" && r.conclusion !== "skipped",
|
|
961
|
+
);
|
|
962
|
+
if (failing.length > 0) {
|
|
963
|
+
const names = failing.map((r) => `"${r.name}"`).join(", ");
|
|
964
|
+
return deny(
|
|
965
|
+
`CI checks are failing on branch "${branch}": ${names}. Fix the failing checks before stopping.`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const pending = runs.filter(
|
|
970
|
+
(r) => r.status === "in_progress" || r.status === "queued" || r.status === "waiting",
|
|
971
|
+
);
|
|
972
|
+
if (pending.length > 0) {
|
|
973
|
+
const names = pending.map((r) => `"${r.name}"`).join(", ");
|
|
974
|
+
return deny(
|
|
975
|
+
`CI checks are still running on branch "${branch}": ${names}. Wait for all checks to complete and verify they pass.`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return allow(`All CI checks passed on branch "${branch}".`);
|
|
980
|
+
} catch {
|
|
981
|
+
return allow("Could not check CI status, skipping.");
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
789
985
|
// -- Registry --
|
|
790
986
|
|
|
791
987
|
export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
@@ -1053,6 +1249,49 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
|
1053
1249
|
defaultEnabled: false,
|
|
1054
1250
|
category: "AI Behavior",
|
|
1055
1251
|
},
|
|
1252
|
+
{
|
|
1253
|
+
name: "require-commit-before-stop",
|
|
1254
|
+
description: "Require all changes to be committed before Claude stops",
|
|
1255
|
+
fn: requireCommitBeforeStop,
|
|
1256
|
+
match: { events: ["Stop"] },
|
|
1257
|
+
defaultEnabled: false,
|
|
1258
|
+
category: "Workflow",
|
|
1259
|
+
beta: true,
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: "require-push-before-stop",
|
|
1263
|
+
description: "Require all commits to be pushed to remote before Claude stops",
|
|
1264
|
+
fn: requirePushBeforeStop,
|
|
1265
|
+
match: { events: ["Stop"] },
|
|
1266
|
+
defaultEnabled: false,
|
|
1267
|
+
category: "Workflow",
|
|
1268
|
+
beta: true,
|
|
1269
|
+
params: {
|
|
1270
|
+
remote: {
|
|
1271
|
+
type: "string",
|
|
1272
|
+
description: "Remote name to push to (default: origin)",
|
|
1273
|
+
default: "origin",
|
|
1274
|
+
},
|
|
1275
|
+
} satisfies PolicyParamsSchema,
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
name: "require-pr-before-stop",
|
|
1279
|
+
description: "Require a pull request to exist for the current branch before Claude stops",
|
|
1280
|
+
fn: requirePrBeforeStop,
|
|
1281
|
+
match: { events: ["Stop"] },
|
|
1282
|
+
defaultEnabled: false,
|
|
1283
|
+
category: "Workflow",
|
|
1284
|
+
beta: true,
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
name: "require-ci-green-before-stop",
|
|
1288
|
+
description: "Require CI checks to pass on the current branch before Claude stops",
|
|
1289
|
+
fn: requireCiGreenBeforeStop,
|
|
1290
|
+
match: { events: ["Stop"] },
|
|
1291
|
+
defaultEnabled: false,
|
|
1292
|
+
category: "Workflow",
|
|
1293
|
+
beta: true,
|
|
1294
|
+
},
|
|
1056
1295
|
];
|
|
1057
1296
|
|
|
1058
1297
|
export function registerBuiltinPolicies(enabledNames: string[]): void {
|
|
@@ -51,6 +51,9 @@ export async function evaluatePolicies(
|
|
|
51
51
|
let instructPolicyName: string | null = null;
|
|
52
52
|
let instructReason: string | null = null;
|
|
53
53
|
|
|
54
|
+
// Track informational messages from allow decisions
|
|
55
|
+
const allowMessages: string[] = [];
|
|
56
|
+
|
|
54
57
|
for (const policy of policies) {
|
|
55
58
|
// Inject params: merge policyParams[policy.name] over schema defaults
|
|
56
59
|
const schema = POLICY_PARAMS_MAP.get(policy.name);
|
|
@@ -133,6 +136,11 @@ export async function evaluatePolicies(
|
|
|
133
136
|
instructReason = result.reason ?? `Instruction from policy: ${policy.name}`;
|
|
134
137
|
hookLogInfo(`instruct by "${policy.name}": ${instructReason}`);
|
|
135
138
|
}
|
|
139
|
+
|
|
140
|
+
// Accumulate informational messages from allow decisions
|
|
141
|
+
if (result.decision === "allow" && result.reason) {
|
|
142
|
+
allowMessages.push(result.reason);
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
// No deny — check if we accumulated an instruct
|
|
@@ -166,6 +174,16 @@ export async function evaluatePolicies(
|
|
|
166
174
|
};
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
// All policies allowed
|
|
177
|
+
// All policies allowed — pass along any informational messages
|
|
178
|
+
if (allowMessages.length > 0) {
|
|
179
|
+
const combined = allowMessages.join("\n");
|
|
180
|
+
const response = {
|
|
181
|
+
hookSpecificOutput: {
|
|
182
|
+
hookEventName: eventType,
|
|
183
|
+
additionalContext: combined,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
return { exitCode: 0, stdout: JSON.stringify(response), stderr: "", policyName: null, reason: combined, decision: "allow" };
|
|
187
|
+
}
|
|
170
188
|
return { exitCode: 0, stdout: "", stderr: "", policyName: null, reason: null, decision: "allow" };
|
|
171
189
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { PolicyResult } from "./policy-types";
|
|
5
5
|
|
|
6
|
-
export function allow(): PolicyResult {
|
|
7
|
-
return { decision: "allow" };
|
|
6
|
+
export function allow(reason?: string): PolicyResult {
|
|
7
|
+
return reason ? { decision: "allow", reason } : { decision: "allow" };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function deny(reason: string): PolicyResult {
|
package/README.md
CHANGED
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
[](https://github.com/exospherehost/failproofai/actions)
|
|
16
16
|
[](https://discord.com/invite/zT92CAgvkj)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously - for **Claude Code** & the **Agents SDK**.
|
|
19
19
|
|
|
20
|
-
- **
|
|
21
|
-
- **Custom Policies**
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
20
|
+
- **30 Built-in Policies** - Catch common agent failure modes out of the box. Block destructive commands, prevent secret leakage, keep agents inside project boundaries, detect loops, and more.
|
|
21
|
+
- **Custom Policies** - Write your own reliability rules in JavaScript. Use the `allow`/`deny`/`instruct` API to enforce conventions, prevent drift, gate operations, or integrate with external systems.
|
|
22
|
+
- **Easy Configuration** - Tune any policy without writing code. Set allowlists, protected branches, thresholds per-project or globally. Three-scope config merges automatically.
|
|
23
|
+
- **Agent Monitor** - See what your agents did while you were away. Browse sessions, inspect every tool call, and review exactly where policies fired.
|
|
24
24
|
|
|
25
|
-
Everything runs locally
|
|
25
|
+
Everything runs locally - no data leaves your machine.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
## Requirements
|
|
30
30
|
|
|
31
31
|
- Node.js >= 20.9.0
|
|
32
|
-
- Bun >= 1.3.0 (optional
|
|
32
|
+
- Bun >= 1.3.0 (optional - only needed for development / building from source)
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
@@ -59,7 +59,7 @@ Writes hook entries into `~/.claude/settings.json`. Claude Code will now invoke
|
|
|
59
59
|
failproofai
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Opens `http://localhost:8020`
|
|
62
|
+
Opens `http://localhost:8020` - browse sessions, inspect logs, manage policies.
|
|
63
63
|
|
|
64
64
|
### 3. Check what's active
|
|
65
65
|
|
|
@@ -128,7 +128,7 @@ Policy configuration lives in `~/.failproofai/policies-config.json` (global) or
|
|
|
128
128
|
}
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
**Three config scopes** are merged automatically (project → local → global). See [docs/configuration.
|
|
131
|
+
**Three config scopes** are merged automatically (project → local → global). See [docs/configuration.mdx](docs/configuration.mdx) for full merge rules.
|
|
132
132
|
|
|
133
133
|
---
|
|
134
134
|
|
|
@@ -136,40 +136,40 @@ Policy configuration lives in `~/.failproofai/policies-config.json` (global) or
|
|
|
136
136
|
|
|
137
137
|
| Policy | Description | Configurable |
|
|
138
138
|
|--------|-------------|:---:|
|
|
139
|
-
| `block-sudo` |
|
|
140
|
-
| `block-rm-rf` |
|
|
141
|
-
| `block-curl-pipe-sh` |
|
|
139
|
+
| `block-sudo` | Prevent agents from running privileged system commands | `allowPatterns` |
|
|
140
|
+
| `block-rm-rf` | Prevent accidental recursive file deletion | `allowPaths` |
|
|
141
|
+
| `block-curl-pipe-sh` | Prevent agents from piping untrusted scripts to shell | |
|
|
142
142
|
| `block-failproofai-commands` | Prevent self-uninstallation | |
|
|
143
|
-
| `sanitize-jwt` |
|
|
144
|
-
| `sanitize-api-keys` |
|
|
145
|
-
| `sanitize-connection-strings` |
|
|
146
|
-
| `sanitize-private-key-content` | Redact PEM private key blocks | |
|
|
147
|
-
| `sanitize-bearer-tokens` | Redact Authorization Bearer tokens | |
|
|
148
|
-
| `block-env-files` |
|
|
149
|
-
| `protect-env-vars` |
|
|
150
|
-
| `block-read-outside-cwd` |
|
|
151
|
-
| `block-secrets-write` |
|
|
152
|
-
| `block-push-master` |
|
|
153
|
-
| `block-work-on-main` |
|
|
154
|
-
| `block-force-push` |
|
|
155
|
-
| `warn-git-amend` |
|
|
156
|
-
| `warn-git-stash-drop` |
|
|
157
|
-
| `warn-all-files-staged` |
|
|
158
|
-
| `warn-destructive-sql` |
|
|
159
|
-
| `warn-schema-alteration` |
|
|
160
|
-
| `warn-large-file-write` |
|
|
161
|
-
| `warn-package-publish` |
|
|
162
|
-
| `warn-background-process` |
|
|
163
|
-
| `warn-global-package-install` |
|
|
143
|
+
| `sanitize-jwt` | Stop JWT tokens from leaking into agent context | |
|
|
144
|
+
| `sanitize-api-keys` | Stop API keys from leaking into agent context | `additionalPatterns` |
|
|
145
|
+
| `sanitize-connection-strings` | Stop database credentials from leaking into agent context | |
|
|
146
|
+
| `sanitize-private-key-content` | Redact PEM private key blocks from output | |
|
|
147
|
+
| `sanitize-bearer-tokens` | Redact Authorization Bearer tokens from output | |
|
|
148
|
+
| `block-env-files` | Keep agents from reading .env files | |
|
|
149
|
+
| `protect-env-vars` | Prevent agents from printing environment variables | |
|
|
150
|
+
| `block-read-outside-cwd` | Keep agents inside project boundaries | `allowPaths` |
|
|
151
|
+
| `block-secrets-write` | Prevent writes to private key and certificate files | `additionalPatterns` |
|
|
152
|
+
| `block-push-master` | Prevent accidental pushes to main/master | `protectedBranches` |
|
|
153
|
+
| `block-work-on-main` | Keep agents off protected branches | `protectedBranches` |
|
|
154
|
+
| `block-force-push` | Prevent `git push --force` | |
|
|
155
|
+
| `warn-git-amend` | Remind agents before amending commits | |
|
|
156
|
+
| `warn-git-stash-drop` | Remind agents before dropping stashes | |
|
|
157
|
+
| `warn-all-files-staged` | Catch accidental `git add -A` | |
|
|
158
|
+
| `warn-destructive-sql` | Catch DROP/DELETE SQL before execution | |
|
|
159
|
+
| `warn-schema-alteration` | Catch ALTER TABLE before execution | |
|
|
160
|
+
| `warn-large-file-write` | Catch unexpectedly large file writes | `thresholdKb` |
|
|
161
|
+
| `warn-package-publish` | Catch accidental `npm publish` | |
|
|
162
|
+
| `warn-background-process` | Catch unintended background process launches | |
|
|
163
|
+
| `warn-global-package-install` | Catch unintended global package installs | |
|
|
164
164
|
| …and more | | |
|
|
165
165
|
|
|
166
|
-
Full policy details and parameter reference: [docs/built-in-policies.
|
|
166
|
+
Full policy details and parameter reference: [docs/built-in-policies.mdx](docs/built-in-policies.mdx)
|
|
167
167
|
|
|
168
168
|
---
|
|
169
169
|
|
|
170
170
|
## Custom policies
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
Write your own policies to keep agents reliable and on-task:
|
|
173
173
|
|
|
174
174
|
```js
|
|
175
175
|
import { customPolicies, allow, deny, instruct } from "failproofai";
|
|
@@ -197,8 +197,9 @@ failproofai policies --install --custom ./my-policies.js
|
|
|
197
197
|
|
|
198
198
|
| Function | Effect |
|
|
199
199
|
|----------|--------|
|
|
200
|
-
| `allow()` | Permit the
|
|
201
|
-
| `
|
|
200
|
+
| `allow()` | Permit the operation |
|
|
201
|
+
| `allow(message)` | Permit and send informational context to Claude *(beta)* |
|
|
202
|
+
| `deny(message)` | Block the operation; message shown to Claude |
|
|
202
203
|
| `instruct(message)` | Add context to Claude's prompt; does not block |
|
|
203
204
|
|
|
204
205
|
### Context object (`ctx`)
|
|
@@ -213,7 +214,7 @@ failproofai policies --install --custom ./my-policies.js
|
|
|
213
214
|
| `session.sessionId` | `string` | Session identifier |
|
|
214
215
|
| `session.transcriptPath` | `string` | Path to the session transcript file |
|
|
215
216
|
|
|
216
|
-
Custom hooks support transitive local imports, async/await, and access to `process.env`. Errors
|
|
217
|
+
Custom hooks support transitive local imports, async/await, and access to `process.env`. Errors are fail-open (logged to `~/.failproofai/hook.log`, built-in policies continue). See [docs/custom-hooks.mdx](docs/custom-hooks.mdx) for the full guide.
|
|
217
218
|
|
|
218
219
|
---
|
|
219
220
|
|
|
@@ -233,14 +234,26 @@ FAILPROOFAI_TELEMETRY_DISABLED=1 failproofai
|
|
|
233
234
|
|
|
234
235
|
| Guide | Description |
|
|
235
236
|
|-------|-------------|
|
|
236
|
-
| [Getting Started](docs/getting-started.
|
|
237
|
-
| [
|
|
238
|
-
| [
|
|
239
|
-
| [
|
|
240
|
-
| [
|
|
241
|
-
| [
|
|
242
|
-
| [
|
|
243
|
-
|
|
237
|
+
| [Getting Started](docs/getting-started.mdx) | Installation and first steps |
|
|
238
|
+
| [Built-in Policies](docs/built-in-policies.mdx) | All 30 built-in policies with parameters |
|
|
239
|
+
| [Custom Policies](docs/custom-policies.mdx) | Write your own policies |
|
|
240
|
+
| [Configuration](docs/configuration.mdx) | Config file format and scope merging |
|
|
241
|
+
| [Dashboard](docs/dashboard.mdx) | Monitor sessions and review policy activity |
|
|
242
|
+
| [Architecture](docs/architecture.mdx) | How the hook system works |
|
|
243
|
+
| [Testing](docs/testing.mdx) | Running tests and writing new ones |
|
|
244
|
+
|
|
245
|
+
### Run docs locally
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
docker build -f Dockerfile.docs -t failproofai-docs .
|
|
249
|
+
docker run --rm -p 3000:3000 failproofai-docs
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Opens the Mintlify docs site at `http://localhost:3000`. The container watches for changes if you mount the docs directory:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
docker run --rm -p 3000:3000 -v $(pwd)/docs:/app/docs failproofai-docs
|
|
256
|
+
```
|
|
244
257
|
|
|
245
258
|
---
|
|
246
259
|
|