failproofai 1.0.0-beta.2 → 1.0.0-beta.3
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 +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -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.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/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- 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/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- 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]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__10s_z4l._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- 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/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_0jfts82.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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 +32 -20
- package/.next/standalone/.next/static/chunks/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/Cargo.lock +1992 -198
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
- package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
- package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
- package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
- package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
- package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
- package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
- package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
- package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
- package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
- package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2195 -770
- package/dist/worker.mjs +365 -52
- package/package.json +2 -2
- package/src/hooks/builtin-policies.ts +139 -0
- package/src/hooks/cloud-connection.ts +205 -0
- package/src/hooks/cloud-enrollment-cli.ts +227 -0
- package/src/hooks/cloud-enrollment.ts +173 -0
- package/src/hooks/cloud-managed-policies.ts +138 -0
- package/src/hooks/collector-config.ts +226 -0
- package/src/hooks/configure-wizard.ts +221 -5
- package/src/hooks/custom-hooks-loader.ts +69 -25
- package/src/hooks/daemon-client.ts +18 -0
- package/src/hooks/handler.ts +144 -6
- package/src/hooks/hook-activity-store.ts +50 -0
- package/src/hooks/loader-utils.ts +6 -4
- package/src/hooks/policy-types.ts +35 -0
- package/src/hooks/session-pause-cli.ts +157 -0
- package/src/hooks/session-pause.ts +236 -0
- package/src/hooks/tui.ts +115 -0
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
//! The hook-activity source.
|
|
2
|
+
//!
|
|
3
|
+
//! Tails `~/.failproofai/cache/hook-activity/*.jsonl` and turns each row into
|
|
4
|
+
//! AgentEye hook events. This is the capability `agenteye-collector`
|
|
5
|
+
//! structurally cannot have: failproofai already sits in the hook path of
|
|
6
|
+
//! every supported agent CLI, and nothing else on the machine does.
|
|
7
|
+
//!
|
|
8
|
+
//! # One source covers every CLI
|
|
9
|
+
//!
|
|
10
|
+
//! The store is CLI-agnostic — each row carries its own `integration` — so
|
|
11
|
+
//! twelve agents are covered by one tailer rather than twelve. Coverage is a
|
|
12
|
+
//! function of where failproofai's hooks are installed, not of anything here.
|
|
13
|
+
//!
|
|
14
|
+
//! # Session ids line up for free
|
|
15
|
+
//!
|
|
16
|
+
//! Measured on a real machine: 25 of 43 hook sessions share an exact session id
|
|
17
|
+
//! with a Claude transcript, and a live Copilot run produced a hook `sessionId`
|
|
18
|
+
//! identical to both its resume id and the id inside its own transcript. So
|
|
19
|
+
//! hook events land on the same AgentEye session as the tool calls around them
|
|
20
|
+
//! with no mapping table, which is what makes them useful rather than a
|
|
21
|
+
//! separate stream nobody correlates.
|
|
22
|
+
//!
|
|
23
|
+
//! # Rotation
|
|
24
|
+
//!
|
|
25
|
+
//! The store appends to `current.jsonl` and, every 25 rows, RENAMES it to
|
|
26
|
+
//! `page-<ts>-<seq>.jsonl` before creating a fresh `current.jsonl`. Cursors are
|
|
27
|
+
//! keyed by inode precisely so that rename is a no-op for us — see
|
|
28
|
+
//! [`crate::cursor`]. The store's other files (`current.count`, `stats.json`,
|
|
29
|
+
//! `current.lock`) are excluded by the `.jsonl` check.
|
|
30
|
+
//!
|
|
31
|
+
//! # A known limit of the aggregated mode
|
|
32
|
+
//!
|
|
33
|
+
//! Under [`HooksVerbosity::Decisions`], `allow` rows are rolled up per
|
|
34
|
+
//! `(session, event, tool, minute)`. That roll-up is computed per poll pass, so
|
|
35
|
+
//! it is only idempotent when a re-read covers the same rows.
|
|
36
|
+
//!
|
|
37
|
+
//! In normal operation it is: cursors advance only after the pass's events are
|
|
38
|
+
//! durably spooled, so a crash re-reads exactly what it read before and
|
|
39
|
+
//! produces byte-identical buckets, which the server's content-hash dedup
|
|
40
|
+
//! collapses. Measured against a 20,392-row corpus, a second run with cursors
|
|
41
|
+
//! intact emits nothing at all.
|
|
42
|
+
//!
|
|
43
|
+
//! What is NOT idempotent is a re-read that sees a *different* number of rows
|
|
44
|
+
//! for a bucket than a previous one did — losing the cursor file after a pass,
|
|
45
|
+
//! say. The bucket is then emitted twice with different counts, and because the
|
|
46
|
+
//! payloads differ the server stores both rather than collapsing them, so that
|
|
47
|
+
//! minute's allow total is overstated. Deny and instruct events are unaffected:
|
|
48
|
+
//! they are emitted per row with an offset-derived id, so a re-read reproduces
|
|
49
|
+
//! them exactly.
|
|
50
|
+
//!
|
|
51
|
+
//! Aggregating a stream cannot avoid this in general — a bucket is only final
|
|
52
|
+
//! once no more rows can land in it, which is not knowable while tailing. Use
|
|
53
|
+
//! [`HooksVerbosity::All`] where exact allow counts matter more than volume.
|
|
54
|
+
|
|
55
|
+
pub mod transform;
|
|
56
|
+
|
|
57
|
+
use std::collections::BTreeMap;
|
|
58
|
+
use std::path::{Path, PathBuf};
|
|
59
|
+
|
|
60
|
+
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
|
61
|
+
|
|
62
|
+
use crate::config::HooksVerbosity;
|
|
63
|
+
use crate::cursor::{CursorStore, FileCursor, identity};
|
|
64
|
+
use crate::spool::{DEFAULT_MAX_BATCH_BYTES, SpoolWriter, is_batch_file};
|
|
65
|
+
use crate::supervisor::{Shutdown, TaskError};
|
|
66
|
+
use transform::{AllowBucket, HookRow, bucket_key};
|
|
67
|
+
|
|
68
|
+
/// How often the store is re-scanned. Hook rows are written by short-lived
|
|
69
|
+
/// processes many times a minute; this is frequent enough to feel live without
|
|
70
|
+
/// scanning 800 files constantly.
|
|
71
|
+
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
|
|
72
|
+
|
|
73
|
+
/// Most bytes read from one file per poll. Bounds memory when a backlog is
|
|
74
|
+
/// being worked through.
|
|
75
|
+
const MAX_READ_BYTES: u64 = 8 * 1024 * 1024;
|
|
76
|
+
|
|
77
|
+
/// Run the source until shutdown.
|
|
78
|
+
pub async fn run(
|
|
79
|
+
store_dir: PathBuf,
|
|
80
|
+
state_dir: PathBuf,
|
|
81
|
+
spool_dir: PathBuf,
|
|
82
|
+
verbosity: HooksVerbosity,
|
|
83
|
+
environment: String,
|
|
84
|
+
sd: Shutdown,
|
|
85
|
+
) -> Result<(), TaskError> {
|
|
86
|
+
if verbosity == HooksVerbosity::Off {
|
|
87
|
+
// Nothing to do, but returning Ok would end the task; idle instead so
|
|
88
|
+
// the supervisor's task list stays a straightforward reflection of
|
|
89
|
+
// what is configured.
|
|
90
|
+
while !sd.is_set() {
|
|
91
|
+
sd.sleep(POLL_INTERVAL).await;
|
|
92
|
+
}
|
|
93
|
+
return Ok(());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let mut cursors = CursorStore::load(state_dir.clone());
|
|
97
|
+
tracing::info!(
|
|
98
|
+
dir = %store_dir.display(),
|
|
99
|
+
resumed = cursors.len(),
|
|
100
|
+
?verbosity,
|
|
101
|
+
"hook-activity source started"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
loop {
|
|
105
|
+
match poll_once(
|
|
106
|
+
&store_dir,
|
|
107
|
+
&spool_dir,
|
|
108
|
+
&mut cursors,
|
|
109
|
+
verbosity,
|
|
110
|
+
&environment,
|
|
111
|
+
)
|
|
112
|
+
.await
|
|
113
|
+
{
|
|
114
|
+
Ok(events) => crate::health::report_poll(
|
|
115
|
+
"hooks",
|
|
116
|
+
store_dir.exists(),
|
|
117
|
+
events,
|
|
118
|
+
cursors.len() as u64,
|
|
119
|
+
),
|
|
120
|
+
Err(err) => {
|
|
121
|
+
// Logged and retried on the next tick rather than returned: one
|
|
122
|
+
// unreadable page must not restart the source and re-scan
|
|
123
|
+
// everything.
|
|
124
|
+
tracing::warn!(%err, "hook-activity poll failed; retrying next tick");
|
|
125
|
+
crate::health::report_error("hooks", &err.to_string());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if !sd.sleep(POLL_INTERVAL).await {
|
|
129
|
+
return Ok(());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// One pass over the store.
|
|
135
|
+
async fn poll_once(
|
|
136
|
+
store_dir: &Path,
|
|
137
|
+
spool_dir: &Path,
|
|
138
|
+
cursors: &mut CursorStore,
|
|
139
|
+
verbosity: HooksVerbosity,
|
|
140
|
+
environment: &str,
|
|
141
|
+
) -> Result<u64, TaskError> {
|
|
142
|
+
let files = list_pages(store_dir).await;
|
|
143
|
+
if files.is_empty() {
|
|
144
|
+
return Ok(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let mut writer = SpoolWriter::new(
|
|
148
|
+
spool_dir.to_path_buf(),
|
|
149
|
+
DEFAULT_MAX_BATCH_BYTES,
|
|
150
|
+
"hooks",
|
|
151
|
+
"activity",
|
|
152
|
+
);
|
|
153
|
+
// Allow rows are rolled up across the whole pass, so a bucket spanning two
|
|
154
|
+
// files still emits once.
|
|
155
|
+
let mut buckets: BTreeMap<transform::BucketKey, AllowBucket> = BTreeMap::new();
|
|
156
|
+
let mut advanced: Vec<FileCursor> = Vec::new();
|
|
157
|
+
let mut emitted = 0u64;
|
|
158
|
+
|
|
159
|
+
for path in files {
|
|
160
|
+
let Ok(meta) = tokio::fs::metadata(&path).await else {
|
|
161
|
+
continue;
|
|
162
|
+
};
|
|
163
|
+
let (dev, inode) = identity(&meta);
|
|
164
|
+
let size = meta.len();
|
|
165
|
+
|
|
166
|
+
let mut offset = cursors
|
|
167
|
+
.resume(dev, inode, &path)
|
|
168
|
+
.map(|c| c.offset)
|
|
169
|
+
.unwrap_or(0);
|
|
170
|
+
|
|
171
|
+
// The store never rewrites a page, so a shrink means the file was
|
|
172
|
+
// replaced under a reused inode. Re-read rather than seek past the end.
|
|
173
|
+
if size < offset {
|
|
174
|
+
tracing::warn!(file = %path.display(), size, offset, "file shrank; re-reading from the start");
|
|
175
|
+
offset = 0;
|
|
176
|
+
}
|
|
177
|
+
if size == offset {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let read_end = size.min(offset + MAX_READ_BYTES);
|
|
182
|
+
let Ok(chunk) = read_range(&path, offset, read_end).await else {
|
|
183
|
+
continue;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Consume only up to the last newline: the final line may still be
|
|
187
|
+
// being written, and half a JSON object parses as nothing.
|
|
188
|
+
let Some(last_nl) = chunk.iter().rposition(|b| *b == b'\n') else {
|
|
189
|
+
continue;
|
|
190
|
+
};
|
|
191
|
+
let usable = &chunk[..=last_nl];
|
|
192
|
+
|
|
193
|
+
let mut line_start = offset;
|
|
194
|
+
for line in usable.split_inclusive(|b| *b == b'\n') {
|
|
195
|
+
let this_offset = line_start;
|
|
196
|
+
line_start += line.len() as u64;
|
|
197
|
+
|
|
198
|
+
let Ok(text) = std::str::from_utf8(line) else {
|
|
199
|
+
continue;
|
|
200
|
+
};
|
|
201
|
+
let text = text.trim();
|
|
202
|
+
if text.is_empty() {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let Ok(row) = serde_json::from_str::<HookRow>(text) else {
|
|
206
|
+
continue;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// An observe-mode row is an `allow` by construction — the verdict
|
|
210
|
+
// was evaluated and discarded — so the roll-up would sweep it up
|
|
211
|
+
// and erase the would-be verdict, which is the entire measurement
|
|
212
|
+
// a trial exists to produce. Emit those exactly.
|
|
213
|
+
let aggregate =
|
|
214
|
+
verbosity == HooksVerbosity::Decisions && row.is_allow() && !row.has_observation();
|
|
215
|
+
if aggregate {
|
|
216
|
+
if let Some(key) = bucket_key(&row) {
|
|
217
|
+
buckets
|
|
218
|
+
.entry(key.clone())
|
|
219
|
+
.or_insert_with(|| AllowBucket {
|
|
220
|
+
session_id: key.0.clone(),
|
|
221
|
+
agent_id: transform::agent_id(&row),
|
|
222
|
+
event_name: key.1.clone(),
|
|
223
|
+
tool_name: key.2.clone(),
|
|
224
|
+
minute_ms: key.3,
|
|
225
|
+
count: 0,
|
|
226
|
+
total_duration_ms: 0.0,
|
|
227
|
+
max_duration_ms: 0.0,
|
|
228
|
+
attribution: key.4.clone(),
|
|
229
|
+
})
|
|
230
|
+
.add(&row);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for event in transform::to_events(&row, this_offset, environment) {
|
|
236
|
+
writer.push(event).await.map_err(io_err)?;
|
|
237
|
+
emitted += 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
advanced.push(FileCursor {
|
|
242
|
+
path: path.clone(),
|
|
243
|
+
dev,
|
|
244
|
+
inode,
|
|
245
|
+
offset: offset + usable.len() as u64,
|
|
246
|
+
size_seen: size,
|
|
247
|
+
// The hook store is a flat row log, not a session transcript: it
|
|
248
|
+
// has no per-session lifecycle and carries no cross-line state, so
|
|
249
|
+
// every field the tailing engine uses stays at its default here.
|
|
250
|
+
..Default::default()
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for bucket in buckets.values() {
|
|
255
|
+
if let Some(event) = bucket.to_event(environment) {
|
|
256
|
+
writer.push(event).await.map_err(io_err)?;
|
|
257
|
+
emitted += 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Flush BEFORE advancing cursors. A crash between the two costs a re-ship,
|
|
262
|
+
// which the server dedups on a content hash; the other order loses the
|
|
263
|
+
// events outright.
|
|
264
|
+
writer.flush().await.map_err(io_err)?;
|
|
265
|
+
|
|
266
|
+
for c in advanced {
|
|
267
|
+
cursors.set(c);
|
|
268
|
+
}
|
|
269
|
+
cursors.retain_existing();
|
|
270
|
+
cursors.save().map_err(io_err)?;
|
|
271
|
+
Ok(emitted)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn io_err(e: std::io::Error) -> TaskError {
|
|
275
|
+
TaskError::from(e.to_string())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Every `.jsonl` in the store, oldest first.
|
|
279
|
+
///
|
|
280
|
+
/// Oldest first so a backlog is shipped in the order it happened; the
|
|
281
|
+
/// `.jsonl` filter is what excludes `current.count`, `stats.json` and the
|
|
282
|
+
/// lock file without naming them.
|
|
283
|
+
async fn list_pages(dir: &Path) -> Vec<PathBuf> {
|
|
284
|
+
let Ok(mut rd) = tokio::fs::read_dir(dir).await else {
|
|
285
|
+
return Vec::new();
|
|
286
|
+
};
|
|
287
|
+
let mut out: Vec<(std::time::SystemTime, PathBuf)> = Vec::new();
|
|
288
|
+
while let Ok(Some(entry)) = rd.next_entry().await {
|
|
289
|
+
let path = entry.path();
|
|
290
|
+
if !is_batch_file(&path) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
let modified = entry
|
|
294
|
+
.metadata()
|
|
295
|
+
.await
|
|
296
|
+
.and_then(|m| m.modified())
|
|
297
|
+
.unwrap_or(std::time::UNIX_EPOCH);
|
|
298
|
+
out.push((modified, path));
|
|
299
|
+
}
|
|
300
|
+
out.sort_by_key(|(t, _)| *t);
|
|
301
|
+
out.into_iter().map(|(_, p)| p).collect()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async fn read_range(path: &Path, from: u64, to: u64) -> std::io::Result<Vec<u8>> {
|
|
305
|
+
let mut f = tokio::fs::File::open(path).await?;
|
|
306
|
+
f.seek(std::io::SeekFrom::Start(from)).await?;
|
|
307
|
+
let mut buf = vec![0u8; (to - from) as usize];
|
|
308
|
+
f.read_exact(&mut buf).await?;
|
|
309
|
+
Ok(buf)
|
|
310
|
+
}
|