failproofai 1.0.2-beta.4 → 1.0.2-beta.6
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 +5 -5
- 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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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 +4 -4
- 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 +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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- 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/build-manifest.json +2 -2
- 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 +2 -2
- 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/build-manifest.json +2 -2
- 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 +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/build-manifest.json +2 -2
- 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/build-manifest.json +2 -2
- 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]__03r-lqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0clqfab._.js → [root-of-the-server]__1tg4pu6._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1lbh81u._.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]__1m84s36._.js → [root-of-the-server]__00-s7h8._.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 +6 -6
- 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]__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]__14muoc4._.js → [root-of-the-server]__128se8m._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.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/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0ovx4lz._.js → _0no96tu._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1dkh76z._.js → _11fc-hb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.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/chunks/ssr/lib_1cf5ya2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- 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/{0lshnlpy1m6bc.js → 0931avh-06lj5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k_yvjc1yz0pq.js → 0h-3zffoy-_ji.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ds64uculrze8.js → 0p84npzxdgpc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d_re32wscs0b.js → 0sdgnflvw8kta.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2dbi_x0c8n8fi.js → 0z7au3njlak0j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1o0ahew9568ls.js → 1id7o44jjegf0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{00_t-xvc59tkb.js → 276n-edifojbd.js} +2 -2
- package/.next/standalone/.next/static/chunks/{07j2u70_3qsx8.js → 2jfskwd2u8o5r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2817_8e94ah9z.js → 2pxr7nr1nyotn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1oz0svxwt9574.js → 3i8rg2fv9nd6e.js} +2 -2
- package/.next/standalone/.next/static/chunks/{1q76jk_1_wgjq.js → 3vu_7_eor1m-k.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-17utl_jw0wj11.js → turbopack-2x-o099q64i8h.js} +1 -1
- package/.next/standalone/lib/opencode-db.ts +77 -0
- package/.next/standalone/lib/opencode-projects.ts +37 -41
- package/.next/standalone/lib/opencode-sessions.ts +59 -36
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +116 -34
- package/dist/cli.mjs +1732 -1177
- package/dist/worker.mjs +180 -165
- package/lib/opencode-db.ts +77 -0
- package/lib/opencode-projects.ts +37 -41
- package/lib/opencode-sessions.ts +59 -36
- package/package.json +9 -9
- package/src/audit/cache.ts +167 -11
- package/src/audit/cli-adapters/claude.ts +111 -1
- package/src/audit/cli-adapters/index.ts +31 -1
- package/src/audit/cli.ts +12 -0
- package/src/audit/index.ts +164 -13
- package/src/hooks/configure-wizard.ts +112 -18
- package/src/hooks/fp-reset.ts +1 -1
- package/src/hooks/handler.ts +13 -1
- package/src/hooks/manager.ts +69 -4
- package/src/hooks/onboarding-attempt.ts +4 -4
- package/src/hooks/pack-cli.ts +399 -32
- package/src/hooks/pack-store.ts +21 -8
- package/src/hooks/read-stdin.ts +78 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_ssgManifest.js +0 -0
|
@@ -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,27 @@ 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;
|
|
702
746
|
|
|
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
747
|
|
|
712
748
|
// Running the wizard itself under sudo configures the WRONG ACCOUNT, and
|
|
713
749
|
// does it silently: homedir() becomes /root, so the hooks land in root's
|
|
@@ -724,7 +760,10 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
724
760
|
"The one step that needs root (installing the service) asks for your password\n" +
|
|
725
761
|
"on its own.\n",
|
|
726
762
|
);
|
|
727
|
-
|
|
763
|
+
// Same reason as above: this configured nothing, and it is not a
|
|
764
|
+
// cancellation. `running_as_sudo` has been in `WizardAbort` since it was
|
|
765
|
+
// written and was never once assigned.
|
|
766
|
+
return { applied: false, abort: "running_as_sudo" };
|
|
728
767
|
}
|
|
729
768
|
|
|
730
769
|
// Fire-and-forget: never block the wizard's first paint on telemetry.
|
|
@@ -847,7 +886,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
847
886
|
? "failproofaid can't evaluate — every tool call is denied. Rebuilding needs sudo.\n\n"
|
|
848
887
|
: "Installing failproofaid — needs sudo once.\n\n",
|
|
849
888
|
);
|
|
850
|
-
|
|
889
|
+
// `primeElevation` runs `sudo -v`, which PROMPTS. An unattended run has
|
|
890
|
+
// nobody to type a password, so it goes straight to the `sudo -n` that
|
|
891
|
+
// `runPrivileged` uses anyway — exactly what `failproofai update` already
|
|
892
|
+
// does — and a machine that cannot elevate gets the commands below instead
|
|
893
|
+
// of a hung terminal. This is the same answer for both: sudo cannot be
|
|
894
|
+
// conjured away, only asked for at a moment somebody can answer.
|
|
895
|
+
if (!(unattended ? canElevate() : primeElevation())) {
|
|
851
896
|
// Required means required: write nothing at all, so a machine that could
|
|
852
897
|
// not be set up is left exactly as it was found rather than carrying
|
|
853
898
|
// half a configuration. The commands are printed so an admin can do the
|
|
@@ -866,7 +911,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
866
911
|
"failproofaid is running, but from a service definition written by an older\n" +
|
|
867
912
|
"version — it cannot start a scheduled audit. Refreshing it needs root once.\n\n",
|
|
868
913
|
);
|
|
869
|
-
if (!primeElevation()) {
|
|
914
|
+
if (!(unattended ? canElevate() : primeElevation())) {
|
|
870
915
|
// NOT an abort, unlike the install branch above. There is a working
|
|
871
916
|
// daemon here and hooks are enforcing; only the scheduled audit is out
|
|
872
917
|
// of reach. Stopping setup over that would make an upgrade the thing
|
|
@@ -914,7 +959,7 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
914
959
|
//
|
|
915
960
|
// Setup no longer asks which policies to enable, and that is deliberate.
|
|
916
961
|
// failproofai ships no policies of its own any more: they arrive as packs —
|
|
917
|
-
// from inside this package (`
|
|
962
|
+
// from inside this package (`policies add FailproofAI/policies`) or from anyone's GitHub release.
|
|
918
963
|
// A wizard that pre-ticks OUR list makes a product decision on behalf of
|
|
919
964
|
// somebody who has not seen the list yet, and not everyone wants the set we
|
|
920
965
|
// would have chosen. So setup wires the hooks, and choosing what they
|
|
@@ -971,7 +1016,13 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
971
1016
|
let connect: { url: string; token: string; machineId: string; machineLabel: string } | null = null;
|
|
972
1017
|
|
|
973
1018
|
{
|
|
974
|
-
|
|
1019
|
+
// Supplying a key IS asking to connect — there is no other reason to pass
|
|
1020
|
+
// one — so an unattended run with a token skips straight past the question,
|
|
1021
|
+
// and one without a token stays local, which is the same "Not now" branch a
|
|
1022
|
+
// person picks. Neither needs a separate flag to say so.
|
|
1023
|
+
const choice = unattended
|
|
1024
|
+
? (answers.token ? "key" : "local")
|
|
1025
|
+
: await selectOne<"key" | "local">({
|
|
975
1026
|
message: "Connect this machine to FailproofAI Cloud?",
|
|
976
1027
|
// The hint says what you GET; the body says what LEAVES.
|
|
977
1028
|
//
|
|
@@ -1054,7 +1105,16 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1054
1105
|
let machineId = resolveMachineId();
|
|
1055
1106
|
let machineLabel = existing?.machineLabel ?? resolveMachineLabel();
|
|
1056
1107
|
|
|
1057
|
-
if (existing) {
|
|
1108
|
+
if (existing && unattended && !answers.url) {
|
|
1109
|
+
// Already enrolled, and this run did not name a different place to
|
|
1110
|
+
// enrol. Reusing is what the person picks here too, and re-verifying a
|
|
1111
|
+
// working credential is the one thing an unattended re-run must not
|
|
1112
|
+
// turn into a failure.
|
|
1113
|
+
url = existing.url;
|
|
1114
|
+
token = answers.token ?? existing.token;
|
|
1115
|
+
machineId = answers.machineId ?? existing.machineId;
|
|
1116
|
+
machineLabel = answers.machineLabel ?? existing.machineLabel ?? machineLabel;
|
|
1117
|
+
} else if (existing && !unattended) {
|
|
1058
1118
|
const reuse = await selectOne<"reuse" | "other">({
|
|
1059
1119
|
message: `Use this machine's existing connection to ${existing.url}?`,
|
|
1060
1120
|
choices: [
|
|
@@ -1126,6 +1186,12 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1126
1186
|
}
|
|
1127
1187
|
}
|
|
1128
1188
|
|
|
1189
|
+
if (token === null && unattended) {
|
|
1190
|
+
// Supplied on the command line, so nothing is asked and nothing is
|
|
1191
|
+
// echoed. Reaching here without one cannot happen — an unattended run
|
|
1192
|
+
// only takes the "key" branch BECAUSE a token was given.
|
|
1193
|
+
token = answers.token ?? null;
|
|
1194
|
+
}
|
|
1129
1195
|
if (token === null) {
|
|
1130
1196
|
token = await promptText({
|
|
1131
1197
|
// The destination is in the question now that it is no longer a
|
|
@@ -1149,6 +1215,14 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1149
1215
|
// persisted here.
|
|
1150
1216
|
stdout.write("\nChecking the key… ");
|
|
1151
1217
|
const probe = await validateIngestKey({ url: ingestUrlFor(url), key: token });
|
|
1218
|
+
if (!probe.ok && unattended) {
|
|
1219
|
+
// A key that does not work is a FAILED setup, not a prompt. The
|
|
1220
|
+
// interactive path offers "save it anyway" because a person can weigh
|
|
1221
|
+
// an outage against their own impatience; a script cannot, and one that
|
|
1222
|
+
// exited 0 here would leave a fleet believing it was reporting.
|
|
1223
|
+
stdout.write(`\nThat key did not work: ${probe.reason}\n`);
|
|
1224
|
+
return { applied: false, abort: "cloud_unverified" };
|
|
1225
|
+
}
|
|
1152
1226
|
if (!probe.ok) {
|
|
1153
1227
|
stdout.write(`\nThat did not work: ${probe.reason}\n`);
|
|
1154
1228
|
const retry = await selectOne<"skip" | "anyway">({
|
|
@@ -1173,7 +1247,15 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1173
1247
|
}
|
|
1174
1248
|
}
|
|
1175
1249
|
// 5 — Review & apply
|
|
1176
|
-
|
|
1250
|
+
//
|
|
1251
|
+
// The last question, and the only one a headless run skips outright rather
|
|
1252
|
+
// than answering from a flag. There is nothing to confirm when nobody is
|
|
1253
|
+
// watching, and the command itself was the confirmation.
|
|
1254
|
+
//
|
|
1255
|
+
// NOT the same call as `uninstall`, which does require --yes: that one
|
|
1256
|
+
// REMOVES things, so silence there would destroy on a signal as weak as a
|
|
1257
|
+
// missing terminal. This one does exactly what its name says.
|
|
1258
|
+
const decision = unattended ? "apply" : await selectOne<"apply" | "cancel">({
|
|
1177
1259
|
message: "Ready to apply?",
|
|
1178
1260
|
body: reviewLines({
|
|
1179
1261
|
target,
|
|
@@ -1463,6 +1545,18 @@ export async function runConfigureWizard(io: WizardIO = {}): Promise<WizardResul
|
|
|
1463
1545
|
{ ok: true },
|
|
1464
1546
|
stdout,
|
|
1465
1547
|
);
|
|
1548
|
+
// Setup wires the hooks and deliberately chooses NO policies, so a machine
|
|
1549
|
+
// that has just finished it enforces almost nothing — and the summary line
|
|
1550
|
+
// says "0 policies" without saying what to do about it. Anyone else's pack is
|
|
1551
|
+
// typed the same way, which is the point: ours is named in full rather than
|
|
1552
|
+
// by a short name only we could use.
|
|
1553
|
+
if (policies.length === 0) {
|
|
1554
|
+
stdout.write(
|
|
1555
|
+
`\n Nothing is enforcing yet. Take ours, or anyone's:\n` +
|
|
1556
|
+
` failproofai policies add ${CORE_SOURCE}\n` +
|
|
1557
|
+
` failproofai policies show <owner>/<repo> (look first)\n\n`,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1466
1560
|
return {
|
|
1467
1561
|
applied: true,
|
|
1468
1562
|
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
|
@@ -14,7 +14,7 @@ import { INTEGRATION_TYPES,
|
|
|
14
14
|
type HookScope,
|
|
15
15
|
type IntegrationType,
|
|
16
16
|
} from "./types";
|
|
17
|
-
import { claudeCode, getIntegration, settingsPathsFor } from "./integrations";
|
|
17
|
+
import { claudeCode, getIntegration, settingsPathsFor, type Integration } from "./integrations";
|
|
18
18
|
import { promptPolicySelection } from "./install-prompt";
|
|
19
19
|
import { configuredCustomPolicyPaths, readMergedHooksConfig, readScopedHooksConfig, writeScopedHooksConfig, syncConventionPolicies, findProjectConfigDir } from "./hooks-config";
|
|
20
20
|
import type { HooksConfig, ConventionPolicyRecord } from "./policy-types";
|
|
@@ -271,9 +271,44 @@ function deduplicateScopes(scopes: readonly HookScope[], cwd?: string): HookScop
|
|
|
271
271
|
* rows of the listing already answer it.
|
|
272
272
|
*/
|
|
273
273
|
export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolean {
|
|
274
|
-
return
|
|
274
|
+
return integrationsInstalledAt(scope, cwd).length > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Which integrations are wired at EXACTLY this scope — the honest form of the
|
|
279
|
+
* question above, and the one the multi-scope warning needs.
|
|
280
|
+
*
|
|
281
|
+
* Two ways an integration answers yes for a scope it is not actually installed
|
|
282
|
+
* at, both of which made `failproofai policies` warn about "hooks in multiple
|
|
283
|
+
* scopes" on a machine whose hooks are in exactly one file:
|
|
284
|
+
*
|
|
285
|
+
* - It does not SUPPORT the scope. Hermes and OpenClaw are user-scope only
|
|
286
|
+
* (`HERMES_HOOK_SCOPES = ["user"]`) and their `getSettingsPath` ignores the
|
|
287
|
+
* scope argument entirely, so they hand back the user file — and report it
|
|
288
|
+
* installed — for `project` and `local` alike. `scopes` already declares
|
|
289
|
+
* this; nothing was reading it.
|
|
290
|
+
* - Its project path RESOLVES to its user path. Run this from `$HOME` and
|
|
291
|
+
* `<cwd>/.claude/settings.json` is `~/.claude/settings.json`: one file,
|
|
292
|
+
* counted as two scopes. Cheap to detect and impossible to get right by
|
|
293
|
+
* asking each integration separately.
|
|
294
|
+
*/
|
|
295
|
+
export function integrationsInstalledAt(scope: HookScope, cwd?: string): IntegrationType[] {
|
|
296
|
+
return INTEGRATION_TYPES.filter((id) => {
|
|
275
297
|
try {
|
|
276
|
-
|
|
298
|
+
const integration = getIntegration(id);
|
|
299
|
+
if (!integration.scopes.includes(scope)) return false;
|
|
300
|
+
if (!integration.hooksInstalledInSettings(scope, cwd)) return false;
|
|
301
|
+
// A narrower scope that lands on the same file as a wider one is that
|
|
302
|
+
// wider one, seen twice. Attribute it to the widest scope that resolves
|
|
303
|
+
// there so exactly one of them counts.
|
|
304
|
+
const here = integration.getSettingsPath(scope, cwd);
|
|
305
|
+
return !HOOK_SCOPES.some(
|
|
306
|
+
(other) =>
|
|
307
|
+
other !== scope &&
|
|
308
|
+
HOOK_SCOPES.indexOf(other) < HOOK_SCOPES.indexOf(scope) &&
|
|
309
|
+
integration.scopes.includes(other) &&
|
|
310
|
+
safeSettingsPath(integration, other, cwd) === here,
|
|
311
|
+
);
|
|
277
312
|
} catch {
|
|
278
313
|
// An integration whose settings file is unreadable is not evidence that
|
|
279
314
|
// nothing is installed — keep asking the rest.
|
|
@@ -282,6 +317,18 @@ export function hooksInstalledInSettings(scope: HookScope, cwd?: string): boolea
|
|
|
282
317
|
});
|
|
283
318
|
}
|
|
284
319
|
|
|
320
|
+
function safeSettingsPath(
|
|
321
|
+
integration: Integration,
|
|
322
|
+
scope: HookScope,
|
|
323
|
+
cwd?: string,
|
|
324
|
+
): string | null {
|
|
325
|
+
try {
|
|
326
|
+
return integration.getSettingsPath(scope, cwd);
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
285
332
|
export interface InstallHooksOptions {
|
|
286
333
|
/** Replace the enabled set at this scope instead of unioning (default: additive). */
|
|
287
334
|
replace?: boolean;
|
|
@@ -531,7 +578,7 @@ async function installHooksImpl(
|
|
|
531
578
|
} catch (err) {
|
|
532
579
|
console.log(
|
|
533
580
|
`\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
534
|
-
`Run \`failproofai policies add
|
|
581
|
+
`Run \`failproofai policies add FailproofAI/policies\` once you are online.`,
|
|
535
582
|
);
|
|
536
583
|
}
|
|
537
584
|
} else {
|
|
@@ -944,6 +991,24 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
944
991
|
// a reader scrolling back from their prompt actually sees.
|
|
945
992
|
const footer: Array<string[] | null> = [note(`Config: ${globalPolicyConfigFile()}`, opts)];
|
|
946
993
|
|
|
994
|
+
// A machine with hooks wired and no policies to run is the normal state right
|
|
995
|
+
// after setup — setup installs the hooks and deliberately chooses nothing —
|
|
996
|
+
// and this listing said NOTHING about how to leave it. A reader who had just
|
|
997
|
+
// finished `failproofai config` saw a header, a config path, and no next
|
|
998
|
+
// step. Ours is named in full, the same way anyone else's pack is typed,
|
|
999
|
+
// because it IS one.
|
|
1000
|
+
if (packCount === 0) {
|
|
1001
|
+
footer.unshift(
|
|
1002
|
+
nextStep(
|
|
1003
|
+
`failproofai policies add ${CORE_SOURCE}`,
|
|
1004
|
+
"Nothing is enforcing yet. Take ours, or anyone's:",
|
|
1005
|
+
opts,
|
|
1006
|
+
),
|
|
1007
|
+
note("Someone else's: failproofai policies add <owner>/<repo>", opts),
|
|
1008
|
+
note("Look first: failproofai policies show <owner>/<repo>", opts),
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
947
1012
|
if (installedScopes.length > 1) {
|
|
948
1013
|
footer.push(
|
|
949
1014
|
warning(
|
|
@@ -157,10 +157,11 @@ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): b
|
|
|
157
157
|
// command would just repeat the hard-fail. Only a new release is a reason
|
|
158
158
|
// to ask again (it might add support for this platform).
|
|
159
159
|
return probe.cliVersion !== attempt.cliVersion;
|
|
160
|
-
case "not_a_tty":
|
|
161
160
|
case "running_as_sudo":
|
|
162
|
-
//
|
|
163
|
-
//
|
|
161
|
+
// A property of the invocation, not of the machine, so the next one may
|
|
162
|
+
// well be fine. (`not_a_tty` used to sit here too. It is gone because
|
|
163
|
+
// there is no such refusal any more: a run with no terminal has no
|
|
164
|
+
// questions to ask, so it applies rather than declining.)
|
|
164
165
|
return true;
|
|
165
166
|
default:
|
|
166
167
|
return true;
|
|
@@ -174,7 +175,6 @@ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
|
|
|
174
175
|
daemon_failed: "the failproofaid service could not be started",
|
|
175
176
|
cancelled: "it was cancelled",
|
|
176
177
|
unsupported_platform: "failproofaid does not run on this platform",
|
|
177
|
-
not_a_tty: "there was no terminal to ask in",
|
|
178
178
|
running_as_sudo: "it was run under sudo",
|
|
179
179
|
};
|
|
180
180
|
const detail = why[attempt.reason] ?? "it did not finish";
|