failproofai 1.0.2-beta.6 → 1.0.2-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- 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/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +12 -15
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__128se8m._.js → [root-of-the-server]__1jjjg6g._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tag8_-._.js → [root-of-the-server]__1opxw-7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_11fc-hb._.js → _0_nsohj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0no96tu._.js → _1uvml5y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/{276n-edifojbd.js → 0icodhlwr9x_e.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0p84npzxdgpc5.js → 0tvmd0p_1i_g6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vu_7_eor1m-k.js → 1d6c85605lsgp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2jfskwd2u8o5r.js → 1d7o3wnt3h6se.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0z7au3njlak0j.js → 1vzyn9h2kvj-i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0sdgnflvw8kta.js → 2-9lvhuto1iwo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0h-3zffoy-_ji.js → 3jt6n16p2qpex.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0931avh-06lj5.js → 3swqstog1cd9u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1id7o44jjegf0.js → 3vqddiy9tvlwf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2pxr7nr1nyotn.js → 44oamw33aso19.js} +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +796 -679
- package/dist/cli.mjs +3747 -3845
- package/dist/worker.mjs +1 -1
- package/package.json +9 -9
- package/src/audit/cli.ts +40 -39
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/configure-wizard.ts +24 -10
- package/src/hooks/manager.ts +41 -6
- package/src/hooks/tui.ts +173 -39
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15r6r7w._.js +0 -10
- /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → PKwyBPAHjy3DB8qqapaH4}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → PKwyBPAHjy3DB8qqapaH4}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{mAYN9BOxWVFREnVp3yCHp → PKwyBPAHjy3DB8qqapaH4}/_ssgManifest.js +0 -0
package/bin/failproofai.mjs
CHANGED
|
@@ -296,6 +296,76 @@ if (hookIdx >= 0) {
|
|
|
296
296
|
* CliError → clean message, no stack trace, exit exitCode (1 or 2)
|
|
297
297
|
* Error → unexpected; shows message only, exits 2
|
|
298
298
|
*/
|
|
299
|
+
/**
|
|
300
|
+
* Every `--help` in this file, and the index itself, drawn by ONE renderer.
|
|
301
|
+
*
|
|
302
|
+
* They used to be twelve template literals: `USAGE` on one screen and `Usage:`
|
|
303
|
+
* on the next, a description column hand-counted per screen, no version on any
|
|
304
|
+
* of them, and no colour on any of them while the index they were reached from
|
|
305
|
+
* had all three. The words are still each screen's own — this owns the shape,
|
|
306
|
+
* so a screen cannot drift out of the family without editing the family.
|
|
307
|
+
*
|
|
308
|
+
* Capped at 80 columns by `helpOptsFor`, so help reads the same in a maximised
|
|
309
|
+
* window as in a tmux pane, and narrows on a terminal smaller than that.
|
|
310
|
+
*/
|
|
311
|
+
/**
|
|
312
|
+
* Lines a module ALREADY laid out with the kit — `harness`, `publish`,
|
|
313
|
+
* `policies add`, the pack lane — printed with the outer margins every other
|
|
314
|
+
* screen gets. They used to go out one `console.log` at a time, which is the
|
|
315
|
+
* one thing `printBlock` exists to own: the block arrived flush against the
|
|
316
|
+
* prompt above it while every neighbouring command was breathing.
|
|
317
|
+
*/
|
|
318
|
+
async function printLines(lines, ok = true) {
|
|
319
|
+
const { printBlock } = await import("../src/hooks/tui");
|
|
320
|
+
printBlock(ok ? process.stdout : process.stderr, lines);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* What an action surface prints when it is done: a heading naming the command,
|
|
325
|
+
* then the lines it produced, indented and margined like every other screen.
|
|
326
|
+
*
|
|
327
|
+
* The modules keep returning bare facts — `runFlushCommand` says "2 batches
|
|
328
|
+
* spooled", not how to draw it — so the presentation lives in exactly one
|
|
329
|
+
* place instead of five, and a flush report and a `policies` listing stop
|
|
330
|
+
* looking like output from two different programs.
|
|
331
|
+
*
|
|
332
|
+
* Lines that already carry their own leading space are passed through: those
|
|
333
|
+
* are sub-items a module laid out on purpose, and re-wrapping them would
|
|
334
|
+
* flatten the structure they were expressing.
|
|
335
|
+
*/
|
|
336
|
+
async function printReport(command, lines, opts = {}) {
|
|
337
|
+
const { title, wrap, stack, printBlock, optsFor, INDENT, brandAnsi, ANSI_RESET } =
|
|
338
|
+
await import("../src/hooks/tui");
|
|
339
|
+
const ok = opts.ok !== false;
|
|
340
|
+
const stream = ok ? process.stdout : process.stderr;
|
|
341
|
+
const o = optsFor(stream);
|
|
342
|
+
// `\`like this\`` becomes pink, and loses the backticks. These messages name
|
|
343
|
+
// the command to run next more often than not, and pink is what you type
|
|
344
|
+
// everywhere else on the CLI now — the help screens, the bullets, the next
|
|
345
|
+
// steps. Applied AFTER wrapping, because an escape sequence has no width and
|
|
346
|
+
// colouring first would make every wrap measure the wrong length.
|
|
347
|
+
const paint = (line) =>
|
|
348
|
+
o.color && (line.match(/`/g) || []).length % 2 === 0
|
|
349
|
+
? line.replace(/`([^`]+)`/g, `${brandAnsi("pink")}$1${ANSI_RESET}`)
|
|
350
|
+
: line;
|
|
351
|
+
const body = [];
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (line.trim() === "") body.push("");
|
|
354
|
+
else if (line.startsWith(" ")) body.push(paint(`${INDENT}${line}`));
|
|
355
|
+
else {
|
|
356
|
+
for (const w of wrap(line, Math.max(20, o.cols - INDENT.length * 2))) {
|
|
357
|
+
body.push(paint(`${INDENT}${w}`));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
printBlock(stream, stack(title(`failproofai ${command}`, opts.meta, o), body));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function printHelp(spec) {
|
|
365
|
+
const { helpScreen, helpOptsFor, printBlock } = await import("../src/hooks/tui");
|
|
366
|
+
printBlock(process.stdout, helpScreen({ version, ...spec }, helpOptsFor(process.stdout)));
|
|
367
|
+
}
|
|
368
|
+
|
|
299
369
|
async function runCli() {
|
|
300
370
|
// --help / -h (only when not inside a subcommand that handles its own --help)
|
|
301
371
|
// `update` and `migrate` were missing here, so `failproofai update --help`
|
|
@@ -322,25 +392,37 @@ async function runCli() {
|
|
|
322
392
|
// string — so it gets a topic here rather than a line on the index, where a
|
|
323
393
|
// machine-facing flag would only take space from the human-facing commands.
|
|
324
394
|
if (helpTopic === "hook") {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
395
|
+
await printHelp({
|
|
396
|
+
command: "--hook",
|
|
397
|
+
tagline: "the entry point your agent CLI spawns, once per tool call",
|
|
398
|
+
sections: [
|
|
399
|
+
{
|
|
400
|
+
label: "usage",
|
|
401
|
+
entries: [["failproofai --hook <event> [--cli <name>]"]],
|
|
402
|
+
after: [
|
|
403
|
+
"You do not run this; `failproofai config` writes it into each CLI's",
|
|
404
|
+
"hook configuration for you.",
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
label: "options",
|
|
409
|
+
entries: [
|
|
410
|
+
["--hook <event>", "PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, SessionStart, SessionEnd, PreCompact, Notification, PermissionRequest"],
|
|
411
|
+
["--cli <name>", "claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose. Defaults to claude. It selects which payload shape to expect: each CLI names its events and tool arguments differently, and failproofai canonicalizes them."],
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
label: "how it answers",
|
|
416
|
+
lines: [
|
|
417
|
+
"It reads the event as JSON on stdin and answers on stdout, in whatever",
|
|
418
|
+
"shape that CLI honours. Exit codes and response shapes differ per CLI by",
|
|
419
|
+
"necessity — see docs.befailproof.ai. Denials are reported to the agent,",
|
|
420
|
+
"never to you.",
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
});
|
|
425
|
+
process.exit(0);
|
|
344
426
|
process.exit(0);
|
|
345
427
|
}
|
|
346
428
|
// Canonicalize the topic the same way a typed command is canonicalized, so
|
|
@@ -369,62 +451,53 @@ see docs.befailproof.ai. Denials are reported to the agent, never to you.
|
|
|
369
451
|
if (extraArgs.length > 0) {
|
|
370
452
|
throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai help\` for usage.`);
|
|
371
453
|
}
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// to a file, or on a
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
row("flush --wait", "Send everything spooled now, and block until it lands"),
|
|
420
|
-
row("backfill --since", "Re-send history already read past: 30d, 6m, YYYY-MM-DD"),
|
|
421
|
-
row("policies -i / -u", "Wire failproofai into your agent CLIs, or unwire"),
|
|
422
|
-
row("migrate", "Run pending ~/.failproofai layout migrations"),
|
|
423
|
-
row("uninstall", "Remove hooks and daemon, BEFORE npm rm -g failproofai"),
|
|
424
|
-
"",
|
|
425
|
-
` ${dim("Docs")} ${mint("docs.befailproof.ai")} ${dim("Code")} github.com/failproofai/failproofai`,
|
|
426
|
-
` ${dim("Chat")} ${mint("discord.befailproof.ai")} ${dim("Forum")} reddit.com/r/failproofai`,
|
|
427
|
-
].join("\n"));
|
|
454
|
+
// The index is DATA, not a template literal: one renderer draws it and the
|
|
455
|
+
// eleven `<command> --help` screens, so a row added here cannot end up in a
|
|
456
|
+
// different dialect from the screen it points at. Colour is decoration and
|
|
457
|
+
// never meaning — the column position already says which half is a command,
|
|
458
|
+
// so this reads identically under NO_COLOR, piped to a file, or on a
|
|
459
|
+
// terminal that has never heard of 24-bit.
|
|
460
|
+
await printHelp({
|
|
461
|
+
tagline: "guardrails for the coding agents on this machine",
|
|
462
|
+
sections: [
|
|
463
|
+
{
|
|
464
|
+
label: "get it running",
|
|
465
|
+
entries: [
|
|
466
|
+
["config", "Set this machine up: agents, daemon, cloud"],
|
|
467
|
+
["config --token <key>", "Set up and connect to Cloud, no questions asked"],
|
|
468
|
+
["update", "Finish an npm upgrade: migrate home, match the daemon"],
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
label: "choose what it enforces",
|
|
473
|
+
entries: [
|
|
474
|
+
["policies", "Every policy on this machine, and whether it is on"],
|
|
475
|
+
["policies add", "Turn one on, or install a pack: <owner>/<repo>"],
|
|
476
|
+
["policies remove", "Turn one off, or uninstall a whole pack"],
|
|
477
|
+
["publish", "Ship your own policies as a pack anyone can install"],
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
label: "see what it caught",
|
|
482
|
+
entries: [
|
|
483
|
+
["(no args)", "Open the policy dashboard on localhost:8020"],
|
|
484
|
+
["audit", "Scan your agents' history, then open the audit view"],
|
|
485
|
+
["config --status", "Cloud connection, daemon version, pause state"],
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
// Named, not described. Everything here is real and reachable through
|
|
489
|
+
// `help <command>`; none of it is what anyone types on the first day,
|
|
490
|
+
// and a full row each is what made this screen read as a manual.
|
|
491
|
+
{
|
|
492
|
+
label: "less often",
|
|
493
|
+
lines: ["policies show, harness, flush, backfill, migrate, uninstall, config --pause"],
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
footer: [
|
|
497
|
+
"failproofai <command> [options] failproofai help <command> for detail",
|
|
498
|
+
"docs.befailproof.ai discord.befailproof.ai -h this screen -v version",
|
|
499
|
+
],
|
|
500
|
+
});
|
|
428
501
|
process.exit(0);
|
|
429
502
|
}
|
|
430
503
|
|
|
@@ -574,27 +647,35 @@ see docs.befailproof.ai. Denials are reported to the agent, never to you.
|
|
|
574
647
|
if (args[0] === "flush") {
|
|
575
648
|
const subArgs = args.slice(1);
|
|
576
649
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
650
|
+
await printHelp({
|
|
651
|
+
command: "flush",
|
|
652
|
+
tagline: "deliver what is already spooled, now",
|
|
653
|
+
sections: [
|
|
654
|
+
{ label: "usage", entries: [["failproofai flush [--wait] [--timeout <secs>]"]] },
|
|
655
|
+
{
|
|
656
|
+
label: "why",
|
|
657
|
+
lines: [
|
|
658
|
+
"The collector is unhurried on purpose: a batch is swept once it is older",
|
|
659
|
+
"than two minutes, at most 64 per pass, on a 60-second cadence. That pacing",
|
|
660
|
+
"keeps a backlog from stampeding the server, and it is exactly wrong when",
|
|
661
|
+
"you are standing at a dashboard waiting to see your own events — \"not",
|
|
662
|
+
"delivered yet\" and \"not working\" look identical from there.",
|
|
663
|
+
"",
|
|
664
|
+
"This asks the daemon to make a pass right now, with no minimum age and no",
|
|
665
|
+
"per-pass cap. It re-sends nothing: only batches already spooled and not",
|
|
666
|
+
"yet delivered. For history the collector has already read past, use",
|
|
667
|
+
"`failproofai backfill`.",
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
label: "options",
|
|
672
|
+
entries: [
|
|
673
|
+
["--wait", "Block until the spool drains, or --timeout elapses."],
|
|
674
|
+
["--timeout <secs>", "How long --wait waits. Default: 60."],
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
});
|
|
598
679
|
process.exit(0);
|
|
599
680
|
}
|
|
600
681
|
|
|
@@ -623,10 +704,7 @@ OPTIONS
|
|
|
623
704
|
lastSubcommand = "flush";
|
|
624
705
|
const { runFlushCommand } = await import("../src/hooks/flush-cli");
|
|
625
706
|
const result = await runFlushCommand({ wait: subArgs.includes("--wait"), timeoutSecs });
|
|
626
|
-
|
|
627
|
-
if (result.exitCode === 0) console.log(line);
|
|
628
|
-
else console.error(line);
|
|
629
|
-
}
|
|
707
|
+
await printReport("flush", result.lines, { ok: result.exitCode === 0 });
|
|
630
708
|
await track("cli_flush", {
|
|
631
709
|
ok: result.exitCode === 0,
|
|
632
710
|
waited: subArgs.includes("--wait"),
|
|
@@ -640,29 +718,39 @@ OPTIONS
|
|
|
640
718
|
if (args[0] === "backfill") {
|
|
641
719
|
const subArgs = args.slice(1);
|
|
642
720
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
721
|
+
await printHelp({
|
|
722
|
+
command: "backfill",
|
|
723
|
+
tagline: "re-send history the collector has already read past",
|
|
724
|
+
sections: [
|
|
725
|
+
{ label: "usage", entries: [["failproofai backfill [--since <when>] [--dry-run]"]] },
|
|
726
|
+
{
|
|
727
|
+
label: "why",
|
|
728
|
+
lines: [
|
|
729
|
+
"The collector never re-reads a file it has a cursor for, which is right",
|
|
730
|
+
"until the dashboard's data is cleared, a machine is re-enrolled, or",
|
|
731
|
+
"cursors advanced before there was anywhere to send. Then the history",
|
|
732
|
+
"exists on disk and nowhere else, with no way to ask for it again.",
|
|
733
|
+
"",
|
|
734
|
+
"Re-sending is safe: redaction is deterministic, so a re-sent event hashes",
|
|
735
|
+
"identically to its first send and collapses into the row already there.",
|
|
736
|
+
],
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
label: "options",
|
|
740
|
+
entries: [
|
|
741
|
+
["--since <when>", "How far back: `30d`, `6m`, `YYYY-MM-DD`. Default 30 days."],
|
|
742
|
+
["--dry-run", "Report what would be re-read and change nothing."],
|
|
743
|
+
],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
label: "note",
|
|
747
|
+
lines: [
|
|
748
|
+
"Which streams are sent follows [collector] in ~/.failproofai/config.toml —",
|
|
749
|
+
"a backfill never sends something your config says you do not want.",
|
|
750
|
+
],
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
});
|
|
666
754
|
process.exit(0);
|
|
667
755
|
}
|
|
668
756
|
|
|
@@ -697,10 +785,7 @@ OPTIONS
|
|
|
697
785
|
lastSubcommand = "backfill";
|
|
698
786
|
const { runBackfillCommand } = await import("../src/hooks/backfill-cli");
|
|
699
787
|
const result = runBackfillCommand({ sinceMs, dryRun: subArgs.includes("--dry-run") });
|
|
700
|
-
|
|
701
|
-
if (result.exitCode === 0) console.log(line);
|
|
702
|
-
else console.error(line);
|
|
703
|
-
}
|
|
788
|
+
await printReport("backfill", result.lines, { ok: result.exitCode === 0 });
|
|
704
789
|
await track("cli_backfill", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run"), explicit_since: sinceIdx >= 0 });
|
|
705
790
|
lastSubcommand = null;
|
|
706
791
|
await exitAfterFlush(result.exitCode);
|
|
@@ -713,156 +798,102 @@ OPTIONS
|
|
|
713
798
|
// nothing else — no root, no daemon call — so it works on a machine whose
|
|
714
799
|
// daemon is stopped, which is when someone is most likely to be fixing what
|
|
715
800
|
// it captures.
|
|
716
|
-
// pack
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
console.log(`
|
|
725
|
-
failproofai pack — install policy packs published as GitHub releases
|
|
726
|
-
|
|
727
|
-
Usage:
|
|
728
|
-
failproofai policies add FailproofAI/policies the Failproof AI policies
|
|
729
|
-
failproofai policies add FailproofAI/policies --policy <a,b> just these
|
|
730
|
-
failproofai policies add FailproofAI/policies --category <x,y> whole categories
|
|
731
|
-
failproofai policies add FailproofAI/policies --all everything in it
|
|
732
|
-
failproofai pack list packs installed here
|
|
733
|
-
failproofai pack list <source> what a pack out there contains
|
|
734
|
-
failproofai pack add <source> [--policy <a,b>] [--category <x,y>] [--all]
|
|
735
|
-
failproofai pack remove <publisher/name>
|
|
736
|
-
failproofai pack build <entry.mjs> --id <publisher/name> --version <v>
|
|
737
|
-
|
|
738
|
-
A pack is one entry artifact plus a manifest, verified against the release's
|
|
739
|
-
SHA256SUMS at install time. The digest is recorded, and re-verified before every
|
|
740
|
-
import — so a pack cannot change under this machine after you installed it.
|
|
741
|
-
|
|
742
|
-
<source> is any GitHub repo publishing a pack — ours included:
|
|
743
|
-
acme/support-agent newest release, pinned to its exact tag
|
|
744
|
-
acme/support-agent@v2.1.0 that release
|
|
745
|
-
github:acme/support-agent@v2.1.0 same, explicit
|
|
746
|
-
https://github.com/acme/support-agent/releases/tag/v2.1.0
|
|
747
|
-
|
|
748
|
-
Naming no tag installs the newest release AND pins it, so what gets recorded
|
|
749
|
-
always names one version — running the same command later can install something
|
|
750
|
-
different, and it will tell you the tag it chose.
|
|
751
|
-
|
|
752
|
-
By default you get the pack's OWN defaults — the policies its author marked
|
|
753
|
-
safe to switch on unattended — not everything it contains. Take more, or less:
|
|
754
|
-
|
|
755
|
-
--category x,y Take whole categories (see: failproofai pack list)
|
|
756
|
-
--only a,b Take exactly these policies
|
|
757
|
-
--all Take everything the pack contains
|
|
758
|
-
|
|
759
|
-
Re-adding at a newer version keeps whatever you chose rather than switching the
|
|
760
|
-
rest back on.
|
|
761
|
-
|
|
762
|
-
Examples:
|
|
763
|
-
failproofai pack add FailproofAI/policies
|
|
764
|
-
failproofai policies add FailproofAI/policies --category sanitize,git
|
|
765
|
-
failproofai pack add github:acme/support-agent@v2.1.0 --only block-refunds
|
|
766
|
-
failproofai pack remove acme/support-agent
|
|
767
|
-
|
|
768
|
-
Publishing: pack build writes failproofai-pack.json, failproofai-pack.mjs and
|
|
769
|
-
SHA256SUMS. Attach all three to a GitHub release; anyone then runs
|
|
770
|
-
failproofai pack add <owner>/<repo>
|
|
771
|
-
|
|
772
|
-
Offline: FAILPROOFAI_NO_DOWNLOAD=1 refuses to fetch, while packs already
|
|
773
|
-
installed keep enforcing. FAILPROOFAI_PACK_BASE_URL points the whole thing at a
|
|
774
|
-
mirror instead of github.com.
|
|
775
|
-
`.trimStart());
|
|
776
|
-
process.exit(0);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
lastSubcommand = "pack";
|
|
780
|
-
const { runPackCommand } = await import("../src/hooks/pack-cli");
|
|
781
|
-
const result = await runPackCommand(subArgs);
|
|
782
|
-
for (const line of result.lines) {
|
|
783
|
-
if (result.exitCode === 0) console.log(line);
|
|
784
|
-
else console.error(line);
|
|
785
|
-
}
|
|
786
|
-
await track("cli_pack", {
|
|
787
|
-
ok: result.exitCode === 0,
|
|
788
|
-
// The subcommand only — never the pack id, source or policy names. A pack
|
|
789
|
-
// source is a value the user typed, and a third-party pack name is a
|
|
790
|
-
// publisher-controlled string; we send shape, never value.
|
|
791
|
-
sub: ["list", "add", "remove"].includes(subArgs[0]) ? subArgs[0] : "unknown",
|
|
792
|
-
});
|
|
793
|
-
lastSubcommand = null;
|
|
794
|
-
await exitAfterFlush(result.exitCode);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
801
|
+
// There is no `pack` branch here on purpose. `pack`, `policy` and `p` are
|
|
802
|
+
// rewritten to `policies` at the top of this file, above every dispatch, so
|
|
803
|
+
// `args[0]` can never be "pack" by the time this runs. One lived here anyway
|
|
804
|
+
// for a while, unreachable, carrying a sixty-line help screen that still
|
|
805
|
+
// advertised `pack list`, `pack add` and `pack build` — three spellings this
|
|
806
|
+
// CLI no longer has — in a third heading dialect nothing else used. Nobody
|
|
807
|
+
// could reach it to notice. The pack lane is entered from
|
|
808
|
+
// `policies add|remove|show` below, which is the only door it has.
|
|
797
809
|
|
|
798
810
|
if (args[0] === "harness") {
|
|
799
811
|
const subArgs = args.slice(1);
|
|
800
812
|
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
813
|
+
await printHelp({
|
|
814
|
+
command: "harness",
|
|
815
|
+
tagline: "capture sessions from more than one location per agent CLI",
|
|
816
|
+
sections: [
|
|
817
|
+
{
|
|
818
|
+
label: "usage",
|
|
819
|
+
entries: [
|
|
820
|
+
["failproofai harness list [<harness>]"],
|
|
821
|
+
["failproofai harness add-path <harness> [<label>=]<path>"],
|
|
822
|
+
["failproofai harness remove-path <harness> <path|label>"],
|
|
823
|
+
],
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
label: "why",
|
|
827
|
+
lines: [
|
|
828
|
+
"Each agent CLI is watched wherever its own installer put it —",
|
|
829
|
+
"~/.claude/projects, ~/.hermes/state.db, and so on. That misses every",
|
|
830
|
+
"other arrangement: a second profile, a mounted team share, a container's",
|
|
831
|
+
"home beside the host's, an agent an operator moved. Those hold real",
|
|
832
|
+
"sessions and nothing collects them.",
|
|
833
|
+
],
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
label: "the label",
|
|
837
|
+
lines: [
|
|
838
|
+
"An entry is `<path>` or `<label>=<path>`. The label namespaces agent ids",
|
|
839
|
+
"as <label>-<agentId>, and it matters: two locations holding the same",
|
|
840
|
+
"project derive the SAME id (it comes from the cwd inside the transcript,",
|
|
841
|
+
"identical in both copies), so without a label they merge into one agent",
|
|
842
|
+
"whose sessions interleave from two machines' worth of history. Omit it",
|
|
843
|
+
"and one is derived from the folder name.",
|
|
844
|
+
],
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
label: "harnesses",
|
|
848
|
+
lines: [
|
|
849
|
+
"claude, codex, copilot, openclaw, pi, factory, antigravity, cursor,",
|
|
850
|
+
"goose, opencode, devin, hermes",
|
|
851
|
+
"",
|
|
852
|
+
"`claude` covers subagent transcripts too — they live under the same root.",
|
|
853
|
+
],
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
label: "notes",
|
|
857
|
+
lines: [
|
|
858
|
+
"• Session collection must be on for any of this to be read; extra paths",
|
|
859
|
+
" live under \"collector\" in ~/.failproofai/config.json like the defaults.",
|
|
860
|
+
"• A path overlapping one already captured is REFUSED by the daemon at",
|
|
861
|
+
" startup rather than collected twice under two ids. It says so in the",
|
|
862
|
+
" journal.",
|
|
863
|
+
"• Two entries sharing a LABEL are refused too: they would share one",
|
|
864
|
+
" cursor directory, whose whole map is written at once, so each would",
|
|
865
|
+
" clobber the other's watermark and re-read from zero after a restart.",
|
|
866
|
+
"• Takes effect within seconds — the daemon re-reads config.json on an",
|
|
867
|
+
" interval and cycles its collector. No restart, no sudo.",
|
|
868
|
+
],
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
label: "examples",
|
|
872
|
+
lines: [
|
|
873
|
+
"failproofai harness add-path claude work=/srv/team/.claude/projects",
|
|
874
|
+
"failproofai harness add-path hermes prod=/srv/hermes-prod/state.db",
|
|
875
|
+
"failproofai harness add-path codex /mnt/other-home/.codex/sessions",
|
|
876
|
+
"failproofai harness list",
|
|
877
|
+
"failproofai harness remove-path claude work",
|
|
878
|
+
"",
|
|
879
|
+
"One home per person — label them, or both derive the same folder-name",
|
|
880
|
+
"label and the second is refused:",
|
|
881
|
+
"failproofai harness add-path openclaw user1=/srv/.openclaw-user1",
|
|
882
|
+
"failproofai harness add-path openclaw user2=/srv/.openclaw-user2",
|
|
883
|
+
"",
|
|
884
|
+
"Containers: FAILPROOFAI_<HARNESS>_EXTRA_PATHS replaces the file's entries.",
|
|
885
|
+
"FAILPROOFAI_OPENCLAW_EXTRA_PATHS=\"user1=/srv/.a,user2=/srv/.b\"",
|
|
886
|
+
],
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
});
|
|
856
890
|
process.exit(0);
|
|
857
891
|
}
|
|
858
892
|
|
|
859
893
|
lastSubcommand = "harness";
|
|
860
894
|
const { runHarnessCommand } = await import("../src/hooks/harness-cli");
|
|
861
895
|
const result = runHarnessCommand(subArgs);
|
|
862
|
-
|
|
863
|
-
if (result.exitCode === 0) console.log(line);
|
|
864
|
-
else console.error(line);
|
|
865
|
-
}
|
|
896
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
866
897
|
await track("cli_harness", {
|
|
867
898
|
ok: result.exitCode === 0,
|
|
868
899
|
// The subcommand only — never the harness name, the label or the path.
|
|
@@ -884,30 +915,38 @@ EXAMPLES
|
|
|
884
915
|
if (args[0] === "migrate") {
|
|
885
916
|
const subArgs = args.slice(1);
|
|
886
917
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
918
|
+
await printHelp({
|
|
919
|
+
command: "migrate",
|
|
920
|
+
tagline: "bring ~/.failproofai up to the layout this version speaks",
|
|
921
|
+
sections: [
|
|
922
|
+
{ label: "usage", entries: [["failproofai migrate [--dry-run]"]] },
|
|
923
|
+
{
|
|
924
|
+
label: "why",
|
|
925
|
+
lines: [
|
|
926
|
+
"npm cannot update an installed package on its own, so a machine can sit",
|
|
927
|
+
"on an old version for months and then jump several layouts at once. This",
|
|
928
|
+
"runs the steps for that jump in order, keyed on the LAYOUT recorded in",
|
|
929
|
+
"~/.failproofai/VERSION rather than on the npm version — so skipping",
|
|
930
|
+
"thirty releases with no layout change runs nothing at all.",
|
|
931
|
+
"",
|
|
932
|
+
"It normally happens by itself, on the first command after an upgrade.",
|
|
933
|
+
"This is for running it deliberately, and for seeing what it would do",
|
|
934
|
+
"first.",
|
|
935
|
+
"",
|
|
936
|
+
"Your settings, cloud enrolment, policy selection, your own policy files,",
|
|
937
|
+
"decision history and undelivered events are carried across, not removed.",
|
|
938
|
+
"The irreplaceable files are copied to migrations/backup-layout<n>/ before",
|
|
939
|
+
"anything runs, and every step is recorded in migrations/applied.json.",
|
|
940
|
+
],
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
label: "options",
|
|
944
|
+
entries: [
|
|
945
|
+
["--dry-run", "Print the steps and the files it would save. Change nothing."],
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
});
|
|
911
950
|
process.exit(0);
|
|
912
951
|
}
|
|
913
952
|
|
|
@@ -929,11 +968,12 @@ OPTIONS
|
|
|
929
968
|
// data is fine and an upgrade would read it, so migrating "forward" from it
|
|
930
969
|
// is not a thing that exists.
|
|
931
970
|
if (state.kind === "future") {
|
|
932
|
-
|
|
933
|
-
`This machine's failproofai directory was written by a newer version
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
971
|
+
await printReport("migrate", [
|
|
972
|
+
`This machine's failproofai directory was written by a newer version ` +
|
|
973
|
+
`(layout ${state.found}; this build speaks ${LAYOUT_VERSION}). Upgrade rather than migrate:`,
|
|
974
|
+
"",
|
|
975
|
+
" npm install -g failproofai@latest",
|
|
976
|
+
], { ok: false, meta: `layout ${state.found}` });
|
|
937
977
|
await track("cli_migrate", { ok: false, reason: "future_layout" });
|
|
938
978
|
lastSubcommand = null;
|
|
939
979
|
await exitAfterFlush(1);
|
|
@@ -942,7 +982,7 @@ OPTIONS
|
|
|
942
982
|
|
|
943
983
|
const from = state.kind === "stale" ? state.found : LAYOUT_VERSION;
|
|
944
984
|
if (subArgs.includes("--dry-run")) {
|
|
945
|
-
|
|
985
|
+
await printReport("migrate", describePlan(from), { meta: "dry run" });
|
|
946
986
|
await track("cli_migrate", { ok: true, dry_run: true, from });
|
|
947
987
|
lastSubcommand = null;
|
|
948
988
|
await exitAfterFlush(0);
|
|
@@ -950,7 +990,7 @@ OPTIONS
|
|
|
950
990
|
}
|
|
951
991
|
|
|
952
992
|
if (state.kind !== "stale") {
|
|
953
|
-
|
|
993
|
+
await printReport("migrate", [`Already at layout ${LAYOUT_VERSION}. Nothing to migrate.`]);
|
|
954
994
|
await track("cli_migrate", { ok: true, from, steps: 0 });
|
|
955
995
|
lastSubcommand = null;
|
|
956
996
|
await exitAfterFlush(0);
|
|
@@ -958,16 +998,21 @@ OPTIONS
|
|
|
958
998
|
}
|
|
959
999
|
|
|
960
1000
|
const run = runMigrations(from);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (run.backedUp.length > 0) {
|
|
965
|
-
console.log(`Saved first: ${run.backedUp.join(", ")}`);
|
|
966
|
-
}
|
|
1001
|
+
const report = run.steps.map(
|
|
1002
|
+
(step) => ` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`,
|
|
1003
|
+
);
|
|
1004
|
+
if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
|
|
967
1005
|
if (run.failed) {
|
|
968
|
-
|
|
969
|
-
|
|
1006
|
+
report.push(
|
|
1007
|
+
"",
|
|
1008
|
+
`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
1009
|
+
`The home is still marked layout ${from} and will be retried.`,
|
|
1010
|
+
);
|
|
970
1011
|
}
|
|
1012
|
+
await printReport("migrate", report, {
|
|
1013
|
+
ok: !run.failed,
|
|
1014
|
+
meta: `layout ${from} → ${LAYOUT_VERSION}`,
|
|
1015
|
+
});
|
|
971
1016
|
await track("cli_migrate", {
|
|
972
1017
|
ok: !run.failed,
|
|
973
1018
|
from,
|
|
@@ -982,26 +1027,35 @@ OPTIONS
|
|
|
982
1027
|
if (args[0] === "update") {
|
|
983
1028
|
const subArgs = args.slice(1);
|
|
984
1029
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1030
|
+
await printHelp({
|
|
1031
|
+
command: "update",
|
|
1032
|
+
tagline: "finish an upgrade: migrate the home, match the daemon",
|
|
1033
|
+
sections: [
|
|
1034
|
+
{
|
|
1035
|
+
label: "usage",
|
|
1036
|
+
entries: [["npm install -g failproofai@latest && failproofai update [--no-daemon]"]],
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
label: "why",
|
|
1040
|
+
lines: [
|
|
1041
|
+
"npm replaces the CLI and nothing else. The daemon binary lives at",
|
|
1042
|
+
"~/.failproofai/bin/failproofaid-<version> and stays exactly where it was,",
|
|
1043
|
+
"so after an npm upgrade the two halves are different versions — and",
|
|
1044
|
+
"failproofaid refuses to start against a layout it does not speak, which",
|
|
1045
|
+
"is the loud version of that problem rather than the silent one.",
|
|
1046
|
+
"",
|
|
1047
|
+
"This does the rest of the upgrade: runs any pending layout migrations,",
|
|
1048
|
+
"puts the matching daemon binary in place, and restarts the service.",
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
label: "options",
|
|
1053
|
+
entries: [
|
|
1054
|
+
["--no-daemon", "Migrate the home only. Leaves a version-skewed daemon in place, so prefer letting it run."],
|
|
1055
|
+
],
|
|
1056
|
+
},
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1005
1059
|
process.exit(0);
|
|
1006
1060
|
}
|
|
1007
1061
|
|
|
@@ -1025,17 +1079,22 @@ OPTIONS
|
|
|
1025
1079
|
const state = detectLayout();
|
|
1026
1080
|
|
|
1027
1081
|
if (state.kind === "future") {
|
|
1028
|
-
|
|
1029
|
-
`This machine's failproofai directory was written by a NEWER version
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1082
|
+
await printReport("update", [
|
|
1083
|
+
`This machine's failproofai directory was written by a NEWER version ` +
|
|
1084
|
+
`(layout ${state.found}; this build speaks ${LAYOUT_VERSION}). This CLI is the stale half:`,
|
|
1085
|
+
"",
|
|
1086
|
+
" npm install -g failproofai@latest",
|
|
1087
|
+
], { ok: false, meta: `layout ${state.found}` });
|
|
1033
1088
|
await track("cli_update", { ok: false, reason: "future_layout" });
|
|
1034
1089
|
lastSubcommand = null;
|
|
1035
1090
|
await exitAfterFlush(1);
|
|
1036
1091
|
return;
|
|
1037
1092
|
}
|
|
1038
1093
|
|
|
1094
|
+
// Collected rather than printed as it goes, so `update` closes with ONE
|
|
1095
|
+
// report — heading, then both halves of the upgrade under it — instead of
|
|
1096
|
+
// a run of bare sentences at column zero.
|
|
1097
|
+
const report = [];
|
|
1039
1098
|
let migrationsRan = 0;
|
|
1040
1099
|
let migrationFailed = false;
|
|
1041
1100
|
if (state.kind === "stale") {
|
|
@@ -1043,17 +1102,18 @@ OPTIONS
|
|
|
1043
1102
|
migrationsRan = run.steps.length;
|
|
1044
1103
|
migrationFailed = Boolean(run.failed);
|
|
1045
1104
|
for (const step of run.steps) {
|
|
1046
|
-
|
|
1105
|
+
report.push(` ${step.ok ? "migrated" : "FAILED "} layout ${step.from} → ${step.to}`);
|
|
1047
1106
|
}
|
|
1048
|
-
if (run.backedUp.length > 0)
|
|
1107
|
+
if (run.backedUp.length > 0) report.push("", `Saved first: ${run.backedUp.join(", ")}`);
|
|
1049
1108
|
if (run.failed) {
|
|
1050
|
-
|
|
1109
|
+
report.push(
|
|
1110
|
+
"",
|
|
1051
1111
|
`Step ${run.failed.from} → ${run.failed.to} did not finish: ${run.failed.error}`,
|
|
1112
|
+
`The home is still marked layout ${state.found} and will be retried.`,
|
|
1052
1113
|
);
|
|
1053
|
-
console.error(`The home is still marked layout ${state.found} and will be retried.`);
|
|
1054
1114
|
}
|
|
1055
1115
|
} else {
|
|
1056
|
-
|
|
1116
|
+
report.push(`Home is at layout ${LAYOUT_VERSION}; no migration was needed.`);
|
|
1057
1117
|
}
|
|
1058
1118
|
|
|
1059
1119
|
// The daemon is refreshed even when a step failed, and deliberately: the
|
|
@@ -1062,20 +1122,24 @@ OPTIONS
|
|
|
1062
1122
|
// the half that can be fixed. The exit code still reports the failure.
|
|
1063
1123
|
let daemonOk = true;
|
|
1064
1124
|
if (subArgs.includes("--no-daemon")) {
|
|
1065
|
-
|
|
1125
|
+
report.push("", "Skipped the daemon (--no-daemon). Its version may not match this CLI.");
|
|
1066
1126
|
} else {
|
|
1067
1127
|
const svc = await import("../src/hooks/daemon-service");
|
|
1068
1128
|
if (!svc.isDaemonSupportedPlatform()) {
|
|
1069
|
-
|
|
1129
|
+
report.push("", `failproofaid does not run on ${process.platform}; nothing to update.`);
|
|
1070
1130
|
} else {
|
|
1071
1131
|
// `refreshDaemonToCliVersion` answers "is there a service at all" itself,
|
|
1072
1132
|
// so there is no separate installed check to get out of step with it.
|
|
1073
1133
|
const result = await svc.refreshDaemonToCliVersion();
|
|
1074
|
-
|
|
1134
|
+
report.push("", ...result.lines);
|
|
1075
1135
|
daemonOk = result.ok;
|
|
1076
1136
|
}
|
|
1077
1137
|
}
|
|
1078
1138
|
|
|
1139
|
+
await printReport("update", report, {
|
|
1140
|
+
ok: daemonOk && !migrationFailed,
|
|
1141
|
+
meta: `v${version}`,
|
|
1142
|
+
});
|
|
1079
1143
|
await track("cli_update", {
|
|
1080
1144
|
ok: daemonOk && !migrationFailed,
|
|
1081
1145
|
migrations: migrationsRan,
|
|
@@ -1089,32 +1153,41 @@ OPTIONS
|
|
|
1089
1153
|
if (args[0] === "uninstall") {
|
|
1090
1154
|
const subArgs = args.slice(1);
|
|
1091
1155
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1156
|
+
await printHelp({
|
|
1157
|
+
command: "uninstall",
|
|
1158
|
+
tagline: "remove failproofai from this machine",
|
|
1159
|
+
sections: [
|
|
1160
|
+
{ label: "usage", entries: [["failproofai uninstall [--purge] [--dry-run] [--yes]"]] },
|
|
1161
|
+
{
|
|
1162
|
+
label: "what it removes",
|
|
1163
|
+
lines: [
|
|
1164
|
+
"• failproofai hook entries from every agent CLI that has them",
|
|
1165
|
+
"• the failproofaid daemon service — ASKED on a plain uninstall (kept",
|
|
1166
|
+
" unless you say yes); always removed with --purge, which prompts for",
|
|
1167
|
+
" your password rather than printing commands to paste",
|
|
1168
|
+
"• the \"require the daemon\" flag — cleared FIRST, so a partial uninstall",
|
|
1169
|
+
" can never leave this machine denying every tool call",
|
|
1170
|
+
],
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
label: "options",
|
|
1174
|
+
entries: [
|
|
1175
|
+
["--purge", "Also delete ~/.failproofai — settings, credentials, audit history and the downloaded daemon binary. Off by default so a reinstall keeps your history."],
|
|
1176
|
+
["--dry-run", "Print what would be removed and change nothing."],
|
|
1177
|
+
["--yes, -y", "Skip the confirmation prompt. Required when there is no TTY."],
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
label: "why this exists",
|
|
1182
|
+
lines: [
|
|
1183
|
+
"npm runs no uninstall script, so `npm rm -g failproofai` removes the",
|
|
1184
|
+
"package and leaves the hook entries and the service behind. Run this",
|
|
1185
|
+
"first, then:",
|
|
1186
|
+
" npm rm -g failproofai",
|
|
1187
|
+
],
|
|
1188
|
+
},
|
|
1189
|
+
],
|
|
1190
|
+
});
|
|
1118
1191
|
process.exit(0);
|
|
1119
1192
|
}
|
|
1120
1193
|
|
|
@@ -1191,10 +1264,10 @@ WHY THIS EXISTS
|
|
|
1191
1264
|
// show the same block twice. `planLines` is reported by the command rather
|
|
1192
1265
|
// than guessed from the text — see UninstallResult.
|
|
1193
1266
|
const skip = planWasShown ? result.planLines : 0;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1267
|
+
await printReport("uninstall", result.lines.slice(skip), {
|
|
1268
|
+
ok: result.exitCode === 0,
|
|
1269
|
+
meta: subArgs.includes("--purge") ? "purge" : subArgs.includes("--dry-run") ? "dry run" : undefined,
|
|
1270
|
+
});
|
|
1198
1271
|
// NOT after a purge. `track` resolves the instance id, and `getInstanceId()`
|
|
1199
1272
|
// lazily WRITES ~/.failproofai/state/telemetry-id — which re-created the
|
|
1200
1273
|
// whole directory seconds after the purge deleted it, leaving a machine the
|
|
@@ -1226,118 +1299,139 @@ WHY THIS EXISTS
|
|
|
1226
1299
|
// worked out from the directory and the git remote. Printing help there
|
|
1227
1300
|
// made the one documented command the only one that did nothing.
|
|
1228
1301
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1302
|
+
await printHelp({
|
|
1303
|
+
command: "publish",
|
|
1304
|
+
tagline: "ship your policies as a pack anyone can install",
|
|
1305
|
+
sections: [
|
|
1306
|
+
{
|
|
1307
|
+
label: "two commands, from nothing",
|
|
1308
|
+
entries: [
|
|
1309
|
+
["failproofai publish --init", "write a policy to start from"],
|
|
1310
|
+
["failproofai publish", "ship it"],
|
|
1311
|
+
],
|
|
1312
|
+
after: [
|
|
1313
|
+
"Write your policies in a git repo, run publish, answer one question,",
|
|
1314
|
+
"done. It asks WHERE only when nothing tells it — no remote to read —",
|
|
1315
|
+
"and works the rest out: which files hold policies, what version is",
|
|
1316
|
+
"next, who you are. Every flag below overrides something it would",
|
|
1317
|
+
"otherwise decide. None is required, and on a pipe or in CI the flags",
|
|
1318
|
+
"are all there is: nothing prompts where nobody can answer.",
|
|
1319
|
+
],
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
label: "what --init does",
|
|
1323
|
+
lines: [
|
|
1324
|
+
"Asks what the pack is called, writes <name>.mjs, and stops. No network,",
|
|
1325
|
+
"no git, nothing published. The file is not a template with blanks — it",
|
|
1326
|
+
"is one policy that already blocks git push --force, so the first thing",
|
|
1327
|
+
"you do is edit something that works. It refuses rather than overwriting",
|
|
1328
|
+
"a file that exists.",
|
|
1329
|
+
"",
|
|
1330
|
+
"Then try it on THIS machine, before anyone else can see it:",
|
|
1331
|
+
"",
|
|
1332
|
+
" failproofai policies -i -c ./<name>.mjs",
|
|
1333
|
+
"",
|
|
1334
|
+
"That enforces the file right now — any path, any filename. Ask your",
|
|
1335
|
+
"agent to do the thing you blocked and watch it get refused. Nothing is",
|
|
1336
|
+
"published and nobody else is affected.",
|
|
1337
|
+
],
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
label: "what publish does",
|
|
1341
|
+
lines: [
|
|
1342
|
+
"In order, stopping before it touches GitHub if anything is wrong:",
|
|
1343
|
+
"",
|
|
1344
|
+
"1 Finds the policy file here by CONTENT — one that imports failproofai",
|
|
1345
|
+
" and calls customPolicies.add — not by filename, so it finds",
|
|
1346
|
+
" guards.mjs and ignores an unrelated policies.mjs. Not recursive:",
|
|
1347
|
+
" publishing a fixture is worse than being asked. Two candidates and",
|
|
1348
|
+
" it lists them.",
|
|
1349
|
+
"2 Reads the repo from git remote get-url origin, in the FILE's",
|
|
1350
|
+
" directory rather than yours, and decides the version (see below).",
|
|
1351
|
+
"3 Finds your credential: GITHUB_TOKEN, GH_TOKEN, or gh auth login.",
|
|
1352
|
+
" Needs release-write and nothing else. Never printed.",
|
|
1353
|
+
"4 Creates the repository if it does not exist — public, see below.",
|
|
1354
|
+
"5 Builds the three assets, validating with the LOADER's own rules: the",
|
|
1355
|
+
" code that decides what may install on a stranger's machine. A pack",
|
|
1356
|
+
" that could never install fails here, where you can fix it.",
|
|
1357
|
+
"6 Creates or reuses the release and uploads, replacing assets of the",
|
|
1358
|
+
" same name — the install URL is built from fixed names, so a stale",
|
|
1359
|
+
" copy is what somebody would fetch.",
|
|
1360
|
+
],
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
label: "how the version is decided",
|
|
1364
|
+
lines: [
|
|
1365
|
+
"--version if you pass one. Otherwise a tag on HEAD — someone who tagged",
|
|
1366
|
+
"v1.2.0 has SAID what this release is. Otherwise one past the highest",
|
|
1367
|
+
"the repository has published: 1.0.0, then 1.0.1, and so on.",
|
|
1368
|
+
"",
|
|
1369
|
+
"Counted from the repository's own releases, never anything local, so a",
|
|
1370
|
+
"fresh clone still gets the right number and two people cannot both mint",
|
|
1371
|
+
"1.0.1. Non-semver releases (nightly) are ignored rather than guessed at.",
|
|
1372
|
+
"",
|
|
1373
|
+
"A tag names a COMMIT, so it is refused when the file has uncommitted",
|
|
1374
|
+
"changes: those bytes are not in that commit, and two packs would claim",
|
|
1375
|
+
"one version. A counted version names no commit and has no such problem.",
|
|
1376
|
+
],
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
label: "the repo must be public",
|
|
1380
|
+
lines: [
|
|
1381
|
+
"Installs are anonymous HTTPS with no credential to offer, so a private",
|
|
1382
|
+
"repository publishes to nobody. A repo created here is public for that",
|
|
1383
|
+
"reason; an existing private one still publishes, and warns.",
|
|
1384
|
+
"",
|
|
1385
|
+
"Only the release matters. Installs read releases/download/<tag>/<asset>",
|
|
1386
|
+
"and never touch your git tree — pushing the source is for humans.",
|
|
1387
|
+
],
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
label: "your policy files",
|
|
1391
|
+
lines: [
|
|
1392
|
+
"Write as many as you like — one per category reads well. Every file",
|
|
1393
|
+
"here that registers policies is bundled into the single artifact a pack",
|
|
1394
|
+
"has to be: only the entry is digest-pinned, and a pack importing",
|
|
1395
|
+
"siblings could not honestly claim to be verified. Bundling needs bun;",
|
|
1396
|
+
"without it, name one self-contained file.",
|
|
1397
|
+
"",
|
|
1398
|
+
"Each policy may carry category and defaultEnabled alongside the usual",
|
|
1399
|
+
"fields. category is what --category selects on; defaultEnabled is what",
|
|
1400
|
+
"a bare `policies add` switches on, and it defaults to false.",
|
|
1401
|
+
],
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
label: "options",
|
|
1405
|
+
entries: [
|
|
1406
|
+
["--init [file]", "Write a starter policy and stop."],
|
|
1407
|
+
["--repo <owner>/<repo>", "Where to release it, created if missing."],
|
|
1408
|
+
["--version <version>", "Skip the counting and say which version this is."],
|
|
1409
|
+
["--id <publisher/name>", "The pack's id. Defaults to --repo."],
|
|
1410
|
+
["--tag <tag>", "Release tag. Defaults to the version; v prefix ok."],
|
|
1411
|
+
["--notes <text>", "Release notes."],
|
|
1412
|
+
["--out <dir>", "Where to write the assets. Default: dist-pack."],
|
|
1413
|
+
["--effect <effect>", "enforce or observe. observe records and blocks nothing. Default: enforce."],
|
|
1414
|
+
["--dry-run", "Build the assets, publish nothing. No credential."],
|
|
1415
|
+
],
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
label: "examples",
|
|
1419
|
+
lines: [
|
|
1420
|
+
"failproofai publish --init",
|
|
1421
|
+
"failproofai publish",
|
|
1422
|
+
"failproofai publish --dry-run",
|
|
1423
|
+
"failproofai publish ./guards.mjs --repo me/guards --version 2.0.0",
|
|
1424
|
+
],
|
|
1425
|
+
},
|
|
1426
|
+
],
|
|
1427
|
+
});
|
|
1331
1428
|
process.exit(0);
|
|
1332
1429
|
}
|
|
1333
1430
|
|
|
1334
1431
|
lastSubcommand = "publish";
|
|
1335
1432
|
const { runPublishCommand } = await import("../src/hooks/pack-cli");
|
|
1336
1433
|
const result = await runPublishCommand(subArgs);
|
|
1337
|
-
|
|
1338
|
-
if (result.exitCode === 0) console.log(line);
|
|
1339
|
-
else console.error(line);
|
|
1340
|
-
}
|
|
1434
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
1341
1435
|
// Shape, never value: whether it published and whether it worked. Never the
|
|
1342
1436
|
// repo, the pack id or the entry path — all three are things the user typed.
|
|
1343
1437
|
await track("cli_publish", { ok: result.exitCode === 0, dry_run: subArgs.includes("--dry-run") });
|
|
@@ -1364,55 +1458,78 @@ EXAMPLES
|
|
|
1364
1458
|
const subArgs = args.slice(1);
|
|
1365
1459
|
|
|
1366
1460
|
if (subArgs.length === 0 || subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1461
|
+
await printHelp({
|
|
1462
|
+
command: "policies add|remove|show",
|
|
1463
|
+
tagline: "choose what your agents may do",
|
|
1464
|
+
sections: [
|
|
1465
|
+
{
|
|
1466
|
+
label: "usage",
|
|
1467
|
+
// Without the `failproofai policies` prefix, which the heading two
|
|
1468
|
+
// lines up already carries: repeating it costs 21 of the 80 columns
|
|
1469
|
+
// on every row and pushes each description into a second line. The
|
|
1470
|
+
// examples below are the copy-pasteable spelling.
|
|
1471
|
+
entries: [
|
|
1472
|
+
["add", "Pick from what is installed here"],
|
|
1473
|
+
["add <name>", "Turn one policy on"],
|
|
1474
|
+
["add <owner>/<repo>", "Install someone's pack"],
|
|
1475
|
+
["remove <name>", "Turn one policy off"],
|
|
1476
|
+
["remove <pack-id>", "Uninstall a pack"],
|
|
1477
|
+
["show <owner>/<repo>", "What a pack contains, before you take it"],
|
|
1478
|
+
],
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
label: "a name or a source",
|
|
1482
|
+
lines: [
|
|
1483
|
+
"Anything with a slash is a pack source; anything without is a policy",
|
|
1484
|
+
"name. Policy names cannot contain a slash, so there is nothing to guess.",
|
|
1485
|
+
"",
|
|
1486
|
+
" block-sudo a policy",
|
|
1487
|
+
" FailproofAI/policies our pack, like any other",
|
|
1488
|
+
" acme/deploy-guard newest release, pinned",
|
|
1489
|
+
" acme/deploy-guard@v2.1.0 that release",
|
|
1490
|
+
" github:acme/deploy-guard@v2.1.0 same, explicit",
|
|
1491
|
+
" https://github.com/acme/x/releases/tag/v2 the URL you copied",
|
|
1492
|
+
],
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
label: "choosing part of a pack",
|
|
1496
|
+
entries: [
|
|
1497
|
+
["--policy a,b", "exactly these"],
|
|
1498
|
+
["--category x,y", "whole categories (failproofai policies show <source>)"],
|
|
1499
|
+
["--all", "everything it contains"],
|
|
1500
|
+
],
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
label: "options (policy names only)",
|
|
1504
|
+
entries: [
|
|
1505
|
+
["--cli <agent>...", "Agent CLI(s) to apply to; space-separated or repeated. Omit to detect installed CLIs and prompt."],
|
|
1506
|
+
["--scope user|project|local", "Config scope. Default: user."],
|
|
1507
|
+
["--beta", "Allow beta policies"],
|
|
1508
|
+
],
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
label: "examples",
|
|
1512
|
+
lines: [
|
|
1513
|
+
"failproofai policies add",
|
|
1514
|
+
"failproofai policies add block-sudo",
|
|
1515
|
+
"failproofai policies add sanitize-api-keys --scope project",
|
|
1516
|
+
"failproofai policies add FailproofAI/policies --category sanitize,git",
|
|
1517
|
+
"failproofai policies add acme/deploy-guard --policy block-prod-deploy",
|
|
1518
|
+
"failproofai policies show acme/deploy-guard",
|
|
1519
|
+
"failproofai policies remove block-sudo",
|
|
1520
|
+
],
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
footer: [
|
|
1524
|
+
"With no flags you get the pack's own defaults and are shown the rest.",
|
|
1525
|
+
"Re-adding at a newer version keeps what you chose.",
|
|
1526
|
+
"Agents: claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
|
|
1527
|
+
"factory, devin, antigravity, goose.",
|
|
1528
|
+
"Publishing your own: failproofai publish --help",
|
|
1529
|
+
"Offline: FAILPROOFAI_NO_DOWNLOAD=1 refuses to fetch; packs already",
|
|
1530
|
+
"installed keep enforcing. FAILPROOFAI_PACK_BASE_URL points it at a mirror.",
|
|
1531
|
+
],
|
|
1532
|
+
});
|
|
1416
1533
|
process.exit(0);
|
|
1417
1534
|
}
|
|
1418
1535
|
|
|
@@ -1475,10 +1592,7 @@ keep enforcing. FAILPROOFAI_PACK_BASE_URL points it all at a mirror.
|
|
|
1475
1592
|
action === "show" ? ["list", ...rest] : [action, ...rest];
|
|
1476
1593
|
const { runPackCommand } = await import("../src/hooks/pack-cli");
|
|
1477
1594
|
const result = await runPackCommand(packArgs);
|
|
1478
|
-
|
|
1479
|
-
if (result.exitCode === 0) console.log(line);
|
|
1480
|
-
else console.error(line);
|
|
1481
|
-
}
|
|
1595
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
1482
1596
|
await track("cli_pack", {
|
|
1483
1597
|
ok: result.exitCode === 0,
|
|
1484
1598
|
// The subcommand only — never the pack id, source or policy names. A
|
|
@@ -1553,10 +1667,7 @@ keep enforcing. FAILPROOFAI_PACK_BASE_URL points it all at a mirror.
|
|
|
1553
1667
|
// the answer is to SHOW the list, here, with what is already on ticked.
|
|
1554
1668
|
const { runPolicyPicker } = await import("../src/hooks/pack-cli");
|
|
1555
1669
|
const result = await runPolicyPicker(action, { stdin: process.stdin, stdout: process.stdout });
|
|
1556
|
-
|
|
1557
|
-
if (result.exitCode === 0) console.log(line);
|
|
1558
|
-
else console.error(line);
|
|
1559
|
-
}
|
|
1670
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
1560
1671
|
await track("cli_policy_picker", { ok: result.exitCode === 0, action });
|
|
1561
1672
|
lastSubcommand = null;
|
|
1562
1673
|
await exitAfterFlush(result.exitCode);
|
|
@@ -1630,69 +1741,63 @@ keep enforcing. FAILPROOFAI_PACK_BASE_URL points it all at a mirror.
|
|
|
1630
1741
|
const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
|
|
1631
1742
|
|
|
1632
1743
|
if (isHelp) {
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
failproofai policies
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
failproofai policies --uninstall --custom
|
|
1691
|
-
failproofai policies add FailproofAI/policies
|
|
1692
|
-
failproofai policies add FailproofAI/policies --category git,database
|
|
1693
|
-
failproofai pack list acme/support-agent
|
|
1694
|
-
failproofai pack build ./my-policies.mjs --id acme/support --version 1.0.0
|
|
1695
|
-
`.trimStart());
|
|
1744
|
+
await printHelp({
|
|
1745
|
+
command: "policies",
|
|
1746
|
+
tagline: "manage the policies your agents run under",
|
|
1747
|
+
sections: [
|
|
1748
|
+
{
|
|
1749
|
+
label: "usage",
|
|
1750
|
+
entries: [
|
|
1751
|
+
["(bare)", "List every policy here and whether it is on"],
|
|
1752
|
+
["add [what]", "Turn one on, take a pack, or pick from a list"],
|
|
1753
|
+
["remove <what>", "Turn one off, or uninstall a pack"],
|
|
1754
|
+
["show <owner>/<repo>", "What a pack holds, before you take it"],
|
|
1755
|
+
["--install, -i", "Wire policies into your agent CLIs"],
|
|
1756
|
+
["--uninstall, -u", "Unwire them, or strip the hooks"],
|
|
1757
|
+
],
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
label: "options",
|
|
1761
|
+
entries: [
|
|
1762
|
+
["[names...]", "Policy names. Omit for the interactive picker."],
|
|
1763
|
+
["--cli <agent>...", "Agent CLI(s); space-separated or repeated. Omit to detect what is installed and prompt."],
|
|
1764
|
+
["--scope <scope>", "user, project or local. Default: user. --uninstall also takes all."],
|
|
1765
|
+
["--beta", "Include beta policies. On --uninstall, only those."],
|
|
1766
|
+
["--custom, -c <path>", "Custom policy file; repeat for several. Bare on --uninstall, clears every explicit path."],
|
|
1767
|
+
],
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
// Enumerated ONCE. It used to be spelled out in full four times on
|
|
1771
|
+
// this screen — twice in the --cli lines, twice more as examples
|
|
1772
|
+
// differing only in which agent they named.
|
|
1773
|
+
label: "agents",
|
|
1774
|
+
lines: [
|
|
1775
|
+
"claude, codex, copilot, cursor, opencode, pi, hermes, openclaw,",
|
|
1776
|
+
"factory, devin, antigravity, goose",
|
|
1777
|
+
"",
|
|
1778
|
+
"Codex, Copilot, Cursor, OpenCode and Pi take user or project scope only.",
|
|
1779
|
+
],
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
label: "examples",
|
|
1783
|
+
lines: [
|
|
1784
|
+
"failproofai policies",
|
|
1785
|
+
"failproofai policies --install",
|
|
1786
|
+
"failproofai policies --install block-sudo sanitize-api-keys",
|
|
1787
|
+
"failproofai policies --install --cli codex --scope project",
|
|
1788
|
+
"failproofai policies --install --cli claude codex copilot cursor",
|
|
1789
|
+
"failproofai policies -i -c ./my-policies.js",
|
|
1790
|
+
"failproofai policies --uninstall block-sudo",
|
|
1791
|
+
"failproofai policies -u",
|
|
1792
|
+
"failproofai policies add FailproofAI/policies --category git,database",
|
|
1793
|
+
],
|
|
1794
|
+
},
|
|
1795
|
+
],
|
|
1796
|
+
footer: [
|
|
1797
|
+
"policy, pack and p are all spellings of policies.",
|
|
1798
|
+
"add, remove and show in full: failproofai policies add --help",
|
|
1799
|
+
],
|
|
1800
|
+
});
|
|
1696
1801
|
process.exit(0);
|
|
1697
1802
|
}
|
|
1698
1803
|
|
|
@@ -1914,82 +2019,100 @@ EXAMPLES
|
|
|
1914
2019
|
// onboarding via bare `failproofai`).
|
|
1915
2020
|
if (args[0] === "config") {
|
|
1916
2021
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2022
|
+
await printHelp({
|
|
2023
|
+
command: "config",
|
|
2024
|
+
tagline: "set this machine up",
|
|
2025
|
+
sections: [
|
|
2026
|
+
{
|
|
2027
|
+
label: "usage",
|
|
2028
|
+
entries: [
|
|
2029
|
+
["(bare)", "Guided setup: agents, daemon, cloud"],
|
|
2030
|
+
["--token <key>", "Set up and connect to Cloud, asking nothing"],
|
|
2031
|
+
["--status", "Connection, daemon version and pause state"],
|
|
2032
|
+
["--pause [<time>]", "Pause enforcement for one session"],
|
|
2033
|
+
["--resume [--all]", "End a pause early"],
|
|
2034
|
+
["--disconnect", "Stop pulling policy and sending activity"],
|
|
2035
|
+
["configure, setup", "Aliases for config"],
|
|
2036
|
+
],
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
label: "what it does",
|
|
2040
|
+
lines: [
|
|
2041
|
+
"Installs the failproofaid service (needs root once), wires hooks into",
|
|
2042
|
+
"every agent CLI it supports, and optionally connects this machine to",
|
|
2043
|
+
"Cloud. It chooses NO policies — take some with:",
|
|
2044
|
+
"",
|
|
2045
|
+
" failproofai policies add <owner>/<repo>",
|
|
2046
|
+
],
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
label: "with no terminal (CI, containers, an agent driving it)",
|
|
2050
|
+
lines: [
|
|
2051
|
+
"It just runs. There is nothing to confirm when nobody is watching, so it",
|
|
2052
|
+
"applies rather than asking — no flag needed.",
|
|
2053
|
+
"",
|
|
2054
|
+
"Exit 1 if anything it was asked to do did not happen — including a key",
|
|
2055
|
+
"the server refused, and a machine that could not reach root. sudo is",
|
|
2056
|
+
"never prompted for here; it either works without a password, or you are",
|
|
2057
|
+
"told the exact commands to run.",
|
|
2058
|
+
],
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
label: "failproof cloud",
|
|
2062
|
+
entries: [
|
|
2063
|
+
["--token <key>", "The API key. Passing one IS the request to connect."],
|
|
2064
|
+
["--url <url>", "Somewhere other than app.befailproof.ai"],
|
|
2065
|
+
["--machine-id <id>", "Defaults to a stable per-machine key"],
|
|
2066
|
+
["--machine-label <n>", "Dashboard name. Alone, renames a connected machine."],
|
|
2067
|
+
["--no-transcripts", "Decisions only, no session transcripts"],
|
|
2068
|
+
["--connect <url>", "Enrol only, on a machine already set up"],
|
|
2069
|
+
["--disconnect", "Stop pulling policy and sending activity"],
|
|
2070
|
+
],
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
label: "what connecting means",
|
|
2074
|
+
lines: [
|
|
2075
|
+
"The key can come from FAILPROOFAI_CLOUD_TOKEN instead of the command",
|
|
2076
|
+
"line, and the url from FAILPROOFAI_CLOUD_URL — the same variable the",
|
|
2077
|
+
"daemon reads. Prefer the environment: an argument is readable from `ps`",
|
|
2078
|
+
"by every user on the box, and lands in shell history and CI logs.",
|
|
2079
|
+
"",
|
|
2080
|
+
"One connection, two capabilities: this machine PULLS centrally-managed",
|
|
2081
|
+
"policies and SENDS what its hooks decided. Both are checked against the",
|
|
2082
|
+
"server before anything is written, and reported separately — a key",
|
|
2083
|
+
"carrying policies:pull but not events:add connects for policy and says",
|
|
2084
|
+
"exactly why the dashboard is empty.",
|
|
2085
|
+
"",
|
|
2086
|
+
"Connecting sends BOTH policy decisions and full session transcripts. A",
|
|
2087
|
+
"transcript carries prompts, file contents and whatever was pasted into a",
|
|
2088
|
+
"terminal — that is the point of connecting, and it is stated here rather",
|
|
2089
|
+
"than buried behind a flag nobody finds. Use --no-transcripts for",
|
|
2090
|
+
"decisions only.",
|
|
2091
|
+
"",
|
|
2092
|
+
"Tokens are stored owner-only in ~/.failproofai/, never in the service",
|
|
2093
|
+
"unit — that file is world-readable. Connecting needs no sudo.",
|
|
2094
|
+
],
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
label: "pausing enforcement (one session, always time-boxed)",
|
|
2098
|
+
entries: [
|
|
2099
|
+
["--pause", "This directory's newest agent session, for 30m"],
|
|
2100
|
+
["--pause 10m", "A given time (max 8h; s/m/h, bare = minutes)"],
|
|
2101
|
+
["--resume", "End the pause early"],
|
|
2102
|
+
["--resume --all", "End every active pause"],
|
|
2103
|
+
["--session <id>", "Target a specific session"],
|
|
2104
|
+
["--status", "What is paused, and when it lifts"],
|
|
2105
|
+
],
|
|
2106
|
+
after: [
|
|
2107
|
+
"A pause suspends builtin, custom and convention policies for that",
|
|
2108
|
+
"session only, and always expires on its own. Cloud-managed policies",
|
|
2109
|
+
"keep enforcing.",
|
|
2110
|
+
],
|
|
2111
|
+
},
|
|
2112
|
+
],
|
|
2113
|
+
footer: ["Prefer flags? failproofai policies --help"],
|
|
2114
|
+
});
|
|
2115
|
+
process.exit(0);
|
|
1993
2116
|
process.exit(0);
|
|
1994
2117
|
}
|
|
1995
2118
|
lastSubcommand = "config";
|
|
@@ -2047,10 +2170,7 @@ PAUSING ENFORCEMENT (one session, always time-boxed)
|
|
|
2047
2170
|
sessions: !args.includes("--no-transcripts"),
|
|
2048
2171
|
});
|
|
2049
2172
|
}
|
|
2050
|
-
|
|
2051
|
-
if (result.exitCode === 0) console.log(line);
|
|
2052
|
-
else console.error(line);
|
|
2053
|
-
}
|
|
2173
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
2054
2174
|
await track("cli_cloud_enrollment", {
|
|
2055
2175
|
action: wantsRename ? "rename" : wantsDisconnect ? "disconnect" : "connect",
|
|
2056
2176
|
ok: result.exitCode === 0,
|
|
@@ -2121,10 +2241,7 @@ PAUSING ENFORCEMENT (one session, always time-boxed)
|
|
|
2121
2241
|
),
|
|
2122
2242
|
);
|
|
2123
2243
|
} else {
|
|
2124
|
-
|
|
2125
|
-
if (result.exitCode === 0) console.log(line);
|
|
2126
|
-
else console.error(line);
|
|
2127
|
-
}
|
|
2244
|
+
await printLines(result.lines, result.exitCode === 0);
|
|
2128
2245
|
}
|
|
2129
2246
|
await track("cli_pause_invoked", {
|
|
2130
2247
|
action: pauseIdx >= 0 ? "pause" : wantsResume ? "resume" : "status",
|