failproofai 1.0.2-beta.0 → 1.0.2-beta.2
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.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/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.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.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]__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]__090sxxw._.js +3 -0
- 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]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0wb8osu._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{_12l4a_k._.js → _1oggjel._.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 +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/{3y54nw0i7dsjl.js → 0nnbtp0w4sxrr.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0sa9v5mhc8861.js → 0ykd_go4oahan.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0eqky-7n7dgdm.js → 140ip21rqmelt.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2i2yt3hzjredm.js → 1da5a_a-3btas.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1oml7c7env4-g.js → 1fac8krph-zgp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hs7ycvsav7v2.js → 1nzy5jc_vf9e0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1sl2xmn9inx3y.js → 1yw71gn8vqco1.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1nuqytbta44af.js → 29eh4k77otcw6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2aawsjld9ied_.js → 2axi5ehjbld9h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2gn7ut4svofm2.js → 2fzxl7n717pbc.js} +1 -1
- package/.next/standalone/.next/static/chunks/{35xg6retpe6rp.js → 3jjmdqqzem21x.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-3kjgdgwci4tqf.js → turbopack-2vqpa-dts1gy4.js} +1 -1
- 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 +2374 -1873
- package/dist/worker.mjs +3 -13
- package/package.json +10 -11
- package/src/hooks/configure-wizard.ts +67 -444
- package/src/hooks/fp-reset.ts +10 -19
- package/src/hooks/handler.ts +2 -2
- package/src/hooks/manager.ts +58 -24
- package/src/hooks/pack-cli.ts +488 -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 +150 -48
- 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/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 → A7czO1PZdTcNzLGa_H1sm}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{nwbyWT06Ud0XdXOrVJyvS → A7czO1PZdTcNzLGa_H1sm}/_ssgManifest.js +0 -0
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,453 @@ 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
|
+
}
|
|
257
|
+
|
|
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
|
+
}
|
|
217
274
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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;
|
|
244
532
|
}
|
|
245
533
|
|
|
246
|
-
|
|
247
|
-
|
|
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);
|
|
566
|
+
}
|
|
567
|
+
|
|
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
|
+
/**
|
|
605
|
+
* The pack's own list, defaults pre-ticked, before anything is installed.
|
|
606
|
+
*
|
|
607
|
+
* Reads the MANIFEST only — `fetchPackPreview` never downloads the entry
|
|
608
|
+
* artifact — so the code of a pack you are still deciding about is never
|
|
609
|
+
* fetched, let alone imported.
|
|
610
|
+
*
|
|
611
|
+
* Returns null when the user cancelled, which must not be confused with an
|
|
612
|
+
* empty selection: one means "install nothing", the other means "do nothing".
|
|
613
|
+
*/
|
|
614
|
+
async function pickFromSource(
|
|
615
|
+
source: string,
|
|
616
|
+
io: { stdin: TTYIn; stdout: TTYOut },
|
|
617
|
+
): Promise<string[] | null> {
|
|
618
|
+
const preview = await fetchPackPreview(source);
|
|
619
|
+
const categories = [...new Set(preview.policies.map((p) => p.category))];
|
|
620
|
+
const rows: MultiChoice<string>[] = [];
|
|
621
|
+
for (const category of categories) {
|
|
622
|
+
for (const policy of preview.policies.filter((p) => p.category === category)) {
|
|
623
|
+
rows.push({
|
|
624
|
+
label: policy.name,
|
|
625
|
+
value: policy.name,
|
|
626
|
+
hint: policy.description,
|
|
627
|
+
checked: policy.defaultEnabled,
|
|
628
|
+
section: `${category} · ${slugifyCategory(category)}`,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const on = preview.policies.filter((p) => p.defaultEnabled).length;
|
|
633
|
+
const picked = await multiSelect<string>({
|
|
634
|
+
message: `${preview.id}@${preview.version} — which of these should be on?`,
|
|
635
|
+
choices: rows,
|
|
636
|
+
summaryNoun: "policies",
|
|
637
|
+
hint: `space toggles · ctrl+a all · ↵ confirm · ${on} of ${rows.length} are the publisher's defaults`,
|
|
638
|
+
stdin: io.stdin,
|
|
639
|
+
stdout: io.stdout,
|
|
640
|
+
});
|
|
641
|
+
return picked;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function add(rest: string[]): Promise<PackCliResult> {
|
|
645
|
+
const source = packAddSource(rest);
|
|
646
|
+
const selection = selectionFrom(rest);
|
|
647
|
+
|
|
648
|
+
// Our own pack, by the short name. `core` is a SPELLING of a GitHub source,
|
|
649
|
+
// not a second delivery path: it resolves to CORE_SOURCE and is fetched,
|
|
650
|
+
// verified and pinned exactly like anybody else's.
|
|
651
|
+
//
|
|
652
|
+
// The package used to carry a copy so this worked with no network. It no
|
|
653
|
+
// longer does, on purpose. A pack that ships inside the binary is a policy set
|
|
654
|
+
// we picked for you and wrote to your disk before you asked for it, and it
|
|
655
|
+
// gave our own policies a route no third-party pack could use — which is the
|
|
656
|
+
// opposite of the thing this whole lane exists to make possible.
|
|
657
|
+
const resolvedSource =
|
|
658
|
+
source && CORE_ALIASES.has(source.toLowerCase()) ? CORE_SOURCE : source;
|
|
659
|
+
|
|
660
|
+
if (!resolvedSource) {
|
|
661
|
+
return fail(["Usage: failproofai policies add <source> [--policy a,b] [--category x,y] [--all]"]);
|
|
248
662
|
}
|
|
249
663
|
if (selection.only && selection.only.length === 0) {
|
|
250
664
|
return fail(["--policy needs at least one policy name, comma-separated"]);
|
|
@@ -253,8 +667,33 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
253
667
|
return fail(["--category needs at least one category, comma-separated"]);
|
|
254
668
|
}
|
|
255
669
|
|
|
670
|
+
// CHOOSE, then install. A pack's `defaultEnabled` flags are the publisher's
|
|
671
|
+
// recommendation, and taking them silently turns a recommendation into a
|
|
672
|
+
// decision made on the user's behalf — announced only afterwards, by which
|
|
673
|
+
// point the policies are already on their machine.
|
|
674
|
+
//
|
|
675
|
+
// So a human at a terminal who named no flags gets the list first, with the
|
|
676
|
+
// publisher's defaults pre-ticked. The manifest is read WITHOUT downloading
|
|
677
|
+
// the entry artifact, so deciding about a stranger's pack still never runs a
|
|
678
|
+
// stranger's code.
|
|
679
|
+
//
|
|
680
|
+
// Skipped entirely when a selection was passed (`--policy`, `--category`,
|
|
681
|
+
// `--all`) or when there is no terminal: a script asked a precise question and
|
|
682
|
+
// must get a precise answer, not a prompt it cannot see.
|
|
683
|
+
const io = { stdin: process.stdin as TTYIn, stdout: process.stdout as TTYOut };
|
|
684
|
+
const chose = !selection.only && !selection.categories && !selection.all;
|
|
685
|
+
if (chose && io.stdin.isTTY && io.stdout.isTTY) {
|
|
686
|
+
const picked = await pickFromSource(resolvedSource!, io);
|
|
687
|
+
if (picked === null) return ok(["Nothing installed."]);
|
|
688
|
+
selection.only = picked;
|
|
689
|
+
// An empty pick is a real answer — install the pack, enable none of it —
|
|
690
|
+
// but `{only: []}` is indistinguishable from "no selection" downstream, so
|
|
691
|
+
// it is carried as an explicit empty list the resolver can see.
|
|
692
|
+
if (picked.length === 0) selection.only = [];
|
|
693
|
+
}
|
|
694
|
+
|
|
256
695
|
try {
|
|
257
|
-
const result = await addPack(
|
|
696
|
+
const result = await addPack(resolvedSource!, selection);
|
|
258
697
|
const lines = [
|
|
259
698
|
...(result.replaced?.length
|
|
260
699
|
? [`Replaced ${result.replaced.join(", ")} — same policies under a new name; your selection was kept.`]
|
|
@@ -278,11 +717,17 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
278
717
|
if (skipped.length > 0) {
|
|
279
718
|
lines.push(` not enabled (${skipped.length}): ${summarise(skipped)}`);
|
|
280
719
|
lines.push("");
|
|
281
|
-
lines.push(` see all: failproofai
|
|
720
|
+
lines.push(` see all: failproofai policies`);
|
|
721
|
+
// Naming --policy here as well as the coarser two: it is the flag people
|
|
722
|
+
// reach for first ("just give me that one"), and it was only ever
|
|
723
|
+
// suggested on the branch that installed our own pack from disk — so
|
|
724
|
+
// every third-party pack told you about categories and everything, and
|
|
725
|
+
// never about taking a single policy.
|
|
726
|
+
lines.push(` one policy: failproofai policies add ${source} --policy ${skipped[0]}`);
|
|
282
727
|
if (result.categories.length > 0) {
|
|
283
|
-
lines.push(` by category: failproofai
|
|
728
|
+
lines.push(` by category: failproofai policies add ${source} --category ${result.categories.slice(0, 3).join(",")}`);
|
|
284
729
|
}
|
|
285
|
-
lines.push(` everything: failproofai
|
|
730
|
+
lines.push(` everything: failproofai policies add ${source} --all`);
|
|
286
731
|
}
|
|
287
732
|
return ok(lines);
|
|
288
733
|
} catch (err) {
|
|
@@ -292,10 +737,10 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
292
737
|
|
|
293
738
|
function remove(rest: string[]): PackCliResult {
|
|
294
739
|
const id = rest[0];
|
|
295
|
-
if (!id) return fail(["Usage: failproofai
|
|
740
|
+
if (!id) return fail(["Usage: failproofai policies remove <publisher/name>"]);
|
|
296
741
|
if (!removePack(id)) return fail([`No installed pack with id ${id}`]);
|
|
297
742
|
return ok([
|
|
298
|
-
`Removed ${id}. Its
|
|
743
|
+
`Removed ${id}. Its policies stop enforcing now; re-add it any time with the same command.`,
|
|
299
744
|
]);
|
|
300
745
|
}
|
|
301
746
|
|
|
@@ -351,7 +796,7 @@ async function listRemote(source: string): Promise<PackCliResult> {
|
|
|
351
796
|
),
|
|
352
797
|
preview.resolvedFromLatest ? note(`Newest release: ${preview.source}`, opts) : null,
|
|
353
798
|
table({ head: ["", "", ""], rows }, opts),
|
|
354
|
-
nextStep(`failproofai
|
|
799
|
+
nextStep(`failproofai policies add ${source}`, "Install the defaults with:", opts),
|
|
355
800
|
note("Or take part of it: --policy <a,b>, --category <x,y>, --all", opts),
|
|
356
801
|
),
|
|
357
802
|
);
|
|
@@ -362,7 +807,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
362
807
|
const opts = optsFor(process.stdout);
|
|
363
808
|
const policyCount = packs.reduce((n, pack) => n + pack.policies.length, 0);
|
|
364
809
|
const head = title(
|
|
365
|
-
"failproofai
|
|
810
|
+
"failproofai policies",
|
|
366
811
|
packs.length === 0
|
|
367
812
|
? undefined
|
|
368
813
|
: `${packs.length} pack${packs.length === 1 ? "" : "s"} · ${policyCount} policies`,
|
|
@@ -377,7 +822,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
377
822
|
{
|
|
378
823
|
what: "No packs installed.",
|
|
379
824
|
hint: "Install one with:",
|
|
380
|
-
cmd: "failproofai
|
|
825
|
+
cmd: "failproofai policies add github:owner/repo@tag",
|
|
381
826
|
},
|
|
382
827
|
opts,
|
|
383
828
|
),
|
|
@@ -448,7 +893,7 @@ async function list(): Promise<PackCliResult> {
|
|
|
448
893
|
if (source && taken.length < pack.policies.length && slugs.length > 0) {
|
|
449
894
|
groups.push(
|
|
450
895
|
nextStep(
|
|
451
|
-
`failproofai
|
|
896
|
+
`failproofai policies add ${source} --category ${slugs.slice(0, 3).join(",")}`,
|
|
452
897
|
"Take a whole category with:",
|
|
453
898
|
opts,
|
|
454
899
|
),
|
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reads the installed policy packs a machine has on disk.
|
|
3
3
|
*
|
|
4
4
|
* A pack is the delivery unit for policies that did not ship compiled into this
|
|
5
|
-
* build: `failproofai
|
|
5
|
+
* build: `failproofai policies add` writes the artifact and records it here, and the
|
|
6
6
|
* loader imports it through the same path custom policies already use.
|
|
7
7
|
*
|
|
8
8
|
* ## Why this is not `cloud-managed-policies.ts`
|
|
@@ -24,11 +24,19 @@
|
|
|
24
24
|
* ## Fail-open, and the condition that makes it sound
|
|
25
25
|
*
|
|
26
26
|
* An unreadable manifest, a bad digest or a corrupt artifact yields zero packs
|
|
27
|
-
* and a recorded error —
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* and a recorded error — this READER does not deny.
|
|
28
|
+
*
|
|
29
|
+
* That used to be defensible because the builtin policies shipped compiled in
|
|
30
|
+
* and kept enforcing underneath. They no longer do: the builtins ARE a pack now,
|
|
31
|
+
* so a silent zero here would be zero enforcement at exit 0 on a machine that
|
|
32
|
+
* reports healthy — exactly what the comment that stood here warned about.
|
|
33
|
+
*
|
|
34
|
+
* What makes it sound now is that the denying moved rather than disappeared:
|
|
35
|
+
* `pack-failclosed.ts` reads these same `errors` and refuses the events the
|
|
36
|
+
* missing policies declared. So this stays a pure reader that reports what it
|
|
37
|
+
* found, and the layer above decides what a failure means. Do not add a throw
|
|
38
|
+
* here — per-pack isolation is the reason one bad third-party pack cannot
|
|
39
|
+
* switch off every other pack on the machine.
|
|
32
40
|
*/
|
|
33
41
|
import { createHash } from "node:crypto";
|
|
34
42
|
import { existsSync, readFileSync } from "node:fs";
|