failproofai 1.0.2-beta.0 → 1.0.2-beta.1
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.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.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/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.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 +14 -14
- 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]__0l4748x._.js +3 -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]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bwu160._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_12l4a_k._.js → _0ns6k3a._.js} +1 -1
- 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/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/{35xg6retpe6rp.js → 01u1cf8js2yd6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2i2yt3hzjredm.js → 0izgu7nqh2zp-.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u4-org_e7zde.js +1 -0
- package/.next/standalone/.next/static/chunks/{3y54nw0i7dsjl.js → 1tynnrrh711lr.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0sa9v5mhc8861.js → 22rqvlr7p_1mv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hs7ycvsav7v2.js → 2whxqyskpirbk.js} +1 -1
- package/.next/standalone/.next/static/chunks/31mwk8k0q3_3g.js +6 -0
- package/.next/standalone/.next/static/chunks/{2gn7ut4svofm2.js → 3ms6gxfm82t6s.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0eqky-7n7dgdm.js → 3tbbs_d8xrvyp.js} +1 -1
- package/.next/standalone/.next/static/chunks/43nlc236oo898.js +1 -0
- package/.next/standalone/app/actions/pack-actions.ts +21 -11
- package/.next/standalone/app/policies/hooks-client.tsx +3 -4
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -1
- package/.next/standalone/package.json +10 -11
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +455 -316
- package/dist/cli.mjs +2296 -1736
- package/dist/worker.mjs +3 -12
- package/package.json +10 -11
- package/src/hooks/configure-wizard.ts +55 -368
- package/src/hooks/fp-reset.ts +10 -2
- package/src/hooks/handler.ts +2 -2
- package/src/hooks/manager.ts +14 -8
- package/src/hooks/pack-cli.ts +423 -43
- package/src/hooks/pack-failclosed.ts +1 -1
- package/src/hooks/pack-manifest.ts +14 -6
- package/src/hooks/pack-store.ts +89 -128
- package/src/hooks/tui.ts +126 -40
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zig5sv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_00e9tnf._.js +0 -23
- package/.next/standalone/.next/static/chunks/1nuqytbta44af.js +0 -1
- package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +0 -6
- package/.next/standalone/.next/static/chunks/2aawsjld9ied_.js +0 -1
- package/policy-pack/SHA256SUMS +0 -2
- package/policy-pack/failproofai-pack.json +0 -732
- package/policy-pack/failproofai-pack.mjs +0 -2017
- package/src/hooks/policy-presets.ts +0 -137
- /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → I5kEb9kSgYS63j4iqfVLO}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → I5kEb9kSgYS63j4iqfVLO}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → I5kEb9kSgYS63j4iqfVLO}/_ssgManifest.js +0 -0
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
|
|
326
|
+
`run \`failproofai policies add core\` to move them into a real pack`,
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
// What actually registered, for the pack-duplicate check below. A paused
|
|
@@ -432,7 +432,7 @@ export async function evaluateHookEvent(
|
|
|
432
432
|
// A pack policy whose name is an ENABLED BUILTIN is the same guard
|
|
433
433
|
// twice. The two register under different keys — `failproofai/block-sudo`
|
|
434
434
|
// and `pack/<id>@<version>/block-sudo` — so nothing deduped them, and
|
|
435
|
-
// the
|
|
435
|
+
// the core pack carries the builtins under exactly these names.
|
|
436
436
|
//
|
|
437
437
|
// A deny hides it (the first verdict short-circuits) but an INSTRUCT does
|
|
438
438
|
// not: instructions accumulate and are joined, so the agent received the
|
package/src/hooks/manager.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { CliError } from "../cli-error";
|
|
|
26
26
|
import { hookLogWarn } from "./hook-logger";
|
|
27
27
|
import { customPoliciesDir, globalPolicyConfigFile } from "./fp-home";
|
|
28
28
|
import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
29
|
-
import {
|
|
29
|
+
import { CORE_SOURCE, addPack, setPackPolicyEnabled } from "./pack-store";
|
|
30
30
|
import type { ResolvedPack } from "./pack-manifest";
|
|
31
31
|
import { hasInstalledPacks, readInstalledPacks } from "./pack-manifest";
|
|
32
32
|
import {
|
|
@@ -480,7 +480,7 @@ async function installHooksImpl(
|
|
|
480
480
|
// would leave a freshly set-up machine enforcing nothing at all — which is
|
|
481
481
|
// the failure mode this whole product exists to prevent.
|
|
482
482
|
//
|
|
483
|
-
//
|
|
483
|
+
// Fetched from the pack's GitHub release: there is no copy in this package,
|
|
484
484
|
// cannot fail behind a proxy, and the machine is guarded the moment it is
|
|
485
485
|
// configured rather than the moment it next reaches github.com.
|
|
486
486
|
// The always-on guard is excluded: a pack may not declare `alwaysOn`, so the
|
|
@@ -489,7 +489,7 @@ async function installHooksImpl(
|
|
|
489
489
|
const alwaysOnNames = new Set(BUILTIN_POLICIES.filter((p) => p.alwaysOn).map((p) => p.name));
|
|
490
490
|
const fromPack = selectedPolicies.filter((name) => !alwaysOnNames.has(name));
|
|
491
491
|
if (fromPack.length > 0) {
|
|
492
|
-
// ONLY when there is no pack yet.
|
|
492
|
+
// ONLY when there is no pack yet. Fetching the core pack with
|
|
493
493
|
// `only: <selection>` REPLACES whatever the machine had chosen, and
|
|
494
494
|
// `selectedPolicies` is derived from `enabledPolicies` — which is empty on a
|
|
495
495
|
// machine whose pack came from `pack add core`. `policy add block-rm-rf`
|
|
@@ -499,11 +499,17 @@ async function installHooksImpl(
|
|
|
499
499
|
// With a pack already installed the names are switched on individually
|
|
500
500
|
// instead, which is additive and touches nothing else.
|
|
501
501
|
if (!hasInstalledPacks()) {
|
|
502
|
-
|
|
503
|
-
|
|
502
|
+
// Fetched, not unpacked from this package: there is no copy in here any
|
|
503
|
+
// more. That makes this the one path in `policies --install` that needs
|
|
504
|
+
// the network, so its failure is reported rather than thrown — the names
|
|
505
|
+
// are already written to config, and the no-pack fallback enforces them
|
|
506
|
+
// until a pack arrives.
|
|
507
|
+
try {
|
|
508
|
+
await addPack(CORE_SOURCE, { only: fromPack });
|
|
509
|
+
} catch (err) {
|
|
504
510
|
console.log(
|
|
505
|
-
`\nWarning: could not
|
|
506
|
-
`
|
|
511
|
+
`\nWarning: could not fetch the policy pack (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
512
|
+
`Run \`failproofai policies add core\` once you are online.`,
|
|
507
513
|
);
|
|
508
514
|
}
|
|
509
515
|
} else {
|
|
@@ -1034,7 +1040,7 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
1034
1040
|
}
|
|
1035
1041
|
|
|
1036
1042
|
// Installed packs. They enforce on this machine exactly like every section
|
|
1037
|
-
// above, and until now the only way to see one was `failproofai
|
|
1043
|
+
// above, and until now the only way to see one was `failproofai policies` —
|
|
1038
1044
|
// so the command that answers "what is enforcing here?" answered it with a
|
|
1039
1045
|
// subset, for the one source a person had to go out of their way to install.
|
|
1040
1046
|
try {
|
package/src/hooks/pack-cli.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `failproofai
|
|
2
|
+
* `failproofai policies add | remove | list`.
|
|
3
3
|
*
|
|
4
4
|
* Presentation only — every rule about what a pack may be lives in
|
|
5
5
|
* `pack-manifest.ts` (what may load) and `pack-store.ts` (what may install).
|
|
6
6
|
* This layer decides nothing; it formats.
|
|
7
7
|
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
9
10
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { resolve } from "node:path";
|
|
@@ -17,15 +18,19 @@ import {
|
|
|
17
18
|
checkPackArtifact,
|
|
18
19
|
CORE_ALIASES,
|
|
19
20
|
fetchPackPreview,
|
|
20
|
-
|
|
21
|
+
CORE_SOURCE,
|
|
22
|
+
packTagMatchesVersion,
|
|
21
23
|
removePack,
|
|
24
|
+
setPackPolicyEnabled,
|
|
22
25
|
slugifyCategory,
|
|
23
26
|
} from "./pack-store";
|
|
24
27
|
import type { PolicyEffect } from "./cloud-managed-policies";
|
|
25
28
|
import { loadCustomHooks } from "./custom-hooks-loader";
|
|
29
|
+
import type { MultiChoice, TTYIn, TTYOut } from "./tui";
|
|
26
30
|
import {
|
|
27
31
|
chip,
|
|
28
32
|
emptyState,
|
|
33
|
+
multiSelect,
|
|
29
34
|
note,
|
|
30
35
|
nextStep,
|
|
31
36
|
optsFor,
|
|
@@ -120,7 +125,7 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
120
125
|
|
|
121
126
|
if (!entry || !id || !version) {
|
|
122
127
|
return fail([
|
|
123
|
-
"Usage: failproofai
|
|
128
|
+
"Usage: failproofai publish <entry.mjs> --repo <owner>/<repo> --version <version>",
|
|
124
129
|
" [--out <dir>] [--effect enforce|observe]",
|
|
125
130
|
]);
|
|
126
131
|
}
|
|
@@ -207,44 +212,413 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
207
212
|
` ${outDir}/${PACK_CHECKSUMS_ASSET}`,
|
|
208
213
|
"",
|
|
209
214
|
`Publish: attach all three to a GitHub release tagged ${identity.version}, then anyone runs:`,
|
|
210
|
-
` failproofai
|
|
215
|
+
` failproofai policies add <owner>/<repo>`,
|
|
211
216
|
]);
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
/**
|
|
220
|
+
* The list, with what is already on ticked, for `failproofai policies add` with
|
|
221
|
+
* nothing after it.
|
|
222
|
+
*
|
|
223
|
+
* Naming nothing used to be an error — "Missing policy name", followed by an
|
|
224
|
+
* instruction to go and read a list somewhere else and come back. That is the
|
|
225
|
+
* command telling the user to do the work it is for. The same objection applies
|
|
226
|
+
* to `pack add` taking a publisher's defaults and only afterwards printing what
|
|
227
|
+
* it decided: a default is a suggestion, and a suggestion the user never saw is
|
|
228
|
+
* just a decision taken on their behalf.
|
|
229
|
+
*
|
|
230
|
+
* So this is one screen showing every policy on the machine, grouped by pack and
|
|
231
|
+
* category, with the current state pre-ticked. It edits the enabled SET — there
|
|
232
|
+
* is no separate "remove" screen, because choosing what is on and choosing what
|
|
233
|
+
* is off are the same act.
|
|
234
|
+
*
|
|
235
|
+
* Refuses rather than guesses when there is no terminal to draw on. `multiSelect`
|
|
236
|
+
* degrades by returning its pre-checked set, which here would mean "confirm
|
|
237
|
+
* exactly what is already true" — a silent no-op reported as a success. A script
|
|
238
|
+
* that lands in this branch asked the wrong question and should be told so.
|
|
239
|
+
*/
|
|
240
|
+
export async function runPolicyPicker(
|
|
241
|
+
action: "add" | "remove",
|
|
242
|
+
io: { stdin?: TTYIn; stdout?: TTYOut } = {},
|
|
243
|
+
): Promise<PackCliResult> {
|
|
244
|
+
const stdin = io.stdin ?? process.stdin;
|
|
245
|
+
const stdout = io.stdout ?? process.stdout;
|
|
246
|
+
const opts = optsFor(stdout as NodeJS.WriteStream);
|
|
247
|
+
const { packs } = readInstalledPacks();
|
|
248
|
+
|
|
249
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
250
|
+
return fail([
|
|
251
|
+
`\`policies ${action}\` with no name needs a terminal to show you the list.`,
|
|
252
|
+
"From a script, name what you mean:",
|
|
253
|
+
` failproofai policies ${action} <policy-name>`,
|
|
254
|
+
` failproofai policies ${action} <owner>/<repo> [--policy a,b] [--category x,y] [--all]`,
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
217
257
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
if (packs.length === 0) {
|
|
259
|
+
return ok(
|
|
260
|
+
stack(
|
|
261
|
+
emptyState(
|
|
262
|
+
{
|
|
263
|
+
what: "No policies are installed yet.",
|
|
264
|
+
hint: "Take ours, or anyone's:",
|
|
265
|
+
cmd: "failproofai policies add core",
|
|
266
|
+
},
|
|
267
|
+
opts,
|
|
268
|
+
),
|
|
269
|
+
note("Someone else's: failproofai policies add <owner>/<repo>", opts),
|
|
270
|
+
note("Look first: failproofai policies show <owner>/<repo>", opts),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// One row per policy across every pack. The pack id leads the section heading
|
|
276
|
+
// because two packs may legitimately ship a category of the same name, and a
|
|
277
|
+
// row that says only "Data" would not tell you whose Data it is.
|
|
278
|
+
const rows: MultiChoice<string>[] = [];
|
|
279
|
+
const owner = new Map<string, string>();
|
|
280
|
+
for (const pack of packs) {
|
|
281
|
+
const taken = new Set(pack.enabled ?? pack.policies.map((pol) => pol.name));
|
|
282
|
+
const categories = [...new Set(pack.policies.map((pol) => pol.category))];
|
|
283
|
+
for (const category of categories) {
|
|
284
|
+
for (const pol of pack.policies.filter((x) => x.category === category)) {
|
|
285
|
+
// Keyed by name, not by pack+name: a name collision across two packs is
|
|
286
|
+
// already impossible to enforce unambiguously, and silently editing the
|
|
287
|
+
// wrong pack's copy would be worse than the collision.
|
|
288
|
+
owner.set(pol.name, pack.id);
|
|
289
|
+
rows.push({
|
|
290
|
+
label: pol.name,
|
|
291
|
+
value: pol.name,
|
|
292
|
+
hint: pol.description,
|
|
293
|
+
checked: taken.has(pol.name),
|
|
294
|
+
section: `${pack.id} · ${category}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
224
297
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const before = new Set(rows.filter((r) => r.checked).map((r) => r.value));
|
|
301
|
+
const picked = await multiSelect<string>({
|
|
302
|
+
message: "Which policies should be on?",
|
|
303
|
+
choices: rows,
|
|
304
|
+
summaryNoun: "policies",
|
|
305
|
+
hint: "space toggles · ctrl+a all · ↵ confirm · what is on now is ticked",
|
|
306
|
+
stdin,
|
|
307
|
+
stdout,
|
|
308
|
+
});
|
|
309
|
+
if (picked === null) return ok(["Nothing changed."]);
|
|
310
|
+
|
|
311
|
+
const after = new Set(picked);
|
|
312
|
+
const turnedOn = [...after].filter((n) => !before.has(n));
|
|
313
|
+
const turnedOff = [...before].filter((n) => !after.has(n));
|
|
314
|
+
if (turnedOn.length === 0 && turnedOff.length === 0) {
|
|
315
|
+
return ok([`Nothing changed — ${after.size} of ${rows.length} still on.`]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const failures: string[] = [];
|
|
319
|
+
for (const name of [...turnedOn, ...turnedOff]) {
|
|
320
|
+
const packId = owner.get(name);
|
|
321
|
+
if (!packId) continue;
|
|
322
|
+
const result = setPackPolicyEnabled(packId, name, after.has(name));
|
|
323
|
+
if (!result.ok) failures.push(`${name}: ${result.reason ?? "could not be written"}`);
|
|
324
|
+
}
|
|
325
|
+
if (failures.length > 0) {
|
|
326
|
+
return fail(["Some policies could not be changed:", ...failures.map((f) => ` ${f}`)]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const lines = [`${after.size} of ${rows.length} policies on.`];
|
|
330
|
+
if (turnedOn.length > 0) lines.push(` turned on (${turnedOn.length}): ${summarise(turnedOn)}`);
|
|
331
|
+
if (turnedOff.length > 0) lines.push(` turned off (${turnedOff.length}): ${summarise(turnedOff)}`);
|
|
332
|
+
return ok(lines);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
// ── publish ───────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
const GITHUB_API = process.env.FAILPROOFAI_GITHUB_API ?? "https://api.github.com";
|
|
339
|
+
const GITHUB_UPLOADS = process.env.FAILPROOFAI_GITHUB_UPLOADS ?? "https://uploads.github.com";
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The credential, from the places a developer already keeps one.
|
|
343
|
+
*
|
|
344
|
+
* `gh auth token` is READ-ONLY and is deliberately not one of the subcommands
|
|
345
|
+
* `block-gh-pipeline` matches — which is exactly why the release itself goes
|
|
346
|
+
* over REST rather than through `gh release create`: that one IS matched, so a
|
|
347
|
+
* user with our own policy switched on could not publish with a gh-shaped
|
|
348
|
+
* implementation. Shipping a publish path our own guardrail blocks is not a
|
|
349
|
+
* thing to do.
|
|
350
|
+
*
|
|
351
|
+
* Never returned in any message. A token that reaches stdout reaches CI logs.
|
|
352
|
+
*/
|
|
353
|
+
function githubToken(): string | null {
|
|
354
|
+
const fromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
355
|
+
if (fromEnv) return fromEnv.trim();
|
|
356
|
+
try {
|
|
357
|
+
const out = execFileSync("gh", ["auth", "token"], {
|
|
358
|
+
encoding: "utf8",
|
|
359
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
360
|
+
timeout: 5_000,
|
|
361
|
+
});
|
|
362
|
+
return out.trim() || null;
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function gh(
|
|
369
|
+
url: string,
|
|
370
|
+
token: string,
|
|
371
|
+
init: { method?: string; body?: BodyInit; contentType?: string } = {},
|
|
372
|
+
): Promise<{ status: number; json: Record<string, unknown> | null; text: string }> {
|
|
373
|
+
const response = await fetch(url, {
|
|
374
|
+
method: init.method ?? "GET",
|
|
375
|
+
headers: {
|
|
376
|
+
Authorization: `Bearer ${token}`,
|
|
377
|
+
Accept: "application/vnd.github+json",
|
|
378
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
379
|
+
"User-Agent": "failproofai",
|
|
380
|
+
...(init.contentType ? { "Content-Type": init.contentType } : {}),
|
|
381
|
+
},
|
|
382
|
+
...(init.body ? { body: init.body } : {}),
|
|
383
|
+
signal: AbortSignal.timeout(60_000),
|
|
384
|
+
});
|
|
385
|
+
const text = await response.text();
|
|
386
|
+
let json: Record<string, unknown> | null = null;
|
|
387
|
+
try {
|
|
388
|
+
json = text ? (JSON.parse(text) as Record<string, unknown>) : null;
|
|
389
|
+
} catch {
|
|
390
|
+
json = null;
|
|
391
|
+
}
|
|
392
|
+
return { status: response.status, json, text };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** GitHub's error bodies are shaped `{message, errors:[{code,field}]}`; the bare
|
|
396
|
+
* `message` alone is frequently just "Validation Failed". */
|
|
397
|
+
function ghError(res: { status: number; json: Record<string, unknown> | null; text: string }): string {
|
|
398
|
+
const message = typeof res.json?.message === "string" ? res.json.message : res.text.slice(0, 200);
|
|
399
|
+
const errors = Array.isArray(res.json?.errors)
|
|
400
|
+
? (res.json.errors as Array<Record<string, unknown>>)
|
|
401
|
+
.map((e) => [e.field, e.code].filter(Boolean).join(" "))
|
|
402
|
+
.filter(Boolean)
|
|
403
|
+
: [];
|
|
404
|
+
return errors.length > 0 ? `${message} (${errors.join("; ")})` : message;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Build the assets AND put them on a GitHub release, in one command.
|
|
409
|
+
*
|
|
410
|
+
* Publishing used to be four commands, and two of them did nothing: `git init`,
|
|
411
|
+
* `git add`, `git commit`, `gh repo create`, `gh release create`. Only the last
|
|
412
|
+
* one publishes — installs read `releases/download/<tag>/<asset>` and never
|
|
413
|
+
* touch the git tree, so pushing source is for humans reading it, not for the
|
|
414
|
+
* install to work. A publisher had to discover that by reading `pack-store.ts`.
|
|
415
|
+
*
|
|
416
|
+
* With no `--repo` this is exactly the old `pack build`: write the three assets
|
|
417
|
+
* and stop. That is what `pack build` now resolves to, so the old command keeps
|
|
418
|
+
* working and means the same thing.
|
|
419
|
+
*
|
|
420
|
+
* Two failures the manual flow let through silently, both refused here:
|
|
421
|
+
* - a tag that does not describe the manifest version, which installs and then
|
|
422
|
+
* reports a version matching no URL;
|
|
423
|
+
* - a private repository, which publishes to nobody, because installs are
|
|
424
|
+
* anonymous HTTPS with no credential to offer.
|
|
425
|
+
*/
|
|
426
|
+
async function publish(rest: string[]): Promise<PackCliResult> {
|
|
427
|
+
const flag = (name: string): string | undefined => {
|
|
428
|
+
const i = rest.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
429
|
+
if (i === -1) return undefined;
|
|
430
|
+
return rest[i].includes("=") ? rest[i].split("=").slice(1).join("=") : rest[i + 1];
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const repo = flag("repo");
|
|
434
|
+
const version = flag("version");
|
|
435
|
+
const entry = packAddSource(rest.filter((a) => a !== "--dry-run")) ?? flag("entry");
|
|
436
|
+
// The id and the repo are usually the same words, and requiring both is asking
|
|
437
|
+
// the same question twice. Either one alone answers for the other.
|
|
438
|
+
const id = flag("id") ?? repo;
|
|
439
|
+
const tag = flag("tag") ?? version;
|
|
440
|
+
const dryRun = rest.includes("--dry-run") || !repo;
|
|
441
|
+
|
|
442
|
+
if (!entry || !version || !id) {
|
|
443
|
+
return fail([
|
|
444
|
+
"Usage: failproofai publish <entry.mjs> --repo <owner>/<repo> --version <version>",
|
|
445
|
+
" [--id <publisher/name>] [--tag <tag>] [--notes <text>]",
|
|
446
|
+
" [--out <dir>] [--effect enforce|observe] [--dry-run]",
|
|
447
|
+
"",
|
|
448
|
+
"With no --repo it writes the three release assets and stops.",
|
|
449
|
+
]);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Build first, always. There is no point authenticating against GitHub to
|
|
453
|
+
// discover the pack does not load.
|
|
454
|
+
const built = await build([entry, "--id", id, "--version", version, ...outFlagFrom(rest), ...effectFlagFrom(rest)]);
|
|
455
|
+
if (built.exitCode !== 0) return built;
|
|
456
|
+
|
|
457
|
+
const outDir = resolve(flag("out") ?? "dist-pack");
|
|
458
|
+
if (dryRun) {
|
|
459
|
+
return ok([
|
|
460
|
+
...built.lines.slice(0, 4),
|
|
461
|
+
"",
|
|
462
|
+
...(repo
|
|
463
|
+
? ["Dry run — nothing was published.", `Drop --dry-run to release it on ${repo}.`]
|
|
464
|
+
: [
|
|
465
|
+
"Nothing was published: name a repository to release it on.",
|
|
466
|
+
` failproofai publish ${entry} --repo <owner>/<repo> --version ${version}`,
|
|
467
|
+
]),
|
|
468
|
+
]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!packTagMatchesVersion(tag!, version)) {
|
|
472
|
+
return fail([
|
|
473
|
+
`Tag ${tag} does not describe version ${version}, so nobody could install it.`,
|
|
474
|
+
`A pack is fetched from releases/download/${tag}/… and then reports ${version} —`,
|
|
475
|
+
`which names no release. Use --tag ${version} (a leading v is fine), or build`,
|
|
476
|
+
"the pack at the version the tag says.",
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const [owner, name] = repo!.split("/");
|
|
481
|
+
if (!owner || !name || repo!.split("/").length !== 2) {
|
|
482
|
+
return fail([`--repo must be <owner>/<repo>, got ${JSON.stringify(repo)}`]);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const token = githubToken();
|
|
486
|
+
if (!token) {
|
|
487
|
+
return fail([
|
|
488
|
+
"No GitHub credential found.",
|
|
489
|
+
"Set GITHUB_TOKEN (or GH_TOKEN), or sign in once with `gh auth login`.",
|
|
490
|
+
"It needs write access to releases on that repository, and nothing else.",
|
|
491
|
+
]);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const repoInfo = await gh(`${GITHUB_API}/repos/${owner}/${name}`, token);
|
|
495
|
+
if (repoInfo.status === 404) {
|
|
496
|
+
return fail([
|
|
497
|
+
`${repo} does not exist, or that credential cannot see it.`,
|
|
498
|
+
`Create it first: gh repo create ${repo} --public`,
|
|
499
|
+
]);
|
|
500
|
+
}
|
|
501
|
+
if (repoInfo.status >= 400) return fail([`Could not read ${repo}: ${ghError(repoInfo)}`]);
|
|
502
|
+
// A private repo is not a smaller audience, it is no audience: `pack add`
|
|
503
|
+
// sends no Authorization header at all, by design, so every install 404s.
|
|
504
|
+
const isPrivate = repoInfo.json?.private === true;
|
|
505
|
+
|
|
506
|
+
// Reuse a release on this tag rather than failing on it. Re-publishing a
|
|
507
|
+
// corrected artifact under an existing tag is a normal thing to need, and the
|
|
508
|
+
// digest change is visible to anyone who reinstalls.
|
|
509
|
+
let releaseId: number | null = null;
|
|
510
|
+
const existing = await gh(`${GITHUB_API}/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag!)}`, token);
|
|
511
|
+
if (existing.status === 200 && typeof existing.json?.id === "number") {
|
|
512
|
+
releaseId = existing.json.id;
|
|
513
|
+
} else {
|
|
514
|
+
const created = await gh(`${GITHUB_API}/repos/${owner}/${name}/releases`, token, {
|
|
515
|
+
method: "POST",
|
|
516
|
+
contentType: "application/json",
|
|
517
|
+
body: JSON.stringify({
|
|
518
|
+
tag_name: tag,
|
|
519
|
+
name: `${id} ${version}`,
|
|
520
|
+
body: flag("notes") ?? `${id}@${version}`,
|
|
521
|
+
// A prerelease is invisible to `releases/latest`, which is how a tagless
|
|
522
|
+
// `policies add owner/repo` resolves a version — so publishing one would
|
|
523
|
+
// make the pack installable only by people who already knew its tag.
|
|
524
|
+
prerelease: false,
|
|
525
|
+
draft: false,
|
|
526
|
+
}),
|
|
527
|
+
});
|
|
528
|
+
if (created.status >= 400 || typeof created.json?.id !== "number") {
|
|
529
|
+
return fail([`Could not create the release on ${repo}: ${ghError(created)}`]);
|
|
242
530
|
}
|
|
243
|
-
|
|
531
|
+
releaseId = created.json.id;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const assets = [PACK_MANIFEST_ASSET, PACK_ENTRY_ASSET, PACK_CHECKSUMS_ASSET];
|
|
535
|
+
const uploaded: string[] = [];
|
|
536
|
+
const listed = await gh(`${GITHUB_API}/repos/${owner}/${name}/releases/${releaseId}/assets?per_page=100`, token);
|
|
537
|
+
const already = Array.isArray(listed.json)
|
|
538
|
+
? (listed.json as unknown as Array<{ id: number; name: string }>)
|
|
539
|
+
: [];
|
|
540
|
+
for (const asset of assets) {
|
|
541
|
+
// Asset names are fixed and the URL is constructed from them, so a stale
|
|
542
|
+
// copy under the same name is what an installer would fetch. Replace, never
|
|
543
|
+
// append.
|
|
544
|
+
const prior = already.find((a) => a.name === asset);
|
|
545
|
+
if (prior) {
|
|
546
|
+
await gh(`${GITHUB_API}/repos/${owner}/${name}/releases/assets/${prior.id}`, token, { method: "DELETE" });
|
|
547
|
+
}
|
|
548
|
+
const bytes = readFileSync(resolve(outDir, asset));
|
|
549
|
+
const upload = await gh(
|
|
550
|
+
`${GITHUB_UPLOADS}/repos/${owner}/${name}/releases/${releaseId}/assets?name=${encodeURIComponent(asset)}`,
|
|
551
|
+
token,
|
|
552
|
+
{
|
|
553
|
+
method: "POST",
|
|
554
|
+
contentType: asset.endsWith(".json") ? "application/json" : "application/octet-stream",
|
|
555
|
+
body: new Uint8Array(bytes),
|
|
556
|
+
},
|
|
557
|
+
);
|
|
558
|
+
if (upload.status >= 400) {
|
|
559
|
+
return fail([
|
|
560
|
+
`Uploaded ${uploaded.length} of ${assets.length} assets, then ${asset} failed: ${ghError(upload)}`,
|
|
561
|
+
`The release exists but is INCOMPLETE — an install would fail on the missing asset.`,
|
|
562
|
+
`Re-run the same command; existing assets are replaced, not duplicated.`,
|
|
563
|
+
]);
|
|
564
|
+
}
|
|
565
|
+
uploaded.push(asset);
|
|
244
566
|
}
|
|
245
567
|
|
|
246
|
-
|
|
247
|
-
|
|
568
|
+
const lines = [
|
|
569
|
+
`Published ${id}@${version} to ${repo} at tag ${tag}.`,
|
|
570
|
+
` ${assets.length} assets attached`,
|
|
571
|
+
"",
|
|
572
|
+
"Anyone can now install it:",
|
|
573
|
+
` failproofai policies add ${repo}`,
|
|
574
|
+
` failproofai policies show ${repo} (look first, without running it)`,
|
|
575
|
+
];
|
|
576
|
+
if (isPrivate) {
|
|
577
|
+
lines.push(
|
|
578
|
+
"",
|
|
579
|
+
`WARNING: ${repo} is PRIVATE, so nobody can install this.`,
|
|
580
|
+
"Installs are anonymous HTTPS with no credential to offer — every one will 404.",
|
|
581
|
+
`Make it public: gh repo edit ${repo} --visibility public`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return ok(lines);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Pass --out through to build without re-parsing it. */
|
|
588
|
+
function outFlagFrom(rest: string[]): string[] {
|
|
589
|
+
const i = rest.findIndex((a) => a === "--out" || a.startsWith("--out="));
|
|
590
|
+
if (i === -1) return [];
|
|
591
|
+
return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
|
|
592
|
+
}
|
|
593
|
+
function effectFlagFrom(rest: string[]): string[] {
|
|
594
|
+
const i = rest.findIndex((a) => a === "--effect" || a.startsWith("--effect="));
|
|
595
|
+
if (i === -1) return [];
|
|
596
|
+
return rest[i].includes("=") ? [rest[i]] : [rest[i], rest[i + 1]];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** `failproofai publish` — exported for bin/failproofai.mjs and the tests. */
|
|
600
|
+
export async function runPublishCommand(rest: string[]): Promise<PackCliResult> {
|
|
601
|
+
return publish(rest);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function add(rest: string[]): Promise<PackCliResult> {
|
|
605
|
+
const source = packAddSource(rest);
|
|
606
|
+
const selection = selectionFrom(rest);
|
|
607
|
+
|
|
608
|
+
// Our own pack, by the short name. `core` is a SPELLING of a GitHub source,
|
|
609
|
+
// not a second delivery path: it resolves to CORE_SOURCE and is fetched,
|
|
610
|
+
// verified and pinned exactly like anybody else's.
|
|
611
|
+
//
|
|
612
|
+
// The package used to carry a copy so this worked with no network. It no
|
|
613
|
+
// longer does, on purpose. A pack that ships inside the binary is a policy set
|
|
614
|
+
// we picked for you and wrote to your disk before you asked for it, and it
|
|
615
|
+
// gave our own policies a route no third-party pack could use — which is the
|
|
616
|
+
// opposite of the thing this whole lane exists to make possible.
|
|
617
|
+
const resolvedSource =
|
|
618
|
+
source && CORE_ALIASES.has(source.toLowerCase()) ? CORE_SOURCE : source;
|
|
619
|
+
|
|
620
|
+
if (!resolvedSource) {
|
|
621
|
+
return fail(["Usage: failproofai policies add <source> [--policy a,b] [--category x,y] [--all]"]);
|
|
248
622
|
}
|
|
249
623
|
if (selection.only && selection.only.length === 0) {
|
|
250
624
|
return fail(["--policy needs at least one policy name, comma-separated"]);
|
|
@@ -254,7 +628,7 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
254
628
|
}
|
|
255
629
|
|
|
256
630
|
try {
|
|
257
|
-
const result = await addPack(
|
|
631
|
+
const result = await addPack(resolvedSource!, selection);
|
|
258
632
|
const lines = [
|
|
259
633
|
...(result.replaced?.length
|
|
260
634
|
? [`Replaced ${result.replaced.join(", ")} — same policies under a new name; your selection was kept.`]
|
|
@@ -278,11 +652,17 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
278
652
|
if (skipped.length > 0) {
|
|
279
653
|
lines.push(` not enabled (${skipped.length}): ${summarise(skipped)}`);
|
|
280
654
|
lines.push("");
|
|
281
|
-
lines.push(` see all: failproofai
|
|
655
|
+
lines.push(` see all: failproofai policies`);
|
|
656
|
+
// Naming --policy here as well as the coarser two: it is the flag people
|
|
657
|
+
// reach for first ("just give me that one"), and it was only ever
|
|
658
|
+
// suggested on the branch that installed our own pack from disk — so
|
|
659
|
+
// every third-party pack told you about categories and everything, and
|
|
660
|
+
// never about taking a single policy.
|
|
661
|
+
lines.push(` one policy: failproofai policies add ${source} --policy ${skipped[0]}`);
|
|
282
662
|
if (result.categories.length > 0) {
|
|
283
|
-
lines.push(` by category: failproofai
|
|
663
|
+
lines.push(` by category: failproofai policies add ${source} --category ${result.categories.slice(0, 3).join(",")}`);
|
|
284
664
|
}
|
|
285
|
-
lines.push(` everything: failproofai
|
|
665
|
+
lines.push(` everything: failproofai policies add ${source} --all`);
|
|
286
666
|
}
|
|
287
667
|
return ok(lines);
|
|
288
668
|
} catch (err) {
|
|
@@ -292,10 +672,10 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
292
672
|
|
|
293
673
|
function remove(rest: string[]): PackCliResult {
|
|
294
674
|
const id = rest[0];
|
|
295
|
-
if (!id) return fail(["Usage: failproofai
|
|
675
|
+
if (!id) return fail(["Usage: failproofai policies remove <publisher/name>"]);
|
|
296
676
|
if (!removePack(id)) return fail([`No installed pack with id ${id}`]);
|
|
297
677
|
return ok([
|
|
298
|
-
`Removed ${id}. Its
|
|
678
|
+
`Removed ${id}. Its policies stop enforcing now; re-add it any time with the same command.`,
|
|
299
679
|
]);
|
|
300
680
|
}
|
|
301
681
|
|
|
@@ -351,7 +731,7 @@ async function listRemote(source: string): Promise<PackCliResult> {
|
|
|
351
731
|
),
|
|
352
732
|
preview.resolvedFromLatest ? note(`Newest release: ${preview.source}`, opts) : null,
|
|
353
733
|
table({ head: ["", "", ""], rows }, opts),
|
|
354
|
-
nextStep(`failproofai
|
|
734
|
+
nextStep(`failproofai policies add ${source}`, "Install the defaults with:", opts),
|
|
355
735
|
note("Or take part of it: --policy <a,b>, --category <x,y>, --all", opts),
|
|
356
736
|
),
|
|
357
737
|
);
|
|
@@ -362,7 +742,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
362
742
|
const opts = optsFor(process.stdout);
|
|
363
743
|
const policyCount = packs.reduce((n, pack) => n + pack.policies.length, 0);
|
|
364
744
|
const head = title(
|
|
365
|
-
"failproofai
|
|
745
|
+
"failproofai policies",
|
|
366
746
|
packs.length === 0
|
|
367
747
|
? undefined
|
|
368
748
|
: `${packs.length} pack${packs.length === 1 ? "" : "s"} · ${policyCount} policies`,
|
|
@@ -377,7 +757,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
377
757
|
{
|
|
378
758
|
what: "No packs installed.",
|
|
379
759
|
hint: "Install one with:",
|
|
380
|
-
cmd: "failproofai
|
|
760
|
+
cmd: "failproofai policies add github:owner/repo@tag",
|
|
381
761
|
},
|
|
382
762
|
opts,
|
|
383
763
|
),
|
|
@@ -448,7 +828,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
448
828
|
if (source && taken.length < pack.policies.length && slugs.length > 0) {
|
|
449
829
|
groups.push(
|
|
450
830
|
nextStep(
|
|
451
|
-
`failproofai
|
|
831
|
+
`failproofai policies add ${source} --category ${slugs.slice(0, 3).join(",")}`,
|
|
452
832
|
"Take a whole category with:",
|
|
453
833
|
opts,
|
|
454
834
|
),
|
|
@@ -183,7 +183,7 @@ export function packFailureReason(guards: MissingGuard[]): string {
|
|
|
183
183
|
return (
|
|
184
184
|
`Blocked because a policy pack this machine is configured to enforce is not running — ${named}. ` +
|
|
185
185
|
`Enforcement you were told you had is missing, so this is refused rather than allowed silently. ` +
|
|
186
|
-
`A human needs to run: failproofai
|
|
186
|
+
`A human needs to run: failproofai policies, then failproofai policies add <source> (or failproofai config). ` +
|
|
187
187
|
`The agent cannot run those itself.`
|
|
188
188
|
);
|
|
189
189
|
}
|