failproofai 1.0.2-beta.5 → 1.0.2-beta.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 +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- 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 +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1zkek3z._.js → [root-of-the-server]__1tg4pu6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_12moi5-._.js → _19uwam-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +12 -15
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0stn0gx._.js → [root-of-the-server]__1jjjg6g._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tag8_-._.js → [root-of-the-server]__1opxw-7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1kesqs-._.js → _0_nsohj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1km9-gn._.js → _1uvml5y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- 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 +19 -19
- package/.next/standalone/.next/static/chunks/{3_b73tc78jwsq.js → 0icodhlwr9x_e.js} +1 -1
- package/.next/standalone/.next/static/chunks/{21r7iw3rmixf5.js → 0tvmd0p_1i_g6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{33np9nrh1mvj9.js → 1d6c85605lsgp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{26lfjhb2kzzgb.js → 1d7o3wnt3h6se.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2_27jraat038x.js → 1vzyn9h2kvj-i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hwc8e923g5df.js → 2-9lvhuto1iwo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36-8i_vbkh85w.js → 3jt6n16p2qpex.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tyt-3a5rtzy8.js → 3swqstog1cd9u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{391tpdqcas7rq.js → 3vqddiy9tvlwf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3jqm8ubpv3i-4.js → 44oamw33aso19.js} +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +853 -669
- package/dist/cli.mjs +4534 -4429
- package/dist/worker.mjs +2 -2
- package/package.json +9 -9
- package/src/audit/cli.ts +52 -39
- package/src/audit/index.ts +33 -2
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/configure-wizard.ts +126 -18
- package/src/hooks/fp-reset.ts +1 -1
- package/src/hooks/handler.ts +13 -1
- package/src/hooks/manager.ts +60 -7
- package/src/hooks/onboarding-attempt.ts +4 -4
- package/src/hooks/pack-cli.ts +162 -21
- package/src/hooks/pack-store.ts +21 -8
- package/src/hooks/read-stdin.ts +78 -8
- package/src/hooks/tui.ts +173 -39
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15r6r7w._.js +0 -10
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_ssgManifest.js +0 -0
package/dist/worker.mjs
CHANGED
|
@@ -2559,7 +2559,7 @@ var init_builtin_policies = __esm(() => {
|
|
|
2559
2559
|
});
|
|
2560
2560
|
|
|
2561
2561
|
// package.json
|
|
2562
|
-
var version = "1.0.2-beta.
|
|
2562
|
+
var version = "1.0.2-beta.7";
|
|
2563
2563
|
var init_package = () => {};
|
|
2564
2564
|
|
|
2565
2565
|
// src/posthog-key.ts
|
|
@@ -9408,7 +9408,7 @@ async function evaluateHookEvent(eventType, cli = "claude", stdinPayload, opts)
|
|
|
9408
9408
|
const legacyNames = activePause || packsInstalledHere ? [] : config.enabledPolicies;
|
|
9409
9409
|
registerBuiltinPolicies(activePause ? [] : legacyNames);
|
|
9410
9410
|
if (legacyNames.length > 0) {
|
|
9411
|
-
hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add
|
|
9411
|
+
hookLogWarn(`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` + `run \`failproofai policies add FailproofAI/policies\` to move them into a real pack`);
|
|
9412
9412
|
}
|
|
9413
9413
|
const enabledBuiltinNames = new Set(legacyNames);
|
|
9414
9414
|
let cloudManagedPolicies = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.2-beta.
|
|
3
|
+
"version": "1.0.2-beta.7",
|
|
4
4
|
"description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -120,15 +120,15 @@
|
|
|
120
120
|
"sharp": "0.35.0"
|
|
121
121
|
},
|
|
122
122
|
"optionalDependencies": {
|
|
123
|
-
"@failproofai/failproofaid-linux-x64": "1.0.2-beta.
|
|
124
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.2-beta.
|
|
125
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.2-beta.
|
|
126
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.
|
|
123
|
+
"@failproofai/failproofaid-linux-x64": "1.0.2-beta.7",
|
|
124
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.2-beta.7",
|
|
125
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.2-beta.7",
|
|
126
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.7"
|
|
127
127
|
},
|
|
128
128
|
"failproofaidBinaries": {
|
|
129
|
-
"linux-x64": "
|
|
130
|
-
"linux-arm64": "
|
|
131
|
-
"darwin-x64": "
|
|
132
|
-
"darwin-arm64": "
|
|
129
|
+
"linux-x64": "75a8e231e057986f2b357e1fcb596641afd82a4b58f9271bb4b342951a364c2d",
|
|
130
|
+
"linux-arm64": "47d2933d06e70faefa78fe06fdd4e06bc03e94a319d13a8837145d192cd84d8c",
|
|
131
|
+
"darwin-x64": "6eae79c75746efe6922ca84e7dd899d01a50e651879abc8f90de69cd51c403f3",
|
|
132
|
+
"darwin-arm64": "ddca616decc3d2415ce6a58ae5fe15786bd2cfa5a5319803f4207a01d40e4a0a"
|
|
133
133
|
}
|
|
134
134
|
}
|
package/src/audit/cli.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* an exit code. See `runScheduledAudit`.
|
|
26
26
|
*/
|
|
27
27
|
import { runAudit } from "./index";
|
|
28
|
+
import { hasInstalledPacks } from "../hooks/pack-manifest";
|
|
29
|
+
import { CORE_SOURCE } from "../hooks/pack-store";
|
|
28
30
|
import { acquireAuditLock, type AuditLockInfo } from "./audit-lock";
|
|
29
31
|
import { writeDashboardCache } from "./dashboard-cache";
|
|
30
32
|
import type { AuditResult, RunAuditOptions } from "./types";
|
|
@@ -33,7 +35,8 @@ import { getInstanceId } from "../../lib/telemetry-id";
|
|
|
33
35
|
import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
|
|
34
36
|
import { openWhenReady } from "./open-browser";
|
|
35
37
|
import { describeOutcome, reportHarm } from "./report-harm";
|
|
36
|
-
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM } from "../hooks/tui";
|
|
38
|
+
import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM, helpScreen, helpOptsFor } from "../hooks/tui";
|
|
39
|
+
import { version } from "../../package.json";
|
|
37
40
|
|
|
38
41
|
/** Port the bundled dashboard binds to. Matches `scripts/launch.ts`'s default
|
|
39
42
|
* for `start` mode, which `failproofai` (bare) already uses. */
|
|
@@ -62,9 +65,6 @@ export const AUDIT_STAGES: ReadonlyArray<{ label: string; detail: string }> = [
|
|
|
62
65
|
{ label: "aggregating results", detail: "counting hits, ranking by frequency" },
|
|
63
66
|
];
|
|
64
67
|
|
|
65
|
-
/** Column the description text starts at. The widest command line is 37 wide. */
|
|
66
|
-
const HELP_DESC_COL = 40;
|
|
67
|
-
|
|
68
68
|
/**
|
|
69
69
|
* `audit --help`.
|
|
70
70
|
*
|
|
@@ -80,41 +80,44 @@ const HELP_DESC_COL = 40;
|
|
|
80
80
|
* one. It still works, and still refuses every argument it always refused.
|
|
81
81
|
*/
|
|
82
82
|
export function helpText(): string {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
83
|
+
// Drawn by the same renderer as `config --help` and `policies --help`, from
|
|
84
|
+
// the same kit `audit`'s output already uses for its palette. It used to
|
|
85
|
+
// build its own rows against a hand-set HELP_DESC_COL, which is how `audit`
|
|
86
|
+
// and `config` ended up with their descriptions in different columns.
|
|
87
|
+
//
|
|
88
|
+
// The `failproofai audit` prefix is dropped from the rows: the heading two
|
|
89
|
+
// lines above already carries it, and repeating it cost 17 of the 80 columns
|
|
90
|
+
// on every row — which is what forced the four-word-per-line descriptions.
|
|
91
|
+
const lines = helpScreen(
|
|
92
|
+
{
|
|
93
|
+
command: "audit",
|
|
94
|
+
version,
|
|
95
|
+
tagline: "review your agent CLIs for risky and wasteful patterns",
|
|
96
|
+
sections: [
|
|
97
|
+
{
|
|
98
|
+
label: "usage",
|
|
99
|
+
entries: [
|
|
100
|
+
["(bare)", `Scan your session history, then open http://localhost:${DASHBOARD_PORT}/audit`],
|
|
101
|
+
["--schedule [days]", "Scan on a timer and email the findings. Default 7 days, range 1-90. Signs you in the first time."],
|
|
102
|
+
// Its own row now, rather than a clause inside --schedule's. It
|
|
103
|
+
// does not stand alone, which is why it used to be a clause — but
|
|
104
|
+
// a clause wraps, and `--email <address>` landing with the flag at
|
|
105
|
+
// the end of one line and its placeholder at the start of the next
|
|
106
|
+
// is not a flag anybody can read or copy.
|
|
107
|
+
["--email <address>", "With --schedule, skips the sign-in prompt."],
|
|
108
|
+
["--no-schedule", "Stop the timer. Leaves you signed in."],
|
|
109
|
+
["--status", "Whether scheduling is on, where reports go, the daemon's state, and when the next scan is due."],
|
|
110
|
+
["-h, --help", "Show this help."],
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
footer: ["Everything runs on this machine; only a scheduled digest ever leaves it."],
|
|
115
|
+
},
|
|
116
|
+
helpOptsFor(process.stdout),
|
|
117
|
+
);
|
|
118
|
+
// The margins `printBlock` would add, spelled out because this one returns
|
|
119
|
+
// its text for a caller to write rather than writing it.
|
|
120
|
+
return ["", ...lines, ""].join("\n") + "\n";
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
@@ -504,6 +507,16 @@ export async function runPostSetupAudit(): Promise<void> {
|
|
|
504
507
|
writeDashboardCache({}, result);
|
|
505
508
|
printSummary(result);
|
|
506
509
|
process.stdout.write("\n");
|
|
510
|
+
// The audit says what ALREADY happened; nothing here says how to stop it
|
|
511
|
+
// happening again. This is the first thing a new machine runs, and setup
|
|
512
|
+
// installs no policies by design, so without this the whole first session
|
|
513
|
+
// ends on a count of findings and no way to act on it.
|
|
514
|
+
if (!hasInstalledPacks()) {
|
|
515
|
+
process.stdout.write(
|
|
516
|
+
` ${c(DIM, "none of this is being enforced yet. take ours, or anyone's:")}\n` +
|
|
517
|
+
` ${c(CYAN, `failproofai policies add ${CORE_SOURCE}`)}\n\n`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
507
520
|
} finally {
|
|
508
521
|
attempt.lock.release();
|
|
509
522
|
}
|
package/src/audit/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { batchAll } from "../../lib/concurrency";
|
|
11
11
|
import { BUILTIN_POLICIES } from "../hooks/builtin-policies";
|
|
12
12
|
import { readMergedHooksConfig } from "../hooks/hooks-config";
|
|
13
|
+
import { readInstalledPacks } from "../hooks/pack-manifest";
|
|
13
14
|
import { normalizePolicyName } from "../hooks/policy-registry";
|
|
14
15
|
import { INTEGRATION_TYPES, type IntegrationType } from "../hooks/types";
|
|
15
16
|
import { ADAPTERS } from "./cli-adapters";
|
|
@@ -379,12 +380,42 @@ async function runAuditInner(opts: RunAuditOptions, startedAt: number): Promise<
|
|
|
379
380
|
const clis = (opts.clis ?? Array.from(INTEGRATION_TYPES)) as IntegrationType[];
|
|
380
381
|
const sinceMs = parseSinceOpt(opts.since);
|
|
381
382
|
|
|
382
|
-
//
|
|
383
|
-
//
|
|
383
|
+
// What the user actually has switched on — drives the "already protected"
|
|
384
|
+
// vs "slipping through" split.
|
|
385
|
+
//
|
|
386
|
+
// Read from BOTH places it can live, because it moved and this only followed
|
|
387
|
+
// it half way. `enabledPolicies` is the pre-packs key, still written by
|
|
388
|
+
// `policies add <name>` and still true on a machine running the migration
|
|
389
|
+
// shim. Everything installed as a PACK records its selection in
|
|
390
|
+
// `installed.json` instead, and `pack-store` never writes `enabledPolicies`
|
|
391
|
+
// at all — so reading that key alone meant installing a pack could not move
|
|
392
|
+
// this split. Every policy read as slipping through no matter what was
|
|
393
|
+
// enforcing, and the number the audit leads with never changed however many
|
|
394
|
+
// policies you installed. Verified: same 302 hits, same 17 findings, before
|
|
395
|
+
// and after installing all 38.
|
|
396
|
+
//
|
|
397
|
+
// The split, not the REPLAY. What gets replayed stays the compiled builtins
|
|
398
|
+
// on purpose — see `initReplay`: an audit is a fixed yardstick, and one that
|
|
399
|
+
// changed shape with whatever pack a machine happened to have could not be
|
|
400
|
+
// compared against its own history. A pack policy sharing a builtin's name
|
|
401
|
+
// marks that builtin protected, which is exactly right: the same rule is
|
|
402
|
+
// enforcing, whichever artifact carries it.
|
|
384
403
|
const userConfig = readMergedHooksConfig();
|
|
385
404
|
const enabledBuiltins = new Set(
|
|
386
405
|
(userConfig.enabledPolicies ?? []).map((n) => normalizePolicyName(n)),
|
|
387
406
|
);
|
|
407
|
+
try {
|
|
408
|
+
for (const pack of readInstalledPacks().packs) {
|
|
409
|
+
// `enabled: null` means the whole pack was taken, so every policy in its
|
|
410
|
+
// catalog counts — the field records a SELECTION, and its absence is not
|
|
411
|
+
// an empty one.
|
|
412
|
+
const taken = pack.enabled ?? pack.policies.map((entry) => entry.name);
|
|
413
|
+
for (const name of taken) enabledBuiltins.add(normalizePolicyName(name));
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// An unreadable manifest must not fail the audit. It costs the split its
|
|
417
|
+
// pack half, which is the same answer this gave for every machine before.
|
|
418
|
+
}
|
|
388
419
|
|
|
389
420
|
// 1. Discover transcripts across all selected CLIs.
|
|
390
421
|
const allTranscripts: TranscriptMetadata[] = [];
|
|
@@ -149,7 +149,7 @@ export function runBackfillCommand(opts: BackfillOptions = {}): BackfillResult {
|
|
|
149
149
|
exitCode: 1,
|
|
150
150
|
lines: [
|
|
151
151
|
"This machine is not connected, so there is nowhere to send history.",
|
|
152
|
-
"Connect first: `failproofai config --
|
|
152
|
+
"Connect first: `failproofai config --token <key>`.",
|
|
153
153
|
],
|
|
154
154
|
};
|
|
155
155
|
}
|
|
@@ -30,6 +30,8 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { dirname, resolve, sep } from "node:path";
|
|
32
32
|
|
|
33
|
+
import { CORE_SOURCE } from "./pack-store";
|
|
34
|
+
|
|
33
35
|
import {
|
|
34
36
|
selectOne,
|
|
35
37
|
promptText,
|
|
@@ -109,6 +111,31 @@ export interface WizardIO {
|
|
|
109
111
|
stdout?: TTYOut;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Answers supplied up front, so the run needs nobody at the keyboard.
|
|
116
|
+
*
|
|
117
|
+
* This is the whole of headless setup, and it is small for a reason nobody
|
|
118
|
+
* planned: the wizard stopped DECIDING most of what it used to. Scope, agents,
|
|
119
|
+
* policies and the custom-policy toggle are all hardcoded now (see the
|
|
120
|
+
* constants further down), so there is no decision surface left to expose —
|
|
121
|
+
* only the three questions that remain, and each has one sensible unattended
|
|
122
|
+
* answer.
|
|
123
|
+
*
|
|
124
|
+
* None of this skips sudo. Installing a system service needs root and nothing
|
|
125
|
+
* here changes that: elevation goes through `sudo -n`, which never prompts, and
|
|
126
|
+
* a machine that cannot elevate is told exactly what to run as root.
|
|
127
|
+
*/
|
|
128
|
+
export interface WizardAnswers {
|
|
129
|
+
/** Cloud API key. Its presence IS the request to connect. */
|
|
130
|
+
token?: string;
|
|
131
|
+
/** Where to connect. Defaults to the same URL the wizard infers. */
|
|
132
|
+
url?: string;
|
|
133
|
+
machineId?: string;
|
|
134
|
+
machineLabel?: string;
|
|
135
|
+
/** Record decisions only, no session transcripts. */
|
|
136
|
+
noTranscripts?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
112
139
|
/**
|
|
113
140
|
* Why a wizard run ended without applying. Distinguished so the caller can
|
|
114
141
|
* pick an exit code — a user who pressed Esc did nothing wrong (exit 0), a
|
|
@@ -120,8 +147,9 @@ export type WizardAbort =
|
|
|
120
147
|
| "needs_root"
|
|
121
148
|
| "daemon_failed"
|
|
122
149
|
| "unsupported_platform"
|
|
123
|
-
| "
|
|
124
|
-
|
|
150
|
+
| "running_as_sudo"
|
|
151
|
+
/** A cloud key was supplied and the server refused it. Unattended only. */
|
|
152
|
+
| "cloud_unverified";
|
|
125
153
|
|
|
126
154
|
export interface WizardResult {
|
|
127
155
|
applied: boolean;
|
|
@@ -695,19 +723,37 @@ export async function maybeFirstRunConfigure(
|
|
|
695
723
|
|
|
696
724
|
// ── The wizard ───────────────────────────────────────────────────────────────
|
|
697
725
|
|
|
698
|
-
export async function runConfigureWizard(
|
|
726
|
+
export async function runConfigureWizard(
|
|
727
|
+
io: WizardIO = {},
|
|
728
|
+
answers: WizardAnswers = {},
|
|
729
|
+
): Promise<WizardResult> {
|
|
699
730
|
const stdin: TTYIn = io.stdin ?? process.stdin;
|
|
700
731
|
const stdout: TTYOut = io.stdout ?? process.stdout;
|
|
701
732
|
const cwd = process.cwd();
|
|
733
|
+
// No terminal means no questions — not a refusal.
|
|
734
|
+
//
|
|
735
|
+
// `failproofai config` IS the authorisation: somebody typed the command whose
|
|
736
|
+
// entire job is to configure this machine. Requiring a flag on top of that to
|
|
737
|
+
// permit what the command already means is asking the same question twice,
|
|
738
|
+
// and this used to refuse outright — so a CI job, a container or an agent
|
|
739
|
+
// could not set a machine up at all, which is the gap this closes.
|
|
740
|
+
//
|
|
741
|
+
// Safe to do here because the IMPLICIT path is guarded separately:
|
|
742
|
+
// `maybeFirstRunConfigure` has its own TTY check and returns before ever
|
|
743
|
+
// reaching this function, so setup never runs off the back of some other
|
|
744
|
+
// command on a headless box.
|
|
745
|
+
const unattended = !stdin.isTTY || !stdout.isTTY;
|
|
746
|
+
// A key on the command line settles the whole run, not just the question it
|
|
747
|
+
// literally answers.
|
|
748
|
+
//
|
|
749
|
+
// Somebody typing `failproofai config --token <key>` has said: set this
|
|
750
|
+
// machine up, connect it, send its data. There is nothing left to confirm —
|
|
751
|
+
// so asking "Connect to Cloud?", then the key, then "Ready to apply?" made
|
|
752
|
+
// the flag look like it had not been read. The only thing still worth
|
|
753
|
+
// stopping for is the sudo password, which is a CREDENTIAL rather than a
|
|
754
|
+
// question, and no flag can supply it.
|
|
755
|
+
const preAnswered = Boolean(answers.token);
|
|
702
756
|
|
|
703
|
-
if (!stdin.isTTY || !stdout.isTTY) {
|
|
704
|
-
stdout.write(
|
|
705
|
-
"failproofai config needs an interactive terminal.\n" +
|
|
706
|
-
"Use the flag form instead, e.g.:\n" +
|
|
707
|
-
" failproofai policies --install --scope user --cli claude\n",
|
|
708
|
-
);
|
|
709
|
-
return { applied: false };
|
|
710
|
-
}
|
|
711
757
|
|
|
712
758
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
713
759
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
@@ -724,7 +770,10 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
724
770
|
"The one step that needs root (installing the service) asks for your password\n" +
|
|
725
771
|
"on its own.\n",
|
|
726
772
|
);
|
|
727
|
-
|
|
773
|
+
// Same reason as above: this configured nothing, and it is not a
|
|
774
|
+
// cancellation. `running_as_sudo` has been in `WizardAbort` since it was
|
|
775
|
+
// written and was never once assigned.
|
|
776
|
+
return { applied: false, abort: "running_as_sudo" };
|
|
728
777
|
}
|
|
729
778
|
|
|
730
779
|
// Fire-and-forget: never block the wizard's first paint on telemetry.
|
|
@@ -847,7 +896,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
847
896
|
? "failproofaid can't evaluate — every tool call is denied. Rebuilding needs sudo.\n\n"
|
|
848
897
|
: "Installing failproofaid — needs sudo once.\n\n",
|
|
849
898
|
);
|
|
850
|
-
|
|
899
|
+
// `primeElevation` runs `sudo -v`, which PROMPTS. An unattended run has
|
|
900
|
+
// nobody to type a password, so it goes straight to the `sudo -n` that
|
|
901
|
+
// `runPrivileged` uses anyway — exactly what `failproofai update` already
|
|
902
|
+
// does — and a machine that cannot elevate gets the commands below instead
|
|
903
|
+
// of a hung terminal. This is the same answer for both: sudo cannot be
|
|
904
|
+
// conjured away, only asked for at a moment somebody can answer.
|
|
905
|
+
if (!(unattended ? canElevate() : primeElevation())) {
|
|
851
906
|
// Required means required: write nothing at all, so a machine that could
|
|
852
907
|
// not be set up is left exactly as it was found rather than carrying
|
|
853
908
|
// half a configuration. The commands are printed so an admin can do the
|
|
@@ -866,7 +921,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
866
921
|
"failproofaid is running, but from a service definition written by an older\n" +
|
|
867
922
|
"version — it cannot start a scheduled audit. Refreshing it needs root once.\n\n",
|
|
868
923
|
);
|
|
869
|
-
if (!primeElevation()) {
|
|
924
|
+
if (!(unattended ? canElevate() : primeElevation())) {
|
|
870
925
|
// NOT an abort, unlike the install branch above. There is a working
|
|
871
926
|
// daemon here and hooks are enforcing; only the scheduled audit is out
|
|
872
927
|
// of reach. Stopping setup over that would make an upgrade the thing
|
|
@@ -914,7 +969,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
914
969
|
//
|
|
915
970
|
// Setup no longer asks which policies to enable, and that is deliberate.
|
|
916
971
|
// failproofai ships no policies of its own any more: they arrive as packs —
|
|
917
|
-
// from inside this package (`
|
|
972
|
+
// from inside this package (`policies add FailproofAI/policies`) or from anyone's GitHub release.
|
|
918
973
|
// A wizard that pre-ticks OUR list makes a product decision on behalf of
|
|
919
974
|
// somebody who has not seen the list yet, and not everyone wants the set we
|
|
920
975
|
// would have chosen. So setup wires the hooks, and choosing what they
|
|
@@ -971,7 +1026,19 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
971
1026
|
let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
|
|
972
1027
|
|
|
973
1028
|
{
|
|
974
|
-
|
|
1029
|
+
// Supplying a key IS asking to connect — there is no other reason to pass
|
|
1030
|
+
// one — so an unattended run with a token skips straight past the question,
|
|
1031
|
+
// and one without a token stays local, which is the same "Not now" branch a
|
|
1032
|
+
// person picks. Neither needs a separate flag to say so.
|
|
1033
|
+
const choice = answers.token
|
|
1034
|
+
// A key on the command line IS the answer to this question. Gating it on
|
|
1035
|
+
// there being no terminal was wrong: `--token` means "use this key", not
|
|
1036
|
+
// "use this key only if nobody is watching", and a run that asked anyway
|
|
1037
|
+
// made the flag look broken to the person who had just typed it.
|
|
1038
|
+
? "key"
|
|
1039
|
+
: unattended
|
|
1040
|
+
? "local"
|
|
1041
|
+
: await selectOne<"key" | "local">({
|
|
975
1042
|
message: "Connect this machine to FailproofAI Cloud?",
|
|
976
1043
|
// The hint says what you GET; the body says what LEAVES.
|
|
977
1044
|
//
|
|
@@ -1054,7 +1121,16 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1054
1121
|
let machineId = resolveMachineId();
|
|
1055
1122
|
let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
|
|
1056
1123
|
|
|
1057
|
-
if (existing) {
|
|
1124
|
+
if (existing && unattended && !answers.url) {
|
|
1125
|
+
// Already enrolled, and this run did not name a different place to
|
|
1126
|
+
// enrol. Reusing is what the person picks here too, and re-verifying a
|
|
1127
|
+
// working credential is the one thing an unattended re-run must not
|
|
1128
|
+
// turn into a failure.
|
|
1129
|
+
url = existing.url;
|
|
1130
|
+
token = answers.token ?? existing.token;
|
|
1131
|
+
machineId = answers.machineId ?? existing.machineId;
|
|
1132
|
+
machineLabel = answers.machineLabel ?? existing.machineLabel ?? machineLabel;
|
|
1133
|
+
} else if (existing && !unattended) {
|
|
1058
1134
|
const reuse = await selectOne<"reuse" | "other">({
|
|
1059
1135
|
message: `Use this machine's existing connection to ${existing.url}?`,
|
|
1060
1136
|
choices: [
|
|
@@ -1126,6 +1202,10 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1126
1202
|
}
|
|
1127
1203
|
}
|
|
1128
1204
|
|
|
1205
|
+
// Supplied on the command line, so nothing is asked and nothing is
|
|
1206
|
+
// echoed — terminal or not. Prompting for a value the caller already gave
|
|
1207
|
+
// is the failure this whole flag exists to avoid.
|
|
1208
|
+
if (token === null) token = answers.token ?? null;
|
|
1129
1209
|
if (token === null) {
|
|
1130
1210
|
token = await promptText({
|
|
1131
1211
|
// The destination is in the question now that it is no longer a
|
|
@@ -1149,6 +1229,14 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1149
1229
|
// persisted here.
|
|
1150
1230
|
stdout.write("\nChecking the key… ");
|
|
1151
1231
|
const probe = await validateIngestKey({ url: ingestUrlFor(url), key: token });
|
|
1232
|
+
if (!probe.ok && unattended) {
|
|
1233
|
+
// A key that does not work is a FAILED setup, not a prompt. The
|
|
1234
|
+
// interactive path offers "save it anyway" because a person can weigh
|
|
1235
|
+
// an outage against their own impatience; a script cannot, and one that
|
|
1236
|
+
// exited 0 here would leave a fleet believing it was reporting.
|
|
1237
|
+
stdout.write(`\nThat key did not work: ${probe.reason}\n`);
|
|
1238
|
+
return { applied: false, abort: "cloud_unverified" };
|
|
1239
|
+
}
|
|
1152
1240
|
if (!probe.ok) {
|
|
1153
1241
|
stdout.write(`\nThat did not work: ${probe.reason}\n`);
|
|
1154
1242
|
const retry = await selectOne<"skip" | "anyway">({
|
|
@@ -1173,7 +1261,15 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1173
1261
|
}
|
|
1174
1262
|
}
|
|
1175
1263
|
// 5 — Review & apply
|
|
1176
|
-
|
|
1264
|
+
//
|
|
1265
|
+
// The last question, and the only one a headless run skips outright rather
|
|
1266
|
+
// than answering from a flag. There is nothing to confirm when nobody is
|
|
1267
|
+
// watching, and the command itself was the confirmation.
|
|
1268
|
+
//
|
|
1269
|
+
// NOT the same call as `uninstall`, which does require --yes: that one
|
|
1270
|
+
// REMOVES things, so silence there would destroy on a signal as weak as a
|
|
1271
|
+
// missing terminal. This one does exactly what its name says.
|
|
1272
|
+
const decision = unattended || preAnswered ? "apply" : await selectOne<"apply" | "cancel">({
|
|
1177
1273
|
message: "Ready to apply?",
|
|
1178
1274
|
body: reviewLines({
|
|
1179
1275
|
target,
|
|
@@ -1463,6 +1559,18 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1463
1559
|
{ ok: true },
|
|
1464
1560
|
stdout,
|
|
1465
1561
|
);
|
|
1562
|
+
// Setup wires the hooks and deliberately chooses NO policies, so a machine
|
|
1563
|
+
// that has just finished it enforces almost nothing — and the summary line
|
|
1564
|
+
// says "0 policies" without saying what to do about it. Anyone else's pack is
|
|
1565
|
+
// typed the same way, which is the point: ours is named in full rather than
|
|
1566
|
+
// by a short name only we could use.
|
|
1567
|
+
if (policies.length === 0) {
|
|
1568
|
+
stdout.write(
|
|
1569
|
+
`\n Nothing is enforcing yet. Take ours, or anyone's:\n` +
|
|
1570
|
+
` failproofai policies add ${CORE_SOURCE}\n` +
|
|
1571
|
+
` failproofai policies show <owner>/<repo> (look first)\n\n`,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1466
1574
|
return {
|
|
1467
1575
|
applied: true,
|
|
1468
1576
|
target,
|
package/src/hooks/fp-reset.ts
CHANGED
|
@@ -973,7 +973,7 @@ export function resetHome(from: number, to: number = LAYOUT_VERSION): ResetOutco
|
|
|
973
973
|
//
|
|
974
974
|
// Nothing needs carrying either: `enabledPolicies` is left exactly where it
|
|
975
975
|
// is, and the no-pack fallback reads it, so a machine mid-upgrade keeps
|
|
976
|
-
// enforcing precisely what it enforced before. `failproofai policies add
|
|
976
|
+
// enforcing precisely what it enforced before. `failproofai policies add FailproofAI/policies`
|
|
977
977
|
// turns those names into a real pack, carrying the same selection into it.
|
|
978
978
|
// The step's OWN target, not LAYOUT_VERSION.
|
|
979
979
|
//
|
package/src/hooks/handler.ts
CHANGED
|
@@ -323,7 +323,7 @@ export async function evaluateHookEvent(
|
|
|
323
323
|
if (legacyNames.length > 0) {
|
|
324
324
|
hookLogWarn(
|
|
325
325
|
`enforcing ${legacyNames.length} policies from this build because no pack is installed — ` +
|
|
326
|
-
`run \`failproofai policies add
|
|
326
|
+
`run \`failproofai policies add FailproofAI/policies\` to move them into a real pack`,
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
// What actually registered, for the pack-duplicate check below. A paused
|
|
@@ -786,6 +786,18 @@ export async function handleHookEvent(eventType: string, cli: IntegrationType =
|
|
|
786
786
|
error_type: "oversized",
|
|
787
787
|
});
|
|
788
788
|
}
|
|
789
|
+
if (stdinRead.timedOut) {
|
|
790
|
+
// Reported distinctly from a read error because the remedy is different and
|
|
791
|
+
// the cause is upstream: the agent spawned the hook and did not close its
|
|
792
|
+
// stdin. Before this was bounded it was not reported at all — the process
|
|
793
|
+
// simply never returned, and the tool call it was gating never resolved.
|
|
794
|
+
hookLogWarn(`stdin never closed for ${eventType}; evaluated nothing`);
|
|
795
|
+
void trackHookEvent(getInstanceId(), "hook_stdin_error", {
|
|
796
|
+
event_type: eventType,
|
|
797
|
+
cli,
|
|
798
|
+
error_type: "timeout",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
789
801
|
|
|
790
802
|
const result = await evaluateHookEvent(eventType, cli, stdinRead.payload);
|
|
791
803
|
|
package/src/hooks/manager.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { INTEGRATION_TYPES,
|
|
|
15
15
|
type IntegrationType,
|
|
16
16
|
} from "./types";
|
|
17
17
|
import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
|
|
18
|
-
import { promptPolicySelection } from "./install-prompt";
|
|
19
18
|
import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
|
|
20
19
|
import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
|
|
21
20
|
import { BUILTIN_POLICIES } from "./builtin-policies";
|
|
@@ -462,9 +461,25 @@ async function installHooksImpl(
|
|
|
462
461
|
? [...new Set(incoming)]
|
|
463
462
|
: [...new Set([...previousConfig.enabledPolicies, ...incoming])];
|
|
464
463
|
} else {
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
464
|
+
// NOT a policy picker. `--install` wires hooks; choosing what they enforce
|
|
465
|
+
// is `policies add` / `policies remove`.
|
|
466
|
+
//
|
|
467
|
+
// It used to open a second picker here, over `BUILTIN_POLICIES` — the
|
|
468
|
+
// COMPILED catalog, which is no longer how policies arrive. So it offered a
|
|
469
|
+
// list that did not match what was installed, in an older prompt engine
|
|
470
|
+
// that looks nothing like the rest, and wrote its answer to
|
|
471
|
+
// `enabledPolicies` while every pack records its selection in
|
|
472
|
+
// `installed.json`.
|
|
473
|
+
//
|
|
474
|
+
// Two independent enabled-sets, and this one silently overwrote the other:
|
|
475
|
+
// `policies remove block-env-files` followed by `policies --install` put
|
|
476
|
+
// block-env-files straight back on, because the picker pre-ticked from the
|
|
477
|
+
// legacy key and saved all of it again. A command whose job is wiring must
|
|
478
|
+
// not undo a policy decision made somewhere else.
|
|
479
|
+
//
|
|
480
|
+
// Whatever was enabled stays enabled. Explicit names still work
|
|
481
|
+
// (`--install block-sudo`) for the migration shim, which has only this key.
|
|
482
|
+
selectedPolicies = previousConfig.enabledPolicies;
|
|
468
483
|
}
|
|
469
484
|
|
|
470
485
|
// Preserve existing config fields when updating. New writes use the plural
|
|
@@ -556,7 +571,21 @@ async function installHooksImpl(
|
|
|
556
571
|
// pack does not carry it and asking for it by name is a selection the pack
|
|
557
572
|
// cannot satisfy. It ships compiled in and registers regardless.
|
|
558
573
|
const alwaysOnNames = new Set(BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => p.name));
|
|
559
|
-
|
|
574
|
+
// ONLY names the caller actually asked for.
|
|
575
|
+
//
|
|
576
|
+
// This used to take whatever sat in `enabledPolicies` and switch those names
|
|
577
|
+
// on in the pack. It is additive — it can turn a policy on and never off — so
|
|
578
|
+
// a stale entry in that key resurrected a policy the user had deliberately
|
|
579
|
+
// removed, on the next `policies --install`, with no mention of it.
|
|
580
|
+
// `remove block-env-files` then `--install` put it straight back.
|
|
581
|
+
//
|
|
582
|
+
// And the key goes stale by design now: nothing writes pack selections there,
|
|
583
|
+
// so anything left in it is a leftover from before packs, kept only for the
|
|
584
|
+
// migration shim. Re-applying a leftover as if it were a decision is how a
|
|
585
|
+
// machine ends up enforcing something its owner switched off.
|
|
586
|
+
const fromPack = policyNames === undefined
|
|
587
|
+
? []
|
|
588
|
+
: selectedPolicies.filter((name) => !alwaysOnNames.has(name));
|
|
560
589
|
if (fromPack.length > 0) {
|
|
561
590
|
// ONLY when there is no pack yet. Fetching the core pack with
|
|
562
591
|
// `only: <selection>` REPLACES whatever the machine had chosen, and
|
|
@@ -578,7 +607,7 @@ async function installHooksImpl(
|
|
|
578
607
|
} catch (err) {
|
|
579
608
|
console.log(
|
|
580
609
|
`\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
581
|
-
`Run \`failproofai policies add
|
|
610
|
+
`Run \`failproofai policies add FailproofAI/policies\` once you are online.`,
|
|
582
611
|
);
|
|
583
612
|
}
|
|
584
613
|
} else {
|
|
@@ -592,7 +621,13 @@ async function installHooksImpl(
|
|
|
592
621
|
}
|
|
593
622
|
}
|
|
594
623
|
}
|
|
595
|
-
|
|
624
|
+
// Only when this run actually changed something. `--install` with no names
|
|
625
|
+
// wires hooks and touches no policy, and announcing "Enabled 0 policy(ies):"
|
|
626
|
+
// with an empty list described work it had not done — or worse, re-stated a
|
|
627
|
+
// stale key as though it were this run's decision.
|
|
628
|
+
if (policyNames !== undefined && selectedPolicies.length > 0) {
|
|
629
|
+
console.log(`\nEnabled ${selectedPolicies.length} policy(ies): ${selectedPolicies.join(", ")}\n`);
|
|
630
|
+
}
|
|
596
631
|
if (removeCustomHooks) {
|
|
597
632
|
console.log("Custom hooks path cleared.");
|
|
598
633
|
} else if (configToWrite.customPoliciesPaths?.length || configToWrite.customPoliciesPath) {
|
|
@@ -991,6 +1026,24 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
991
1026
|
// a reader scrolling back from their prompt actually sees.
|
|
992
1027
|
const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
|
|
993
1028
|
|
|
1029
|
+
// A machine with hooks wired and no policies to run is the normal state right
|
|
1030
|
+
// after setup — setup installs the hooks and deliberately chooses nothing —
|
|
1031
|
+
// and this listing said NOTHING about how to leave it. A reader who had just
|
|
1032
|
+
// finished `failproofai config` saw a header, a config path, and no next
|
|
1033
|
+
// step. Ours is named in full, the same way anyone else's pack is typed,
|
|
1034
|
+
// because it IS one.
|
|
1035
|
+
if (packCount === 0) {
|
|
1036
|
+
footer.unshift(
|
|
1037
|
+
nextStep(
|
|
1038
|
+
`failproofai policies add ${CORE_SOURCE}`,
|
|
1039
|
+
"Nothing is enforcing yet. Take ours, or anyone's:",
|
|
1040
|
+
opts,
|
|
1041
|
+
),
|
|
1042
|
+
note("Someone else's: failproofai policies add <owner>/<repo>", opts),
|
|
1043
|
+
note("Look first: failproofai policies show <owner>/<repo>", opts),
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
994
1047
|
if (installedScopes.length > 1) {
|
|
995
1048
|
footer.push(
|
|
996
1049
|
warning(
|