failproofai 0.0.6 → 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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- 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 +10 -10
- 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 +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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]__0g72weg._.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]__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]__0okos0k._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0t3ka1q._.js → [root-of-the-server]__0tjjyb9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +7 -7
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ow37ro._.js → [root-of-the-server]__0zn7uo6._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{0lq8ary5l4s8t.js → 01l2mh88iy.ga.js} +1 -1
- package/.next/standalone/.next/static/chunks/{04iuhj_-h-21-.js → 0388wpenm9-a4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ubv3x~0zdd_w.js → 0a0lh_a4f_xs-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{13cot7j99xkb~.js → 0f_9854du76y2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-igg2k65fzo_.js → 0j2o20pqkib~d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k5t-n0s8p2nr.js → 0kkzzoo.s-t3p.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0v23ca5xty1n~.js → 0x0o8~u4jsatb.js} +2 -2
- package/.next/standalone/.next/static/chunks/{061hxr2b-j.6q.js → 12wu.28cbx4dl.js} +1 -1
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +203 -46
- package/package.json +1 -1
- package/src/hooks/builtin-policies.ts +196 -44
- package/src/hooks/custom-hooks-loader.ts +6 -3
- package/src/hooks/hooks-config.ts +31 -5
- /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{my01WPjry7ohRUHyTaYp4 → 9FNjQiktocMN-qDiGqDL5}/_ssgManifest.js +0 -0
|
@@ -159,6 +159,21 @@ const TMUX_DETACH_RE = /\btmux\s+(?:new-session|new)\b[^|&;]*-d\b/;
|
|
|
159
159
|
const DISOWN_RE = /\bdisown\b/;
|
|
160
160
|
const BACKGROUND_AMPERSAND_RE = /(?<![&|])\s?&\s*(?:$|#|;)/;
|
|
161
161
|
|
|
162
|
+
// Infra Commands — leading-token detection across shell separators.
|
|
163
|
+
// Each regex matches the CLI name only when it appears as the first token of a
|
|
164
|
+
// command segment (start-of-string or after ; && || |). Trailing \s prevents
|
|
165
|
+
// false matches on names like "kubectlx" or "awsctl".
|
|
166
|
+
const KUBECTL_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*kubectl(?:\s|$)/;
|
|
167
|
+
const TERRAFORM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*(?:terraform|tofu)(?:\s|$)/;
|
|
168
|
+
const AWS_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*aws(?:\s|$)/;
|
|
169
|
+
const GCLOUD_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*gcloud(?:\s|$)/;
|
|
170
|
+
const AZ_CLI_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*az(?:\s|$)/;
|
|
171
|
+
const HELM_RE = /(?:^|[;\n]|&&|\|\|?|&)\s*helm(?:\s|$)/;
|
|
172
|
+
// gh: only mutating / pipeline-trigger subcommands. Read-only forms
|
|
173
|
+
// (gh pr view, gh run list, gh api ...) are intentionally allowed because
|
|
174
|
+
// failproofai's own workflow policies depend on them.
|
|
175
|
+
const 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/;
|
|
176
|
+
|
|
162
177
|
// Caches the current branch per cwd to avoid repeated execSync calls.
|
|
163
178
|
// Trade-off: if the user switches branches externally mid-session, the cache serves
|
|
164
179
|
// the stale value until the process restarts. This is acceptable since branch switches
|
|
@@ -770,6 +785,48 @@ function blockFailproofaiCommands(ctx: PolicyContext): PolicyResult {
|
|
|
770
785
|
return allow();
|
|
771
786
|
}
|
|
772
787
|
|
|
788
|
+
// Shared CLI-blocker: deny any command whose argv begins with the matched CLI,
|
|
789
|
+
// unless an entry in `allowPatterns` matches via `matchesAllowedPattern` (which
|
|
790
|
+
// already defends against shell-operator injection).
|
|
791
|
+
function blockInfraCli(ctx: PolicyContext, re: RegExp, denyMsg: string): PolicyResult {
|
|
792
|
+
if (ctx.toolName !== "Bash") return allow();
|
|
793
|
+
const cmd = getCommand(ctx);
|
|
794
|
+
if (!re.test(cmd)) return allow();
|
|
795
|
+
const allowPatterns = ((ctx.params?.allowPatterns ?? []) as string[]);
|
|
796
|
+
if (allowPatterns.some((p) => matchesAllowedPattern(cmd, p))) return allow();
|
|
797
|
+
return deny(denyMsg);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function blockKubectl(ctx: PolicyContext): PolicyResult {
|
|
801
|
+
return blockInfraCli(ctx, KUBECTL_RE, "kubectl commands are blocked");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function blockTerraform(ctx: PolicyContext): PolicyResult {
|
|
805
|
+
return blockInfraCli(ctx, TERRAFORM_RE, "terraform/tofu commands are blocked");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function blockAwsCli(ctx: PolicyContext): PolicyResult {
|
|
809
|
+
return blockInfraCli(ctx, AWS_CLI_RE, "aws CLI commands are blocked");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function blockGcloud(ctx: PolicyContext): PolicyResult {
|
|
813
|
+
return blockInfraCli(ctx, GCLOUD_RE, "gcloud commands are blocked");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function blockAzCli(ctx: PolicyContext): PolicyResult {
|
|
817
|
+
return blockInfraCli(ctx, AZ_CLI_RE, "az (Azure) CLI commands are blocked");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function blockHelm(ctx: PolicyContext): PolicyResult {
|
|
821
|
+
return blockInfraCli(ctx, HELM_RE, "helm commands are blocked");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// gh-pipeline only fires on mutating subcommands; allowPatterns are still
|
|
825
|
+
// supported in case a user wants to permit a specific scripted invocation.
|
|
826
|
+
function blockGhPipeline(ctx: PolicyContext): PolicyResult {
|
|
827
|
+
return blockInfraCli(ctx, GH_PIPELINE_RE, "gh pipeline-trigger commands are blocked");
|
|
828
|
+
}
|
|
829
|
+
|
|
773
830
|
// Maximum size of the per-session tool-call sidecar before we stop updating it.
|
|
774
831
|
// If exceeded, repeated-call detection degrades gracefully (allows through) rather
|
|
775
832
|
// than growing the file unboundedly.
|
|
@@ -1197,8 +1254,37 @@ function requireNoConflictsBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1197
1254
|
return allow(`On base branch "${baseBranch}", skipping conflict check.`);
|
|
1198
1255
|
}
|
|
1199
1256
|
|
|
1257
|
+
// -- Precheck: only enforce when an OPEN PR exists on GitHub. Without a
|
|
1258
|
+
// confirmable merge target there is nothing to enforce, so we skip both
|
|
1259
|
+
// the local merge-tree probe and the GitHub mergeability probe.
|
|
1260
|
+
try {
|
|
1261
|
+
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1262
|
+
} catch {
|
|
1263
|
+
return allow("gh CLI not installed, skipping conflict check.");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
let prJson: string;
|
|
1267
|
+
try {
|
|
1268
|
+
prJson = execSync("gh pr view --json mergeable,number,url,state", {
|
|
1269
|
+
cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000,
|
|
1270
|
+
}).trim();
|
|
1271
|
+
} catch {
|
|
1272
|
+
return allow("No pull request found for branch, skipping conflict check.");
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
let pr: { mergeable: string; number: number; url: string; state: string };
|
|
1276
|
+
try {
|
|
1277
|
+
pr = JSON.parse(prJson);
|
|
1278
|
+
} catch {
|
|
1279
|
+
return allow("Could not parse gh pr view output, skipping conflict check.");
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// GitHub stops computing mergeability for non-OPEN PRs (returns UNKNOWN forever).
|
|
1283
|
+
if (pr.state !== "OPEN") {
|
|
1284
|
+
return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1200
1287
|
// -- Layer 1: local git merge-tree --
|
|
1201
|
-
let localSkipped = false;
|
|
1202
1288
|
try {
|
|
1203
1289
|
execFileSync("git", ["rev-parse", "--verify", `origin/${baseBranch}`], {
|
|
1204
1290
|
cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000,
|
|
@@ -1209,17 +1295,14 @@ function requireNoConflictsBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1209
1295
|
{ cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
|
|
1210
1296
|
).trim();
|
|
1211
1297
|
|
|
1212
|
-
if (
|
|
1213
|
-
// Nothing ahead of base — Layer 1 doesn't apply, fall through to Layer 2.
|
|
1214
|
-
localSkipped = true;
|
|
1215
|
-
} else {
|
|
1298
|
+
if (ahead) {
|
|
1216
1299
|
execFileSync(
|
|
1217
1300
|
"git",
|
|
1218
1301
|
["merge-tree", "--write-tree", "--name-only", `origin/${baseBranch}`, "HEAD"],
|
|
1219
1302
|
{ cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 },
|
|
1220
1303
|
);
|
|
1221
|
-
// exit 0 → clean merge, fall through to Layer 2
|
|
1222
1304
|
}
|
|
1305
|
+
// !ahead or merge-tree exit 0 → fall through to Layer 2
|
|
1223
1306
|
} catch (err) {
|
|
1224
1307
|
const e = err as { status?: number; stdout?: string | Buffer };
|
|
1225
1308
|
if (e.status === 1) {
|
|
@@ -1238,46 +1321,10 @@ function requireNoConflictsBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1238
1321
|
`Rebase or merge origin/${baseBranch} now and resolve the conflicts.`,
|
|
1239
1322
|
);
|
|
1240
1323
|
}
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// -- Layer 2: GitHub PR mergeability --
|
|
1245
|
-
try {
|
|
1246
|
-
execSync("gh --version", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1247
|
-
} catch {
|
|
1248
|
-
return allow(
|
|
1249
|
-
localSkipped
|
|
1250
|
-
? "Local conflict check skipped and gh CLI not installed, skipping conflict check."
|
|
1251
|
-
: `Branch "${branch}" merges cleanly with ${baseBranch} locally (gh CLI not installed, PR mergeability not verified).`,
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
let prJson: string;
|
|
1256
|
-
try {
|
|
1257
|
-
prJson = execSync("gh pr view --json mergeable,number,url,state", {
|
|
1258
|
-
cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000,
|
|
1259
|
-
}).trim();
|
|
1260
|
-
} catch {
|
|
1261
|
-
return allow(
|
|
1262
|
-
localSkipped
|
|
1263
|
-
? "No pull request found for branch, skipping conflict check."
|
|
1264
|
-
: `Branch "${branch}" merges cleanly with ${baseBranch} locally (no PR to verify against).`,
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
let pr: { mergeable: string; number: number; url: string; state: string };
|
|
1269
|
-
try {
|
|
1270
|
-
pr = JSON.parse(prJson);
|
|
1271
|
-
} catch {
|
|
1272
|
-
return allow("Could not parse gh pr view output, skipping PR mergeability check.");
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// GitHub stops computing mergeability for non-OPEN PRs (returns UNKNOWN forever).
|
|
1276
|
-
// Skip the check entirely so a merged or closed PR doesn't trap Stop in a wait loop.
|
|
1277
|
-
if (pr.state !== "OPEN") {
|
|
1278
|
-
return allow(`PR #${pr.number} is ${pr.state.toLowerCase()}; skipping conflict check.`);
|
|
1324
|
+
// any other failure (e.g. missing origin/<base>, log failure) → fall through
|
|
1279
1325
|
}
|
|
1280
1326
|
|
|
1327
|
+
// -- Layer 2: GitHub PR mergeability (reuses pr from precheck) --
|
|
1281
1328
|
if (pr.mergeable === "CONFLICTING") {
|
|
1282
1329
|
return deny(
|
|
1283
1330
|
`PR #${pr.number} has merge conflicts per GitHub (${pr.url}). ` +
|
|
@@ -1495,6 +1542,111 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
|
1495
1542
|
defaultEnabled: true,
|
|
1496
1543
|
category: "Dangerous Commands",
|
|
1497
1544
|
},
|
|
1545
|
+
{
|
|
1546
|
+
name: "block-kubectl",
|
|
1547
|
+
description: "Block kubectl commands (Kubernetes cluster mutations)",
|
|
1548
|
+
fn: blockKubectl,
|
|
1549
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1550
|
+
defaultEnabled: false,
|
|
1551
|
+
category: "Infra Commands",
|
|
1552
|
+
params: {
|
|
1553
|
+
allowPatterns: {
|
|
1554
|
+
type: "string[]",
|
|
1555
|
+
description: "kubectl command patterns to allow, matched token-by-token (e.g. 'kubectl get *', 'kubectl describe *')",
|
|
1556
|
+
default: [],
|
|
1557
|
+
},
|
|
1558
|
+
} satisfies PolicyParamsSchema,
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
name: "block-terraform",
|
|
1562
|
+
description: "Block terraform and tofu (OpenTofu) commands",
|
|
1563
|
+
fn: blockTerraform,
|
|
1564
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1565
|
+
defaultEnabled: false,
|
|
1566
|
+
category: "Infra Commands",
|
|
1567
|
+
params: {
|
|
1568
|
+
allowPatterns: {
|
|
1569
|
+
type: "string[]",
|
|
1570
|
+
description: "terraform/tofu command patterns to allow (e.g. 'terraform plan', 'terraform validate')",
|
|
1571
|
+
default: [],
|
|
1572
|
+
},
|
|
1573
|
+
} satisfies PolicyParamsSchema,
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: "block-aws-cli",
|
|
1577
|
+
description: "Block aws CLI commands",
|
|
1578
|
+
fn: blockAwsCli,
|
|
1579
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1580
|
+
defaultEnabled: false,
|
|
1581
|
+
category: "Infra Commands",
|
|
1582
|
+
params: {
|
|
1583
|
+
allowPatterns: {
|
|
1584
|
+
type: "string[]",
|
|
1585
|
+
description: "aws CLI command patterns to allow (e.g. 'aws s3 ls *', 'aws sts get-caller-identity')",
|
|
1586
|
+
default: [],
|
|
1587
|
+
},
|
|
1588
|
+
} satisfies PolicyParamsSchema,
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: "block-gcloud",
|
|
1592
|
+
description: "Block gcloud (Google Cloud) CLI commands",
|
|
1593
|
+
fn: blockGcloud,
|
|
1594
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1595
|
+
defaultEnabled: false,
|
|
1596
|
+
category: "Infra Commands",
|
|
1597
|
+
params: {
|
|
1598
|
+
allowPatterns: {
|
|
1599
|
+
type: "string[]",
|
|
1600
|
+
description: "gcloud command patterns to allow (e.g. 'gcloud auth list', 'gcloud config list')",
|
|
1601
|
+
default: [],
|
|
1602
|
+
},
|
|
1603
|
+
} satisfies PolicyParamsSchema,
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
name: "block-az-cli",
|
|
1607
|
+
description: "Block az (Azure) CLI commands",
|
|
1608
|
+
fn: blockAzCli,
|
|
1609
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1610
|
+
defaultEnabled: false,
|
|
1611
|
+
category: "Infra Commands",
|
|
1612
|
+
params: {
|
|
1613
|
+
allowPatterns: {
|
|
1614
|
+
type: "string[]",
|
|
1615
|
+
description: "az CLI command patterns to allow (e.g. 'az account show', 'az group list')",
|
|
1616
|
+
default: [],
|
|
1617
|
+
},
|
|
1618
|
+
} satisfies PolicyParamsSchema,
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
name: "block-helm",
|
|
1622
|
+
description: "Block helm commands",
|
|
1623
|
+
fn: blockHelm,
|
|
1624
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1625
|
+
defaultEnabled: false,
|
|
1626
|
+
category: "Infra Commands",
|
|
1627
|
+
params: {
|
|
1628
|
+
allowPatterns: {
|
|
1629
|
+
type: "string[]",
|
|
1630
|
+
description: "helm command patterns to allow (e.g. 'helm list', 'helm status *')",
|
|
1631
|
+
default: [],
|
|
1632
|
+
},
|
|
1633
|
+
} satisfies PolicyParamsSchema,
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
name: "block-gh-pipeline",
|
|
1637
|
+
description: "Block gh CLI pipeline-trigger subcommands (workflow run, run rerun/cancel, pr merge, release create/delete, cache delete, secret set/delete)",
|
|
1638
|
+
fn: blockGhPipeline,
|
|
1639
|
+
match: { events: ["PreToolUse"], toolNames: ["Bash"] },
|
|
1640
|
+
defaultEnabled: false,
|
|
1641
|
+
category: "Infra Commands",
|
|
1642
|
+
params: {
|
|
1643
|
+
allowPatterns: {
|
|
1644
|
+
type: "string[]",
|
|
1645
|
+
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",
|
|
1646
|
+
default: [],
|
|
1647
|
+
},
|
|
1648
|
+
} satisfies PolicyParamsSchema,
|
|
1649
|
+
},
|
|
1498
1650
|
{
|
|
1499
1651
|
name: "block-secrets-write",
|
|
1500
1652
|
description: "Block writing secret key files",
|
|
@@ -17,6 +17,7 @@ import { homedir } from "node:os";
|
|
|
17
17
|
import { hookLogWarn, hookLogError, hookLogInfo } from "./hook-logger";
|
|
18
18
|
import { getCustomHooks, clearCustomHooks } from "./custom-hooks-registry";
|
|
19
19
|
import { findDistIndex, rewriteFileTree, TMP_SUFFIX, cleanupTmpFiles } from "./loader-utils";
|
|
20
|
+
import { findProjectConfigDir } from "./hooks-config";
|
|
20
21
|
import type { CustomHook } from "./policy-types";
|
|
21
22
|
|
|
22
23
|
const LOADING_KEY = "__FAILPROOFAI_LOADING_HOOKS__";
|
|
@@ -126,11 +127,13 @@ export async function loadAllCustomHooks(
|
|
|
126
127
|
|
|
127
128
|
const conventionSources: ConventionSource[] = [];
|
|
128
129
|
|
|
130
|
+
const projectRoot = findProjectConfigDir(opts?.sessionCwd ?? process.cwd());
|
|
131
|
+
|
|
129
132
|
// 1. Explicit customPoliciesPath (existing behavior)
|
|
130
133
|
if (customPoliciesPath) {
|
|
131
134
|
const absPath = isAbsolute(customPoliciesPath)
|
|
132
135
|
? customPoliciesPath
|
|
133
|
-
: resolve(
|
|
136
|
+
: resolve(projectRoot, customPoliciesPath);
|
|
134
137
|
if (existsSync(absPath)) {
|
|
135
138
|
await loadSingleFile(absPath);
|
|
136
139
|
} else {
|
|
@@ -140,8 +143,8 @@ export async function loadAllCustomHooks(
|
|
|
140
143
|
|
|
141
144
|
const hooksBeforeConvention = getCustomHooks().length;
|
|
142
145
|
|
|
143
|
-
// 2. Project convention: {
|
|
144
|
-
const projectDir = resolve(
|
|
146
|
+
// 2. Project convention: {projectRoot}/.failproofai/policies/*policies.{js,mjs,ts}
|
|
147
|
+
const projectDir = resolve(projectRoot, ".failproofai", "policies");
|
|
145
148
|
const projectFiles = discoverPolicyFiles(projectDir);
|
|
146
149
|
for (const file of projectFiles) {
|
|
147
150
|
const hooksBefore = getCustomHooks().length;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read/write the hooks configuration file at ~/.failproofai/policies-config.json.
|
|
3
3
|
*/
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
5
5
|
import { resolve, dirname } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import type { HooksConfig } from "./policy-types";
|
|
@@ -19,6 +19,33 @@ function readConfigAt(path: string): Partial<HooksConfig> {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Walk up from `start` until a `.failproofai/` directory is found, and return that
|
|
24
|
+
* dir as the project root. Stops at homedir (the global `~/.failproofai/` is not a
|
|
25
|
+
* project root) or filesystem root. If no marker is found, returns the original
|
|
26
|
+
* `start` so callers fall through to the global-only config merge.
|
|
27
|
+
*
|
|
28
|
+
* Fixes #200: when Claude Code's Bash tool drifts CWD into a subdirectory, the
|
|
29
|
+
* project policy config was silently missed because we resolved it at the exact
|
|
30
|
+
* cwd instead of walking up.
|
|
31
|
+
*/
|
|
32
|
+
export function findProjectConfigDir(start: string): string {
|
|
33
|
+
const home = homedir();
|
|
34
|
+
let dir = resolve(start);
|
|
35
|
+
while (dir !== home) {
|
|
36
|
+
const marker = resolve(dir, ".failproofai");
|
|
37
|
+
try {
|
|
38
|
+
if (statSync(marker).isDirectory()) return dir;
|
|
39
|
+
} catch {
|
|
40
|
+
// not present or unreadable — keep walking
|
|
41
|
+
}
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
return resolve(start);
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
/**
|
|
23
50
|
* Read and merge hooks config from three scopes in priority order:
|
|
24
51
|
* 1. {cwd}/.failproofai/policies-config.json (project)
|
|
@@ -32,7 +59,7 @@ function readConfigAt(path: string): Partial<HooksConfig> {
|
|
|
32
59
|
* llm: first scope that defines it wins
|
|
33
60
|
*/
|
|
34
61
|
export function readMergedHooksConfig(cwd?: string): HooksConfig {
|
|
35
|
-
const base =
|
|
62
|
+
const base = findProjectConfigDir(cwd ?? process.cwd());
|
|
36
63
|
const projectPath = resolve(base, ".failproofai", "policies-config.json");
|
|
37
64
|
const localPath = resolve(base, ".failproofai", "policies-config.local.json");
|
|
38
65
|
const globalPath = resolve(homedir(), ".failproofai", "policies-config.json");
|
|
@@ -105,14 +132,13 @@ export function writeHooksConfig(config: HooksConfig): void {
|
|
|
105
132
|
* Resolve the policies-config path for a specific scope.
|
|
106
133
|
*/
|
|
107
134
|
export function getConfigPathForScope(scope: HookScope, cwd?: string): string {
|
|
108
|
-
const base = cwd ? resolve(cwd) : process.cwd();
|
|
109
135
|
switch (scope) {
|
|
110
136
|
case "user":
|
|
111
137
|
return resolve(homedir(), ".failproofai", "policies-config.json");
|
|
112
138
|
case "project":
|
|
113
|
-
return resolve(
|
|
139
|
+
return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.json");
|
|
114
140
|
case "local":
|
|
115
|
-
return resolve(
|
|
141
|
+
return resolve(findProjectConfigDir(cwd ?? process.cwd()), ".failproofai", "policies-config.local.json");
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
144
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|