failproofai 0.0.6-beta.5 → 0.0.7

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.
Files changed (90) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/index.html +1 -1
  27. package/.next/standalone/.next/server/app/index.rsc +15 -15
  28. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  30. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  31. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  32. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  34. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  37. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0xezw2w._.js → [root-of-the-server]__0tjjyb9._.js} +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0u49i20._.js → [root-of-the-server]__0zn7uo6._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  64. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  65. package/.next/standalone/.next/server/pages/404.html +2 -2
  66. package/.next/standalone/.next/server/pages/500.html +1 -1
  67. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  69. package/.next/standalone/.next/static/chunks/{0ms9sfkvw5h5i.js → 01l2mh88iy.ga.js} +1 -1
  70. package/.next/standalone/.next/static/chunks/{0q~7ymasjp~n7.js → 0388wpenm9-a4.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{03xexm0jftw.x.js → 0a0lh_a4f_xs-.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{0_-7qmtiagi-~.js → 0f_9854du76y2.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{0af6vx3nh6v77.js → 0j2o20pqkib~d.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{11vga8q3t~527.js → 0kkzzoo.s-t3p.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0on5naeqa0w.x.js → 0x0o8~u4jsatb.js} +2 -2
  76. package/.next/standalone/.next/static/chunks/{0hrob6m-1.n1d.js → 12wu.28cbx4dl.js} +1 -1
  77. package/.next/standalone/failproofai-hq.gif +0 -0
  78. package/.next/standalone/package.json +1 -1
  79. package/.next/standalone/server.js +1 -1
  80. package/README.md +6 -2
  81. package/dist/cli.mjs +227 -52
  82. package/package.json +1 -1
  83. package/src/hooks/builtin-policies.ts +201 -41
  84. package/src/hooks/custom-hooks-loader.ts +6 -3
  85. package/src/hooks/hooks-config.ts +31 -5
  86. package/src/hooks/policy-evaluator.ts +31 -7
  87. package/src/hooks/policy-registry.ts +19 -2
  88. /package/.next/standalone/.next/static/{Q-y1SELeezrTyQx0E2uqg → 9FNjQiktocMN-qDiGqDL5}/_buildManifest.js +0 -0
  89. /package/.next/standalone/.next/static/{Q-y1SELeezrTyQx0E2uqg → 9FNjQiktocMN-qDiGqDL5}/_clientMiddlewareManifest.js +0 -0
  90. /package/.next/standalone/.next/static/{Q-y1SELeezrTyQx0E2uqg → 9FNjQiktocMN-qDiGqDL5}/_ssgManifest.js +0 -0
package/dist/cli.mjs CHANGED
@@ -103,7 +103,7 @@ var init_hook_logger = __esm(() => {
103
103
  });
104
104
 
105
105
  // src/hooks/hooks-config.ts
106
- import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
106
+ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2 } from "node:fs";
107
107
  import { resolve, dirname } from "node:path";
108
108
  import { homedir as homedir2 } from "node:os";
109
109
  function readConfigAt(path) {
@@ -117,8 +117,24 @@ function readConfigAt(path) {
117
117
  return {};
118
118
  }
119
119
  }
120
+ function findProjectConfigDir(start) {
121
+ const home = homedir2();
122
+ let dir = resolve(start);
123
+ while (dir !== home) {
124
+ const marker = resolve(dir, ".failproofai");
125
+ try {
126
+ if (statSync2(marker).isDirectory())
127
+ return dir;
128
+ } catch {}
129
+ const parent = dirname(dir);
130
+ if (parent === dir)
131
+ break;
132
+ dir = parent;
133
+ }
134
+ return resolve(start);
135
+ }
120
136
  function readMergedHooksConfig(cwd) {
121
- const base = cwd ? resolve(cwd) : process.cwd();
137
+ const base = findProjectConfigDir(cwd ?? process.cwd());
122
138
  const projectPath = resolve(base, ".failproofai", "policies-config.json");
123
139
  const localPath = resolve(base, ".failproofai", "policies-config.local.json");
124
140
  const globalPath = resolve(homedir2(), ".failproofai", "policies-config.json");
@@ -150,14 +166,13 @@ function readMergedHooksConfig(cwd) {
150
166
  };
151
167
  }
152
168
  function getConfigPathForScope(scope, cwd) {
153
- const base = cwd ? resolve(cwd) : process.cwd();
154
169
  switch (scope) {
155
170
  case "user":
156
171
  return resolve(homedir2(), ".failproofai", "policies-config.json");
157
172
  case "project":
158
- return resolve(base, ".failproofai", "policies-config.json");
173
+ return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.json");
159
174
  case "local":
160
- return resolve(base, ".failproofai", "policies-config.local.json");
175
+ return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.local.json");
161
176
  }
162
177
  }
163
178
  function readScopedHooksConfig(scope, cwd) {
@@ -198,6 +213,9 @@ function instruct(reason) {
198
213
  }
199
214
 
200
215
  // src/hooks/policy-registry.ts
216
+ function normalizePolicyName(name) {
217
+ return name.includes("/") ? name : `${DEFAULT_POLICY_NAMESPACE}/${name}`;
218
+ }
201
219
  function getIndexCache() {
202
220
  return globalThis[INDEX_CACHE_KEY];
203
221
  }
@@ -212,9 +230,10 @@ function getRegistry() {
212
230
  return g[REGISTRY_KEY];
213
231
  }
214
232
  function registerPolicy(name, description, fn, match, priority = 0) {
233
+ const canonical = normalizePolicyName(name);
215
234
  const registry = getRegistry();
216
- const idx = registry.findIndex((p) => p.name === name);
217
- const entry = { name, description, fn, match, priority };
235
+ const idx = registry.findIndex((p) => p.name === canonical);
236
+ const entry = { name: canonical, description, fn, match, priority };
218
237
  if (idx >= 0) {
219
238
  registry[idx] = entry;
220
239
  } else {
@@ -251,7 +270,7 @@ function clearPolicies() {
251
270
  g[REGISTRY_KEY] = [];
252
271
  setIndexCache(null);
253
272
  }
254
- var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__";
273
+ var REGISTRY_KEY = "__FAILPROOFAI_POLICY_REGISTRY__", INDEX_CACHE_KEY = "__FAILPROOFAI_POLICY_INDEX_CACHE__", DEFAULT_POLICY_NAMESPACE = "exospherehost";
255
274
 
256
275
  // src/hooks/builtin-policies.ts
257
276
  import { resolve as resolve2, join as join2 } from "node:path";
@@ -764,6 +783,38 @@ function blockFailproofaiCommands(ctx) {
764
783
  }
765
784
  return allow();
766
785
  }
786
+ function blockInfraCli(ctx, re, denyMsg) {
787
+ if (ctx.toolName !== "Bash")
788
+ return allow();
789
+ const cmd = getCommand(ctx);
790
+ if (!re.test(cmd))
791
+ return allow();
792
+ const allowPatterns = ctx.params?.allowPatterns ?? [];
793
+ if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p)))
794
+ return allow();
795
+ return deny(denyMsg);
796
+ }
797
+ function blockKubectl(ctx) {
798
+ return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
799
+ }
800
+ function blockTerraform(ctx) {
801
+ return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
802
+ }
803
+ function blockAwsCli(ctx) {
804
+ return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
805
+ }
806
+ function blockGcloud(ctx) {
807
+ return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
808
+ }
809
+ function blockAzCli(ctx) {
810
+ return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
811
+ }
812
+ function blockHelm(ctx) {
813
+ return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
814
+ }
815
+ function blockGhPipeline(ctx) {
816
+ return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
817
+ }
767
818
  async function warnRepeatedToolCalls(ctx) {
768
819
  const THRESHOLD = 3;
769
820
  const transcriptPath = ctx.session?.transcriptPath;
@@ -1056,7 +1107,31 @@ function requireNoConflictsBeforeStop(ctx) {
1056
1107
  if (branch === baseBranch) {
1057
1108
  return allow(`On base branch "${baseBranch}", skipping conflict check.`);
1058
1109
  }
1059
- let localSkipped = false;
1110
+ try {
1111
+ execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1112
+ } catch {
1113
+ return allow("gh CLI not installed, skipping conflict check.");
1114
+ }
1115
+ let prJson;
1116
+ try {
1117
+ prJson = execSync("gh pr view --json mergeable,number,url,state", {
1118
+ cwd,
1119
+ encoding: "utf8",
1120
+ stdio: ["pipe", "pipe", "pipe"],
1121
+ timeout: 15000
1122
+ }).trim();
1123
+ } catch {
1124
+ return allow("No pull request found for branch, skipping conflict check.");
1125
+ }
1126
+ let pr;
1127
+ try {
1128
+ pr = JSON.parse(prJson);
1129
+ } catch {
1130
+ return allow("Could not parse gh pr view output, skipping conflict check.");
1131
+ }
1132
+ if (pr.state !== "OPEN") {
1133
+ return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
1134
+ }
1060
1135
  try {
1061
1136
  execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
1062
1137
  cwd,
@@ -1065,9 +1140,7 @@ function requireNoConflictsBeforeStop(ctx) {
1065
1140
  timeout: 3000
1066
1141
  });
1067
1142
  const ahead = execFileSync("git", ["log", `origin/${baseBranch}..HEAD`, "--oneline"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
1068
- if (!ahead) {
1069
- localSkipped = true;
1070
- } else {
1143
+ if (ahead) {
1071
1144
  execFileSync("git", ["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"], { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 });
1072
1145
  }
1073
1146
  } catch (err) {
@@ -1086,29 +1159,6 @@ function requireNoConflictsBeforeStop(ctx) {
1086
1159
  const fileList = files.length ? files.join(", ") : "one or more files";
1087
1160
  return deny(`Branch "${branch}" has merge conflicts with ${baseBranch} in: ${fileList}. ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
1088
1161
  }
1089
- localSkipped = true;
1090
- }
1091
- try {
1092
- execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
1093
- } catch {
1094
- return allow(localSkipped ? "Local conflict check skipped and gh CLI not installed, skipping conflict check." : `Branch "${branch}" merges cleanly with ${baseBranch} locally (gh CLI not installed, PR mergeability not verified).`);
1095
- }
1096
- let prJson;
1097
- try {
1098
- prJson = execSync("gh pr view --json mergeable,number,url", {
1099
- cwd,
1100
- encoding: "utf8",
1101
- stdio: ["pipe", "pipe", "pipe"],
1102
- timeout: 15000
1103
- }).trim();
1104
- } catch {
1105
- return allow(localSkipped ? "No pull request found for branch, skipping conflict check." : `Branch "${branch}" merges cleanly with ${baseBranch} locally (no PR to verify against).`);
1106
- }
1107
- let pr;
1108
- try {
1109
- pr = JSON.parse(prJson);
1110
- } catch {
1111
- return allow("Could not parse gh pr view output, skipping PR mergeability check.");
1112
1162
  }
1113
1163
  if (pr.mergeable === "CONFLICTING") {
1114
1164
  return deny(`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` + `Rebase or merge origin/${baseBranch} now and resolve the conflicts.`);
@@ -1164,14 +1214,14 @@ function requireCiGreenBeforeStop(ctx) {
1164
1214
  }
1165
1215
  }
1166
1216
  function registerBuiltinPolicies(enabledNames) {
1167
- const enabledSet = new Set(enabledNames);
1217
+ const enabledSet = new Set(enabledNames.map(normalizePolicyName));
1168
1218
  for (const policy of BUILTIN_POLICIES) {
1169
- if (enabledSet.has(policy.name)) {
1219
+ if (enabledSet.has(normalizePolicyName(policy.name))) {
1170
1220
  registerPolicy(policy.name, policy.description, policy.fn, policy.match);
1171
1221
  }
1172
1222
  }
1173
1223
  }
1174
- var SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1224
+ var SHELL_OPERATORS, SHELL_METACHAR_RE, JWT_RE, API_KEY_PATTERNS, CONNECTION_STRING_RE, PRIVATE_KEY_RE, BEARER_TOKEN_RE, SQL_TOOL_RE, DESTRUCTIVE_SQL_RE, DELETE_NO_WHERE_RE, SQL_WHERE_RE, SCHEMA_ALTER_RE, PUBLISH_CMD_RE, ENV_PRINTENV_RE, ECHO_ENV_RE, EXPORT_RE, PS_ENV_VAR_RE, PS_CHILDITEM_ENV_RE, DOTNET_GETENV_RE, CMD_ECHO_ENV_RE, ENV_FILE_PATH_RE, ENV_CMD_RE, SUDO_RE, PS_ELEVATION_RE, RUNAS_RE, CURL_PIPE_SH_RE, PS_WEB_PIPE_RE, FORCE_PUSH_RE, SECRET_FILE_RE, SECRET_FILE_ID_RSA_RE, SECRET_FILE_CREDENTIALS_RE, GIT_COMMIT_MERGE_RE, FAILPROOFAI_CLI_RE, FAILPROOFAI_UNINSTALL_RE, GIT_AMEND_RE, GIT_STASH_DROP_RE, GIT_ADD_ALL_RE, NPM_GLOBAL_RE, YARN_GLOBAL_RE, PNPM_GLOBAL_RE, BUN_GLOBAL_RE, CARGO_INSTALL_RE, PIP_SYSTEM_RE, PKG_MANAGER_DETECTORS, NOHUP_RE, SCREEN_DETACH_RE, TMUX_DETACH_RE, DISOWN_RE, BACKGROUND_AMPERSAND_RE, KUBECTL_RE, TERRAFORM_RE, AWS_CLI_RE, GCLOUD_RE, AZ_CLI_RE, HELM_RE, GH_PIPELINE_RE, gitBranchCache, READ_LIKE_CMDS, TOOL_CALL_TRACKER_MAX_BYTES = 65536, SEGMENT_SPLIT_RE, BUILTIN_POLICIES;
1175
1225
  var init_builtin_policies = __esm(() => {
1176
1226
  init_hook_logger();
1177
1227
  SHELL_OPERATORS = new Set(["&&", "||", "|", ";"]);
@@ -1244,6 +1294,13 @@ var init_builtin_policies = __esm(() => {
1244
1294
  TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
1245
1295
  DISOWN_RE = /\bdisown\b/;
1246
1296
  BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
1297
+ KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
1298
+ TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
1299
+ AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
1300
+ GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
1301
+ AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
1302
+ HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
1303
+ GH_PIPELINE_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gh\s+(?:workflow\s+(?:run|enable|disable)|run\s+(?:rerun|cancel)|pr\s+merge|release\s+(?:create|delete)|cache\s+delete|secret\s+(?:set|delete))\b/;
1247
1304
  gitBranchCache = new Map;
1248
1305
  READ_LIKE_CMDS = /(?:^|;|&&|\|\||\|)\s*(?:ls|find|cat|head|tail|less|more|wc|file|stat|tree|du)\s/;
1249
1306
  SEGMENT_SPLIT_RE = /\s*(?:&&|\|\||\||;)\s*/;
@@ -1372,6 +1429,111 @@ var init_builtin_policies = __esm(() => {
1372
1429
  defaultEnabled: true,
1373
1430
  category: "Dangerous Commands"
1374
1431
  },
1432
+ {
1433
+ name: "block-kubectl",
1434
+ description: "Block kubectl commands (Kubernetes cluster mutations)",
1435
+ fn: blockKubectl,
1436
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1437
+ defaultEnabled: false,
1438
+ category: "Infra Commands",
1439
+ params: {
1440
+ allowPatterns: {
1441
+ type: "string[]",
1442
+ description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
1443
+ default: []
1444
+ }
1445
+ }
1446
+ },
1447
+ {
1448
+ name: "block-terraform",
1449
+ description: "Block terraform and tofu (OpenTofu) commands",
1450
+ fn: blockTerraform,
1451
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1452
+ defaultEnabled: false,
1453
+ category: "Infra Commands",
1454
+ params: {
1455
+ allowPatterns: {
1456
+ type: "string[]",
1457
+ description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
1458
+ default: []
1459
+ }
1460
+ }
1461
+ },
1462
+ {
1463
+ name: "block-aws-cli",
1464
+ description: "Block aws CLI commands",
1465
+ fn: blockAwsCli,
1466
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1467
+ defaultEnabled: false,
1468
+ category: "Infra Commands",
1469
+ params: {
1470
+ allowPatterns: {
1471
+ type: "string[]",
1472
+ description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
1473
+ default: []
1474
+ }
1475
+ }
1476
+ },
1477
+ {
1478
+ name: "block-gcloud",
1479
+ description: "Block gcloud (Google Cloud) CLI commands",
1480
+ fn: blockGcloud,
1481
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1482
+ defaultEnabled: false,
1483
+ category: "Infra Commands",
1484
+ params: {
1485
+ allowPatterns: {
1486
+ type: "string[]",
1487
+ description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
1488
+ default: []
1489
+ }
1490
+ }
1491
+ },
1492
+ {
1493
+ name: "block-az-cli",
1494
+ description: "Block az (Azure) CLI commands",
1495
+ fn: blockAzCli,
1496
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1497
+ defaultEnabled: false,
1498
+ category: "Infra Commands",
1499
+ params: {
1500
+ allowPatterns: {
1501
+ type: "string[]",
1502
+ description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
1503
+ default: []
1504
+ }
1505
+ }
1506
+ },
1507
+ {
1508
+ name: "block-helm",
1509
+ description: "Block helm commands",
1510
+ fn: blockHelm,
1511
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1512
+ defaultEnabled: false,
1513
+ category: "Infra Commands",
1514
+ params: {
1515
+ allowPatterns: {
1516
+ type: "string[]",
1517
+ description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
1518
+ default: []
1519
+ }
1520
+ }
1521
+ },
1522
+ {
1523
+ name: "block-gh-pipeline",
1524
+ description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
1525
+ fn: blockGhPipeline,
1526
+ match: { events: ["PreToolUse"], toolNames: ["Bash"] },
1527
+ defaultEnabled: false,
1528
+ category: "Infra Commands",
1529
+ params: {
1530
+ allowPatterns: {
1531
+ type: "string[]",
1532
+ description: "gh pipeline command patterns to allow (e.g. specific scripted invocations); read-only gh subcommands like 'gh pr view' and 'gh run list' are not matched by this policy",
1533
+ default: []
1534
+ }
1535
+ }
1536
+ },
1375
1537
  {
1376
1538
  name: "block-secrets-write",
1377
1539
  description: "Block writing secret key files",
@@ -1611,6 +1773,17 @@ function appendHint(baseReason, hint) {
1611
1773
  return normalizedHint;
1612
1774
  return `${base}. ${normalizedHint}`;
1613
1775
  }
1776
+ function getConfigParamsFor(config, canonicalName) {
1777
+ if (!config?.policyParams)
1778
+ return;
1779
+ const canonicalParams = config.policyParams[canonicalName];
1780
+ if (canonicalParams)
1781
+ return canonicalParams;
1782
+ const defaultPrefix = `${DEFAULT_POLICY_NAMESPACE}/`;
1783
+ if (!canonicalName.startsWith(defaultPrefix))
1784
+ return;
1785
+ return config.policyParams[canonicalName.slice(defaultPrefix.length)];
1786
+ }
1614
1787
  async function evaluatePolicies(eventType, payload, session, config) {
1615
1788
  const toolName = payload.tool_name;
1616
1789
  const toolInput = payload.tool_input;
@@ -1632,7 +1805,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1632
1805
  const schema = POLICY_PARAMS_MAP.get(policy.name);
1633
1806
  let ctx;
1634
1807
  if (schema) {
1635
- const userParams = config?.policyParams?.[policy.name] ?? {};
1808
+ const userParams = getConfigParamsFor(config, policy.name) ?? {};
1636
1809
  const resolvedParams = {};
1637
1810
  for (const [key, spec] of Object.entries(schema)) {
1638
1811
  resolvedParams[key] = key in userParams ? userParams[key] : spec.default;
@@ -1649,7 +1822,7 @@ async function evaluatePolicies(eventType, payload, session, config) {
1649
1822
  continue;
1650
1823
  }
1651
1824
  if (result.decision === "deny") {
1652
- const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1825
+ const reason = appendHint(result.reason ?? `Blocked by policy: ${policy.name}`, getConfigParamsFor(config, policy.name)?.hint);
1653
1826
  hookLogInfo(`deny by "${policy.name}": ${reason}`);
1654
1827
  const displayTool = ctx.toolName ?? "unknown tool";
1655
1828
  if (eventType === "PreToolUse") {
@@ -1707,7 +1880,7 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
1707
1880
  };
1708
1881
  }
1709
1882
  if (result.decision === "instruct") {
1710
- const reason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, config?.policyParams?.[policy.name]?.hint);
1883
+ const reason = appendHint(result.reason ?? `Instruction from policy: ${policy.name}`, getConfigParamsFor(config, policy.name)?.hint);
1711
1884
  instructEntries.push({ policyName: policy.name, reason });
1712
1885
  hookLogInfo(`instruct by "${policy.name}": ${reason}`);
1713
1886
  }
@@ -1766,7 +1939,7 @@ var POLICY_PARAMS_MAP;
1766
1939
  var init_policy_evaluator = __esm(() => {
1767
1940
  init_builtin_policies();
1768
1941
  init_hook_logger();
1769
- POLICY_PARAMS_MAP = new Map(BUILTIN_POLICIES.filter((p) => p.params).map((p) => [p.name, p.params]));
1942
+ POLICY_PARAMS_MAP = new Map(BUILTIN_POLICIES.filter((p) => p.params).map((p) => [normalizePolicyName(p.name), p.params]));
1770
1943
  });
1771
1944
 
1772
1945
  // src/hooks/custom-hooks-registry.ts
@@ -1963,8 +2136,9 @@ async function loadCustomHooks(customPoliciesPath, opts) {
1963
2136
  async function loadAllCustomHooks(customPoliciesPath, opts) {
1964
2137
  clearCustomHooks();
1965
2138
  const conventionSources = [];
2139
+ const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
1966
2140
  if (customPoliciesPath) {
1967
- const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(opts?.sessionCwd ?? process.cwd(), customPoliciesPath);
2141
+ const absPath = isAbsolute(customPoliciesPath) ? customPoliciesPath : resolve4(projectRoot, customPoliciesPath);
1968
2142
  if (existsSync3(absPath)) {
1969
2143
  await loadSingleFile(absPath);
1970
2144
  } else {
@@ -1972,7 +2146,7 @@ async function loadAllCustomHooks(customPoliciesPath, opts) {
1972
2146
  }
1973
2147
  }
1974
2148
  const hooksBeforeConvention = getCustomHooks().length;
1975
- const projectDir = resolve4(opts?.sessionCwd ?? process.cwd(), ".failproofai", "policies");
2149
+ const projectDir = resolve4(projectRoot, ".failproofai", "policies");
1976
2150
  const projectFiles = discoverPolicyFiles(projectDir);
1977
2151
  for (const file of projectFiles) {
1978
2152
  const hooksBefore = getCustomHooks().length;
@@ -2027,6 +2201,7 @@ var init_custom_hooks_loader = __esm(() => {
2027
2201
  init_hook_logger();
2028
2202
  init_custom_hooks_registry();
2029
2203
  init_loader_utils();
2204
+ init_hooks_config();
2030
2205
  CONVENTION_FILE_RE = /policies\.(js|mjs|ts)$/;
2031
2206
  });
2032
2207
 
@@ -2039,7 +2214,7 @@ import {
2039
2214
  readdirSync as readdirSync2,
2040
2215
  mkdirSync as mkdirSync3,
2041
2216
  existsSync as existsSync4,
2042
- statSync as statSync2,
2217
+ statSync as statSync3,
2043
2218
  unlinkSync
2044
2219
  } from "node:fs";
2045
2220
  import { join as join3 } from "node:path";
@@ -2061,7 +2236,7 @@ function acquireLock() {
2061
2236
  if (e.code !== "EEXIST")
2062
2237
  return;
2063
2238
  try {
2064
- const s = statSync2(lockPath);
2239
+ const s = statSync3(lockPath);
2065
2240
  if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
2066
2241
  writeFileSync2(lockPath, String(process.pid), "utf-8");
2067
2242
  return;
@@ -2153,7 +2328,7 @@ var init_hook_activity_store = __esm(() => {
2153
2328
  });
2154
2329
 
2155
2330
  // package.json
2156
- var version2 = "0.0.6-beta.5";
2331
+ var version2 = "0.0.7";
2157
2332
  var init_package = () => {};
2158
2333
 
2159
2334
  // src/posthog-key.ts
@@ -2331,7 +2506,7 @@ import {
2331
2506
  mkdirSync as mkdirSync5,
2332
2507
  existsSync as existsSync6,
2333
2508
  readFileSync as readFileSync4,
2334
- statSync as statSync3,
2509
+ statSync as statSync4,
2335
2510
  renameSync as renameSync4,
2336
2511
  unlinkSync as unlinkSync3,
2337
2512
  readdirSync as readdirSync3,
@@ -2377,7 +2552,7 @@ function appendToServerQueue(entry) {
2377
2552
  return;
2378
2553
  ensureDir2();
2379
2554
  try {
2380
- if (existsSync6(PENDING_FILE) && statSync3(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2555
+ if (existsSync6(PENDING_FILE) && statSync4(PENDING_FILE).size > MAX_QUEUE_BYTES) {
2381
2556
  return;
2382
2557
  }
2383
2558
  } catch {}
@@ -2390,7 +2565,7 @@ function appendToServerQueue(entry) {
2390
2565
  }
2391
2566
  function queueSizeBytes() {
2392
2567
  try {
2393
- return statSync3(PENDING_FILE).size;
2568
+ return statSync4(PENDING_FILE).size;
2394
2569
  } catch {
2395
2570
  return 0;
2396
2571
  }
@@ -2399,7 +2574,7 @@ function claimPendingBatch() {
2399
2574
  if (!existsSync6(PENDING_FILE))
2400
2575
  return null;
2401
2576
  try {
2402
- const size = statSync3(PENDING_FILE).size;
2577
+ const size = statSync4(PENDING_FILE).size;
2403
2578
  if (size === 0)
2404
2579
  return null;
2405
2580
  } catch {
@@ -4466,7 +4641,7 @@ import { realpathSync as realpathSync2 } from "fs";
4466
4641
  import { dirname as dirname7, resolve as resolve8 } from "path";
4467
4642
  import { fileURLToPath as fileURLToPath2 } from "url";
4468
4643
  // package.json
4469
- var version = "0.0.6-beta.5";
4644
+ var version = "0.0.7";
4470
4645
 
4471
4646
  // bin/failproofai.mjs
4472
4647
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.6-beta.5",
3
+ "version": "0.0.7",
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"