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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- 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 +3 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -3
- 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 +2 -2
- 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 +4 -3
- 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 +4 -4
- 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 +4 -4
- 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 +4 -4
- 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/[externals]__0-9ti7z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[externals]__1lt47k4._.js → [externals]__0sg9-3p._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
- 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
- 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
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__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]__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]__0n_lxhg._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yh7hlv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ehudfr._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1n2ywfz._.js → _1es2j7i._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1h33neq._.js → _1un1pug._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +9 -9
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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/0nkpwjk01ei3t.js +1 -0
- package/.next/standalone/.next/static/chunks/{2iryw37qfqy5y.js → 0ofdelz65_517.js} +1 -1
- package/.next/standalone/.next/static/chunks/{34j7nmidlcuni.js → 12uhnpwgsmcev.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0i3a6wf-f_yky.js → 1_1eb_149xyt9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3927pu4ss_l4k.js → 1tx9pnxyjrkoj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1n04wzsaes73g.js → 1x0jrl3a4_i2m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3r0udrtpn78-e.js → 1zpv6omeln7al.js} +1 -1
- package/.next/standalone/.next/static/chunks/{416zumre3fvfw.js → 20hbg-r3io_ip.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11a_m8yksthtf.js → 32bhi32ym8yrz.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0u2ojcw25c_np.js → 3fr78ugeb4tjc.js} +1 -1
- package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
- package/.next/standalone/app/actions/pack-actions.ts +12 -1
- package/.next/standalone/app/policies/hooks-client.tsx +41 -4
- package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/README.md +58 -13
- package/bin/failproofai.mjs +10 -1
- package/dist/cli.mjs +22429 -21292
- package/dist/worker.mjs +1247 -540
- package/package.json +9 -9
- package/src/audit/index.ts +24 -4
- package/src/hooks/builtin-policies.ts +933 -8
- package/src/hooks/custom-hooks-loader.ts +98 -8
- package/src/hooks/handler.ts +26 -14
- package/src/hooks/manager.ts +36 -2
- package/src/hooks/pack-cli.ts +415 -42
- package/src/hooks/pack-failclosed.ts +100 -4
- package/src/hooks/pack-manifest.ts +29 -0
- package/src/hooks/pack-param-key.ts +36 -0
- package/src/hooks/pack-store.ts +101 -9
- package/src/hooks/policy-catalog.ts +9 -1
- package/src/hooks/policy-evaluator.ts +77 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-wzs7f._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wlvpe5._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0l7zv8b._.js +0 -23
- package/.next/standalone/.next/static/chunks/1ch5l9t2p16y4.js +0 -1
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
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
|
|
124
|
-
"@failproofai/failproofaid-linux-arm64": "1.0.2
|
|
125
|
-
"@failproofai/failproofaid-darwin-x64": "1.0.2
|
|
126
|
-
"@failproofai/failproofaid-darwin-arm64": "1.0.2
|
|
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": "
|
|
130
|
-
"linux-arm64": "
|
|
131
|
-
"darwin-x64": "
|
|
132
|
-
"darwin-arm64": "
|
|
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
|
|
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
|
-
**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
34
|
-
gateways
|
|
35
|
-
|
|
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
|
|
140
|
-
failproofai
|
|
150
|
+
failproofai config
|
|
151
|
+
failproofai policies add FailproofAI/policies
|
|
152
|
+
failproofai # dashboard on localhost:8020
|
|
141
153
|
```
|
|
142
154
|
|
|
143
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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.
|
package/bin/failproofai.mjs
CHANGED
|
@@ -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
|
|
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
|
{
|