failproofai 1.0.2-beta.8 → 1.0.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.
Files changed (165) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  22. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  42. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  47. package/.next/standalone/.next/server/app/policies/page.js +4 -4
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  61. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[externals]__0-9ti7z._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/{[externals]__1lt47k4._.js → [externals]__0sg9-3p._.js} +1 -1
  65. package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
  73. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
  74. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  75. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
  78. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
  80. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
  85. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n_lxhg._.js +5 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yh7hlv._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/_0ehudfr._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1n2ywfz._.js → _1es2j7i._.js} +4 -4
  100. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/{_1h33neq._.js → _1un1pug._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +9 -9
  104. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  109. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  110. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  111. package/.next/standalone/.next/server/middleware.js +1 -1
  112. package/.next/standalone/.next/server/pages/404.html +1 -1
  113. package/.next/standalone/.next/server/pages/500.html +1 -1
  114. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  116. package/.next/standalone/.next/static/chunks/0nkpwjk01ei3t.js +1 -0
  117. package/.next/standalone/.next/static/chunks/{2iryw37qfqy5y.js → 0ofdelz65_517.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{34j7nmidlcuni.js → 12uhnpwgsmcev.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{0i3a6wf-f_yky.js → 1_1eb_149xyt9.js} +1 -1
  120. package/.next/standalone/.next/static/chunks/{3927pu4ss_l4k.js → 1tx9pnxyjrkoj.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{1n04wzsaes73g.js → 1x0jrl3a4_i2m.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{3r0udrtpn78-e.js → 1zpv6omeln7al.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/{416zumre3fvfw.js → 20hbg-r3io_ip.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{11a_m8yksthtf.js → 32bhi32ym8yrz.js} +2 -2
  125. package/.next/standalone/.next/static/chunks/{0u2ojcw25c_np.js → 3fr78ugeb4tjc.js} +1 -1
  126. package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
  127. package/.next/standalone/app/actions/pack-actions.ts +12 -1
  128. package/.next/standalone/app/policies/hooks-client.tsx +41 -4
  129. package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
  130. package/.next/standalone/package.json +9 -9
  131. package/.next/standalone/server.js +1 -1
  132. package/README.md +58 -13
  133. package/bin/failproofai.mjs +10 -1
  134. package/dist/cli.mjs +22429 -21292
  135. package/dist/worker.mjs +1247 -540
  136. package/package.json +9 -9
  137. package/src/audit/index.ts +24 -4
  138. package/src/hooks/builtin-policies.ts +933 -8
  139. package/src/hooks/custom-hooks-loader.ts +98 -8
  140. package/src/hooks/handler.ts +26 -14
  141. package/src/hooks/manager.ts +36 -2
  142. package/src/hooks/pack-cli.ts +415 -42
  143. package/src/hooks/pack-failclosed.ts +100 -4
  144. package/src/hooks/pack-manifest.ts +29 -0
  145. package/src/hooks/pack-param-key.ts +36 -0
  146. package/src/hooks/pack-store.ts +101 -9
  147. package/src/hooks/policy-catalog.ts +9 -1
  148. package/src/hooks/policy-evaluator.ts +77 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
  156. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  157. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  158. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
  159. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-wzs7f._.js +0 -3
  160. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wlvpe5._.js +0 -5
  161. package/.next/standalone/.next/server/chunks/ssr/_0l7zv8b._.js +0 -23
  162. package/.next/standalone/.next/static/chunks/1ch5l9t2p16y4.js +0 -1
  163. /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_buildManifest.js +0 -0
  164. /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_clientMiddlewareManifest.js +0 -0
  165. /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_ssgManifest.js +0 -0
@@ -0,0 +1,204 @@
1
+ """When `--all` stops early, the response must say so.
2
+
3
+ The defect these pin was measured against a live instance, on the CLI this one
4
+ was moved from:
5
+
6
+ fp --json sessions --since 7d --limit 200 -> 200 rows, next_cursor set
7
+ fp --json sessions --since 7d --all -> 50 rows (25x undercount)
8
+ fp --json sessions --since 7d --all --limit 5000 -> 1,274 rows
9
+
10
+ `--all` inherits `--limit`'s default of 50 — which is documented and deliberate
11
+ here — but the envelope then hard-coded `next_cursor: null`, so a 4% sample of a
12
+ customer's fleet came back with exit 0, no warning, and a response that read as
13
+ complete. Nothing in the output could tell you otherwise.
14
+
15
+ The fix (`Walk`, carried through `client.paginate`) is present in this repo and
16
+ was untested when these were written. That combination is the one worth
17
+ guarding: a silent-completeness bug cannot be caught by reading the code, only
18
+ by asserting on what the envelope claims.
19
+
20
+ So every test here checks the REPORTED completeness, not just the row count.
21
+
22
+ The fake feed is a real cursor server — it honours `limit` and `cursor` — not a
23
+ list of canned pages. A fixture that ignored `limit` would pass no matter how
24
+ few rows the CLI asked for, which is to say it could not fail on the bug.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+
31
+ import httpx
32
+ import pytest
33
+ import respx
34
+
35
+ from fp_cli import client
36
+ from fp_cli.app import app
37
+
38
+ from .conftest import BASE_URL as BASE
39
+
40
+
41
+ def _feed(key: str, total: int, row):
42
+ """respx side_effect: a cursor-paginated feed holding ``total`` rows.
43
+
44
+ The cursor is the offset of the next row (opaque to the CLI, which only
45
+ round-trips it). ``limit`` is honoured exactly, so the CLI's own page sizing
46
+ decides how much of the feed it can actually reach.
47
+ """
48
+
49
+ def handler(request: httpx.Request) -> httpx.Response:
50
+ params = request.url.params
51
+ limit = int(params["limit"])
52
+ offset = int(params.get("cursor") or 0)
53
+ rows = [row(i) for i in range(offset, min(offset + limit, total))]
54
+ nxt = offset + len(rows)
55
+ return httpx.Response(
56
+ 200, json={key: rows, "next_cursor": str(nxt) if nxt < total else None}
57
+ )
58
+
59
+ return handler
60
+
61
+
62
+ def _event_row(i: int) -> dict:
63
+ return {
64
+ "id": i, "session_id": "s", "agent_id": "a", "event_type": "tool_use",
65
+ "ts": "2026-08-24T12:00:00Z", "environment": "prod", "summary": f"row {i}",
66
+ }
67
+
68
+
69
+ def _session_row(i: int) -> dict:
70
+ return {
71
+ "session_id": f"sess-{i}", "agent_id": "a", "environment": "prod",
72
+ "last_event_at": "2026-08-24T12:00:00Z", "latest_evaluation": None,
73
+ }
74
+
75
+
76
+ def _eval_row(i: int) -> dict:
77
+ return {
78
+ "id": f"eval-{i}", "session_id": "s", "agent_id": "a", "environment": "prod",
79
+ "status": "done", "scores": {"helpfulness": 0.9}, "created_at": "2026-08-24T12:00:00Z",
80
+ }
81
+
82
+
83
+ # (command, endpoint, server key, envelope key, row builder). Every command that
84
+ # takes `--all`; the bug was reported on `sessions` but lived in the shared
85
+ # walker, so all four had it. `errors` is the one where the two keys differ: it
86
+ # reads the shared errored-events feed (`{"events": …}`) and re-labels it
87
+ # `errors` on the way out.
88
+ FEEDS = [
89
+ ("events", "/api/events/summary", "events", "events", _event_row),
90
+ ("sessions", "/api/sessions", "sessions", "sessions", _session_row),
91
+ ("evals", "/api/evaluations", "evaluations", "evaluations", _eval_row),
92
+ ("errors", "/api/events/summary", "events", "errors", _event_row),
93
+ ]
94
+ IDS = [f[0] for f in FEEDS]
95
+
96
+ TOTAL = 274 # > the 50 default, and not a multiple of the 200 page size
97
+
98
+
99
+ @pytest.mark.parametrize("command,endpoint,server_key,key,row", FEEDS, ids=IDS)
100
+ @respx.mock
101
+ def test_all_stopped_by_the_default_limit_is_reported_not_silent(
102
+ logged_in, runner, command, endpoint, server_key, key, row
103
+ ):
104
+ """THE REGRESSION. `--all` stopping at 50 is the documented behaviour; saying
105
+ the feed was exhausted is the bug."""
106
+ respx.get(f"{BASE}{endpoint}").mock(side_effect=_feed(server_key, TOTAL, row))
107
+ res = runner.invoke(app, ["--base-url", BASE, "--json", command, "--all"])
108
+ assert res.exit_code == 0, res.output
109
+ out = json.loads(res.stdout)
110
+ assert len(out[key]) == 50, "the documented default bound still applies"
111
+ assert out["next_cursor"] is not None, (
112
+ "a walk that stopped on --limit must hand back a resumable cursor; "
113
+ "null here is the 4%-sample-reported-as-complete bug"
114
+ )
115
+
116
+
117
+ @pytest.mark.parametrize("command,endpoint,server_key,key,row", FEEDS, ids=IDS)
118
+ @respx.mock
119
+ def test_all_that_exhausts_the_feed_reports_complete(
120
+ logged_in, runner, command, endpoint, server_key, key, row
121
+ ):
122
+ """The other direction, and the reason `truncated` cannot just be "we hit the
123
+ limit": a walk that took every row must NOT claim there is more."""
124
+ respx.get(f"{BASE}{endpoint}").mock(side_effect=_feed(server_key, 30, row))
125
+ res = runner.invoke(app, ["--base-url", BASE, "--json", command, "--all", "--limit", "500"])
126
+ assert res.exit_code == 0, res.output
127
+ out = json.loads(res.stdout)
128
+ assert len(out[key]) == 30
129
+ assert out["next_cursor"] is None, "an exhausted feed must not look truncated"
130
+
131
+
132
+ @respx.mock
133
+ def test_an_explicit_limit_is_honoured_and_reported(logged_in, runner):
134
+ """`--all --limit N` is how you actually get more than the default, so the
135
+ bound has to hold exactly and still report the remainder."""
136
+ respx.get(f"{BASE}/api/sessions").mock(side_effect=_feed("sessions", TOTAL, _session_row))
137
+ res = runner.invoke(
138
+ app, ["--base-url", BASE, "--json", "sessions", "--all", "--limit", "220"]
139
+ )
140
+ assert res.exit_code == 0, res.output
141
+ out = json.loads(res.stdout)
142
+ assert len(out["sessions"]) == 220
143
+ assert out["next_cursor"] is not None
144
+
145
+
146
+ @respx.mock
147
+ def test_all_pages_in_chunks_rather_than_one_giant_request(logged_in, runner):
148
+ """The walk must actually WALK. Asking for 220 in a single request would work
149
+ against this fake and fail against a server that caps a page at 200."""
150
+ route = respx.get(f"{BASE}/api/sessions").mock(
151
+ side_effect=_feed("sessions", TOTAL, _session_row)
152
+ )
153
+ res = runner.invoke(
154
+ app, ["--base-url", BASE, "--json", "sessions", "--all", "--limit", "220"]
155
+ )
156
+ assert res.exit_code == 0, res.output
157
+ assert route.call_count > 1, "a 220-row walk must span more than one request"
158
+ for call in route.calls:
159
+ assert int(call.request.url.params["limit"]) <= 200
160
+
161
+
162
+ # --- the walker itself, below the CLI ---------------------------------------
163
+
164
+
165
+ def _pages(total: int, page: int):
166
+ """A `fetch_page` over ``total`` rows, honouring the limit it is given."""
167
+
168
+ def fetch(cursor=None, limit=None):
169
+ offset = int(cursor or 0)
170
+ size = min(limit or page, page)
171
+ items = list(range(offset, min(offset + size, total)))
172
+ nxt = offset + len(items)
173
+ return client.Page(items=items, next_cursor=str(nxt) if nxt < total else None)
174
+
175
+ return fetch
176
+
177
+
178
+ def test_walk_records_the_cursor_it_stopped_on():
179
+ walk = client.Walk()
180
+ items = list(client.paginate(_pages(500, 200), limit=250, walk=walk))
181
+ assert len(items) == 250
182
+ assert walk.truncated is True
183
+ assert walk.next_cursor is not None, "a bounded walk must be resumable"
184
+
185
+
186
+ def test_walk_that_exhausts_the_feed_is_not_flagged_truncated():
187
+ walk = client.Walk()
188
+ items = list(client.paginate(_pages(120, 200), limit=500, walk=walk))
189
+ assert len(items) == 120
190
+ assert walk.truncated is False
191
+ assert walk.next_cursor is None
192
+
193
+
194
+ def test_truncated_is_not_derivable_from_the_row_count():
195
+ """The subtle case, and why `Walk` exists at all: a walk whose limit lands
196
+ EXACTLY on the last row is complete, and one that lands exactly on a page
197
+ boundary with more behind it is not. Row count cannot tell those apart."""
198
+ exact = client.Walk()
199
+ list(client.paginate(_pages(200, 200), limit=200, walk=exact))
200
+ assert exact.truncated is False, "taking every row is not a truncation"
201
+
202
+ boundary = client.Walk()
203
+ list(client.paginate(_pages(400, 200), limit=200, walk=boundary))
204
+ assert boundary.truncated is True, "stopping on a boundary with more behind it is"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "1.0.2-beta.8",
3
+ "version": "1.0.2",
4
4
  "description": "Observability and enforcement for AI agent harnesses. 39 built-in policies hooked into 12 of them — Claude Code, Codex, Cursor, Hermes, OpenClaw and more — blocking the tool call before it runs. Local dashboard included, no account needed.",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs",
@@ -120,15 +120,15 @@
120
120
  "sharp": "0.35.0"
121
121
  },
122
122
  "optionalDependencies": {
123
- "@failproofai/failproofaid-linux-x64": "1.0.2-beta.8",
124
- "@failproofai/failproofaid-linux-arm64": "1.0.2-beta.8",
125
- "@failproofai/failproofaid-darwin-x64": "1.0.2-beta.8",
126
- "@failproofai/failproofaid-darwin-arm64": "1.0.2-beta.8"
123
+ "@failproofai/failproofaid-linux-x64": "1.0.2",
124
+ "@failproofai/failproofaid-linux-arm64": "1.0.2",
125
+ "@failproofai/failproofaid-darwin-x64": "1.0.2",
126
+ "@failproofai/failproofaid-darwin-arm64": "1.0.2"
127
127
  },
128
128
  "failproofaidBinaries": {
129
- "linux-x64": "7b49023fd9351ccdddc066f9560d998e6b582b763ab4ad4253831a2548e99ae1",
130
- "linux-arm64": "b43af87cb0ca7e83bfbc053051ef4627ce9594cb7b6a520492689f750302adc1",
131
- "darwin-x64": "c03d157deddf78d1d7928aa6d33909e1ff585eace77d5d25090c56c7eb2a9983",
132
- "darwin-arm64": "a2489beaf7e580a3e012a45d346c8df5764f426f460c0c6fb9bd7b405ac65366"
129
+ "linux-x64": "c2afafa22c3516a35737c2a1a47fe16e5caab29c11a62fc8aaef670c9e0a4fae",
130
+ "linux-arm64": "a93f406ff99ce401d0e1637e716e04383ef28fba06d5351b657593bc71e485af",
131
+ "darwin-x64": "40eb1484d260d98d85249859ae075fa4f885838d7d4302f73d809a5939fe2b24",
132
+ "darwin-arm64": "7306cecba6537d8bca9c6b404add0e0226c4ed445c39054dcf4664e18e3b4b43"
133
133
  }
134
134
  }
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.2-beta.8"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.2"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"instrumentationClientInject":[],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","enablePrerenderSourceMaps":true,"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":true,"coldCacheBadge":false,"devValidationWorker":true,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"dynamicOnHover":false,"useOffline":false,"varyParams":true,"optimisticRouting":true,"instrumentationClientRouterTransitionEvents":false,"prefetchInlining":{"maxSize":2048,"maxBundleSize":10240},"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptOperationCache":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"devMemoryThresholdRestart":true,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"requestInsights":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":true,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":true,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"serverComponentsHmrCancellation":false,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":true,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","turbopackMemoryEvictionMode":"auto","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"useCacheTimeout":54,"instantInsights":{"validationLevel":"warning"},"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"repoRoot":"/home/runner/work/failproofai/failproofai","distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15
 
package/README.md CHANGED
@@ -14,11 +14,11 @@
14
14
 
15
15
  **Translations:** [简体中文](./docs/i18n/README.zh.md) · [日本語](./docs/i18n/README.ja.md) · [한국어](./docs/i18n/README.ko.md) · [Español](./docs/i18n/README.es.md) · [Português](./docs/i18n/README.pt-br.md) · [Deutsch](./docs/i18n/README.de.md) · [Français](./docs/i18n/README.fr.md) · [Русский](./docs/i18n/README.ru.md) · [हिन्दी](./docs/i18n/README.hi.md) · [Türkçe](./docs/i18n/README.tr.md) · [Tiếng Việt](./docs/i18n/README.vi.md) · [Italiano](./docs/i18n/README.it.md) · [العربية](./docs/i18n/README.ar.md) · [עברית](./docs/i18n/README.he.md)
16
16
 
17
- **Observability and enforcement for every harness your agents run in.**
18
- Wherever your agents run, we see it and we can say no. Failproof hooks 12 agent
19
- harnesses coding CLIs like Claude Code and Codex, chat gateways like Hermes,
20
- self-hosted assistants like OpenClaw capturing every run and blocking dangerous
21
- tool calls before they execute. 39 built-in policies. Zero latency. Runs locally.
17
+ **See what your agents do. Stop known failures before they repeat.**
18
+ Failproof AI works wherever your agents run: coding tools like Claude Code and
19
+ Codex, chat gateways like Hermes, self-hosted assistants like OpenClaw, and agents
20
+ you instrument yourself. It records each run and can block dangerous tool calls
21
+ before they execute.
22
22
 
23
23
  </div>
24
24
 
@@ -30,9 +30,9 @@ tool calls before they execute. 39 built-in policies. Zero latency. Runs locally
30
30
 
31
31
  ## Supported harnesses
32
32
 
33
- Twelve harnesses in two classes ten coding CLIs, and two chat and assistant
34
- gateways (Hermes, OpenClaw). Same events, same policies, same session history,
35
- whichever one your agent runs in.
33
+ Twelve harnesses in two classes are supported: ten coding CLIs, plus two
34
+ gateways: Hermes, OpenClaw. The policy API and session history are shared; which
35
+ events can block varies by harness.
36
36
 
37
37
  Agents that run in none of them report through the [Python SDK](https://docs.befailproof.ai/reference/custom-agents),
38
38
  which gives you tracing, sessions and audits. Enforcement there needs a hook in
@@ -134,13 +134,31 @@ your own runtime — [talk to us](mailto:support@befailproof.ai) and we'll map i
134
134
 
135
135
  ## Install
136
136
 
137
+ Give a compatible agent the Failproof AI skill if you want it to guide setup,
138
+ inspect the machine, and route policy, audit, session, and Cloud work correctly:
139
+
140
+ ```sh
141
+ npx skills add FailproofAI/skills
142
+ ```
143
+
144
+ This installs the umbrella skill and its specialist siblings. To install only the
145
+ umbrella, add `--skill failproofai`. Skills supply operating instructions; install
146
+ and configure the product itself with:
147
+
137
148
  ```sh
138
149
  npm install -g failproofai
139
- failproofai policies --install # or just run `failproofai` and accept the first-run prompt
140
- failproofai
150
+ failproofai config
151
+ failproofai policies add FailproofAI/policies
152
+ failproofai # dashboard on localhost:8020
141
153
  ```
142
154
 
143
- 39 built-in policies activate immediately. Dashboard at `localhost:8020`. Disable the first-run prompt with `FAILPROOFAI_NO_FIRST_RUN=1`.
155
+ Setup connects supported agents and installs the background service. It chooses no
156
+ policy pack: before you add one, only `block-failproofai-commands` runs to stop an
157
+ agent disabling Failproof AI.
158
+
159
+ Connect Cloud without prompts with `failproofai config --token <machine-key>`. On a
160
+ shared machine or in CI, set `FAILPROOFAI_CLOUD_TOKEN` and run `failproofai config`
161
+ so the key does not appear in command history.
144
162
 
145
163
  ---
146
164
 
@@ -157,8 +175,8 @@ failproofai
157
175
  | `block-rm-rf` | Recursive file deletion |
158
176
  | `block-force-push` / `block-push-master` | `git push --force`, direct pushes to `main` |
159
177
 
160
- The first five apply to any agent that can call a tool. The last three are the
161
- developer favourites coding CLIs are the harness class we cover deepest.
178
+ These policies protect files, credentials, infrastructure, databases, and agent
179
+ workflows. Exact enforcement support varies by harness and event.
162
180
 
163
181
  → [All 39 built-in policies](https://docs.befailproof.ai/policies/builtin)
164
182
 
@@ -195,6 +213,33 @@ Three decisions available to every policy:
195
213
 
196
214
  ---
197
215
 
216
+ ## Policy packs
217
+
218
+ A policy pack is a versioned set of policies published from a public GitHub
219
+ repository. Inspect one before installing it:
220
+
221
+ ```sh
222
+ failproofai policies show FailproofAI/policies
223
+ failproofai policies add FailproofAI/policies
224
+ ```
225
+
226
+ Anything with a slash is a pack source; anything without one is a policy name.
227
+ You can install selected categories or policies, and pin a release when needed.
228
+
229
+ ```sh
230
+ failproofai policies add FailproofAI/policies --category git,database
231
+ failproofai policies add owner/repo@a1b2c3d4e5f6
232
+ ```
233
+
234
+ Browse published packs in the [Policy Hub](https://befailproof.ai/policy-hub/), or
235
+ run `failproofai publish --init` to start your own. Observe mode lets a pack record
236
+ what it would have done without blocking: `failproofai publish --effect observe`.
237
+
238
+ → [Policy packs](https://docs.befailproof.ai/policies/packs) ·
239
+ [Publish a pack](https://docs.befailproof.ai/policies/publish-a-pack)
240
+
241
+ ---
242
+
198
243
  ## Observability
199
244
 
200
245
  Enforcement is one half. The other half is seeing what the agent actually did.
@@ -1408,11 +1408,19 @@ async function runCli() {
1408
1408
  ],
1409
1409
  },
1410
1410
  {
1411
+ // This screen said "an existing private one still publishes, and
1412
+ // warns" for as long as that was true. It is REFUSED now — exit 1,
1413
+ // nothing created or uploaded — and `--allow-private` was named
1414
+ // nowhere in this help, so the one documented behaviour pointed a
1415
+ // publisher at a command that exits 1 and gave them no way through.
1411
1416
  label: "the repo must be public",
1412
1417
  lines: [
1413
1418
  "Installs are anonymous HTTPS with no credential to offer, so a private",
1414
1419
  "repository publishes to nobody. A repo created here is public for that",
1415
- "reason; an existing private one still publishes, and warns.",
1420
+ "reason; an existing private one is REFUSED, before anything is built,",
1421
+ "created or uploaded. --allow-private publishes to one anyway, for",
1422
+ "somebody who will hand the three assets over another way — it still",
1423
+ "says plainly that no `policies add` can reach them.",
1416
1424
  "",
1417
1425
  "Only the release matters. Installs read releases/download/<tag>/<asset>",
1418
1426
  "and never touch your git tree — pushing the source is for humans.",
@@ -1444,6 +1452,7 @@ async function runCli() {
1444
1452
  ["--out <dir>", "Where to write the assets. Default: dist-pack."],
1445
1453
  ["--effect <effect>", "enforce or observe. observe records and blocks nothing. Default: enforce."],
1446
1454
  ["--dry-run", "Build the assets, publish nothing. No credential."],
1455
+ ["--allow-private", "Publish to an already-private repo anyway. Nobody can install it."],
1447
1456
  ],
1448
1457
  },
1449
1458
  {