failproofai 1.0.4-beta.8 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +4 -37
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +4 -4
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -3
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -15
- package/.next/standalone/.next/server/app/settings/page.js +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]__0hmcwgg._.js → [externals]__0-9ti7z._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__14b7cc6._.js → [externals]__0u3wrvq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1_eakl4._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +10 -10
- package/.next/standalone/.next/server/chunks/_1vslkoe._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.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_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__187jks6._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1fa1jd9._.js → [root-of-the-server]__09flfsx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0kokc8z._.js → [root-of-the-server]__0n_lxhg._.js} +1 -1
- 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 +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1kpd1za._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00y9ww2._.js → [root-of-the-server]__1sc84bb._.js} +2 -21
- package/.next/standalone/.next/server/chunks/ssr/_02yoeej._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_00bqkhj._.js → _04rbnaq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0s-77v5._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1_ozgif._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +14 -51
- package/.next/standalone/.next/server/chunks/ssr/{_14c6q09._.js → _1if9ixi._.js} +2 -21
- package/.next/standalone/.next/server/chunks/ssr/{_07t3jol._.js → _1u8-lu2._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +69 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +21 -65
- package/.next/standalone/.next/static/chunks/{1302duobnz07l.js → 0-tyrpwuc3lla.js} +1 -1
- package/.next/standalone/.next/static/chunks/0ca7octhod0g2.js +69 -0
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{2n85g-crgcx5j.js → 1j6o9min_6qa9.js} +1 -1
- package/.next/standalone/.next/static/chunks/27cl8h-c2w7cd.js +1 -0
- package/.next/standalone/.next/static/chunks/{0jpd8dv2vk930.css → 29-iqnp9v_axt.css} +1 -1
- package/.next/standalone/.next/static/chunks/{3uyhmm01g64k4.css → 2k9f4tyv04809.css} +0 -1
- package/.next/standalone/.next/static/chunks/{1chm9piluggc-.js → 2lwipmv5etc8m.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pd16tn5qmx13.js +1 -0
- package/.next/standalone/.next/static/chunks/{2ukucf6g-kbs5.js → 2u8vh73tpw28_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_x7pqemw5n_w.js → 31u94yukw9fz-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2mizt96igig1y.js → 3p9_8x5ezce-9.js} +1 -1
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0vm4wdx3w-2_w.js → 3tjji451hrgc0.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-3s3c-u_u_l0u9.js → turbopack-3p73obe0xmrxm.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +27 -51
- package/.next/standalone/app/actions/get-scheduled-audit.ts +0 -5
- package/.next/standalone/app/actions/update-scheduled-audit.ts +57 -18
- package/.next/standalone/app/api/auth/status/route.ts +2 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +34 -140
- package/.next/standalone/app/audit/_components/audit-poster.tsx +10 -29
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +1 -6
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -4
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +5 -19
- package/.next/standalone/app/audit/_components/share-templates.ts +44 -110
- package/.next/standalone/app/audit/audit-styles.css +1 -132
- package/.next/standalone/app/settings/page.tsx +1 -1
- package/.next/standalone/app/settings/settings-client.tsx +45 -50
- package/.next/standalone/lib/auth/api-server-client.ts +5 -33
- package/.next/standalone/lib/claude-sessions.ts +27 -101
- package/.next/standalone/lib/codex-sessions.ts +4 -20
- package/.next/standalone/lib/copilot-sessions.ts +1 -32
- package/.next/standalone/lib/cursor-sessions.ts +5 -62
- package/.next/standalone/lib/download-session.ts +13 -58
- package/.next/standalone/lib/openclaw-projects.ts +18 -54
- package/.next/standalone/lib/openclaw-sessions.ts +17 -75
- package/.next/standalone/lib/sqlite-reader.ts +1 -78
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -1
- package/dist/cli.mjs +664 -2121
- package/dist/worker.mjs +105 -422
- package/lib/auth/api-server-client.ts +5 -33
- package/lib/claude-sessions.ts +27 -101
- package/lib/codex-sessions.ts +4 -20
- package/lib/copilot-sessions.ts +1 -32
- package/lib/cursor-sessions.ts +5 -62
- package/lib/download-session.ts +13 -58
- package/lib/openclaw-projects.ts +18 -54
- package/lib/openclaw-sessions.ts +17 -75
- package/lib/sqlite-reader.ts +1 -78
- package/package.json +9 -9
- package/pi-extension/index.ts +5 -50
- package/src/audit/cache.ts +0 -6
- package/src/audit/cli-adapters/claude.ts +0 -13
- package/src/audit/cli-login.ts +1 -4
- package/src/audit/cli.ts +7 -157
- package/src/audit/harm-report.ts +4 -111
- package/src/audit/index.ts +15 -286
- package/src/audit/machine-store.ts +9 -8
- package/src/audit/redact-example.ts +15 -324
- package/src/audit/report-harm.ts +37 -33
- package/src/audit/report.ts +3 -6
- package/src/audit/schedule-cli.ts +33 -54
- package/src/audit/scoring.ts +0 -49
- package/src/audit/types.ts +0 -52
- package/src/hooks/builtin-policies.ts +2 -67
- package/src/hooks/configure-wizard.ts +0 -55
- package/src/hooks/fp-config.ts +48 -83
- package/src/hooks/fp-home.ts +0 -27
- package/src/hooks/integrations.ts +14 -51
- package/src/hooks/uninstall-cli.ts +2 -17
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0faqz9x._.js +0 -22
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__112b4go._.js +0 -24
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l71g3y._.js +0 -22
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-4ja6q._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cr3xu._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04px77e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egr5s1._.js +0 -6
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0i0l1di._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n2txhk._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0r3c1_0._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17xnxlx._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ckpmkk._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kaotix._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/_04ng_6x._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +0 -3
- package/.next/standalone/.next/static/chunks/0igq884ziuv-8.js +0 -1
- package/.next/standalone/.next/static/chunks/349jkb79v5g6z.js +0 -1
- package/.next/standalone/.next/static/chunks/3a4gth64hwtwf.js +0 -1
- package/.next/standalone/app/actions/get-leaks.ts +0 -104
- package/.next/standalone/app/audit/_components/leak-section.tsx +0 -192
- package/.next/standalone/lib/openclaw-db.ts +0 -271
- package/lib/openclaw-db.ts +0 -271
- package/src/audit/desktop-notify.ts +0 -420
- package/src/audit/leak-fingerprint.ts +0 -198
- package/src/audit/leak-record.ts +0 -248
- package/src/audit/leak-scan.ts +0 -268
- package/src/audit/leak-store.ts +0 -220
- package/src/audit/macos-notifier.ts +0 -298
- package/src/hooks/notice.ts +0 -24
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{DNhaAH8iPGH0tIuxee5_2 → dg8VQ7Ejlo0Qsh_x92oIt}/_ssgManifest.js +0 -0
package/lib/openclaw-db.ts
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read OpenClaw 2026.9.2+ transcripts from the per-agent SQLite databases at
|
|
3
|
-
* `agents/<agentId>/agent/openclaw-agent.sqlite`.
|
|
4
|
-
*
|
|
5
|
-
* `event_json` is the exact logical JSONL record, so the dashboard can feed it
|
|
6
|
-
* through the same parser and download format used by archived JSONL files.
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { openSqliteReadonly, type SqliteReader } from "./sqlite-reader";
|
|
11
|
-
|
|
12
|
-
const DB_NAME = "openclaw-agent.sqlite";
|
|
13
|
-
|
|
14
|
-
export interface OpenClawSqliteSession {
|
|
15
|
-
sessionId: string;
|
|
16
|
-
agentId: string;
|
|
17
|
-
dbPath: string;
|
|
18
|
-
transcriptPath: string;
|
|
19
|
-
mtimeMs: number;
|
|
20
|
-
sizeBytes: number;
|
|
21
|
-
channel: string;
|
|
22
|
-
label?: string;
|
|
23
|
-
chatId?: string;
|
|
24
|
-
chatType?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface OpenClawSqliteTranscript {
|
|
28
|
-
agentId: string;
|
|
29
|
-
dbPath: string;
|
|
30
|
-
filePath: string;
|
|
31
|
-
rawLines: Record<string, unknown>[];
|
|
32
|
-
eventJsonLines: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface WindowRow {
|
|
36
|
-
session_id: string;
|
|
37
|
-
session_key: string;
|
|
38
|
-
updated_at: number;
|
|
39
|
-
transcript_updated_at: number | null;
|
|
40
|
-
ended_at: number | null;
|
|
41
|
-
channel: string | null;
|
|
42
|
-
chat_type: string | null;
|
|
43
|
-
display_name: string | null;
|
|
44
|
-
event_updated_at: number;
|
|
45
|
-
size_bytes: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface NodeRow {
|
|
49
|
-
current_session_id: string;
|
|
50
|
-
entry_json: string;
|
|
51
|
-
label: string | null;
|
|
52
|
-
display_name: string | null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ConversationRow {
|
|
56
|
-
session_id: string;
|
|
57
|
-
channel: string;
|
|
58
|
-
kind: string;
|
|
59
|
-
peer_id: string;
|
|
60
|
-
delivery_target: string;
|
|
61
|
-
label: string | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface EventRow {
|
|
65
|
-
event_json: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface SessionMeta {
|
|
69
|
-
channel?: string;
|
|
70
|
-
label?: string;
|
|
71
|
-
chatId?: string;
|
|
72
|
-
chatType?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function str(value: unknown): string | undefined {
|
|
76
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseObject(value: string): Record<string, unknown> | undefined {
|
|
80
|
-
try {
|
|
81
|
-
const parsed: unknown = JSON.parse(value);
|
|
82
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
83
|
-
? (parsed as Record<string, unknown>)
|
|
84
|
-
: undefined;
|
|
85
|
-
} catch {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function syntheticPath(agentId: string, sessionId: string): string {
|
|
91
|
-
return `openclaw-sqlite://${encodeURIComponent(agentId)}/${sessionId}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Existing per-agent SQLite databases, derived from the configured agents. */
|
|
95
|
-
export function listOpenClawDatabases(
|
|
96
|
-
home: string,
|
|
97
|
-
agentIds: string[],
|
|
98
|
-
): Array<{ agentId: string; dbPath: string }> {
|
|
99
|
-
const out: Array<{ agentId: string; dbPath: string }> = [];
|
|
100
|
-
for (const agentId of agentIds) {
|
|
101
|
-
const dbPath = join(home, "agents", agentId, "agent", DB_NAME);
|
|
102
|
-
if (existsSync(dbPath)) out.push({ agentId, dbPath });
|
|
103
|
-
}
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readNodeMetadata(db: SqliteReader): Map<string, SessionMeta> {
|
|
108
|
-
const out = new Map<string, SessionMeta>();
|
|
109
|
-
let rows: NodeRow[];
|
|
110
|
-
try {
|
|
111
|
-
rows = db.query<NodeRow>(
|
|
112
|
-
`SELECT current_session_id, entry_json, label, display_name
|
|
113
|
-
FROM session_nodes`,
|
|
114
|
-
);
|
|
115
|
-
} catch {
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
for (const row of rows) {
|
|
119
|
-
const entry = parseObject(row.entry_json) ?? {};
|
|
120
|
-
const origin =
|
|
121
|
-
entry.origin &&
|
|
122
|
-
typeof entry.origin === "object" &&
|
|
123
|
-
!Array.isArray(entry.origin)
|
|
124
|
-
? (entry.origin as Record<string, unknown>)
|
|
125
|
-
: {};
|
|
126
|
-
out.set(row.current_session_id, {
|
|
127
|
-
channel:
|
|
128
|
-
str(entry.lastChannel) ?? str(origin.provider) ?? str(origin.surface),
|
|
129
|
-
label:
|
|
130
|
-
str(row.label) ??
|
|
131
|
-
str(row.display_name) ??
|
|
132
|
-
str(entry.displayName) ??
|
|
133
|
-
str(origin.label),
|
|
134
|
-
chatId: str(entry.lastTo) ?? str(origin.from),
|
|
135
|
-
chatType: str(entry.chatType) ?? str(origin.chatType),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
return out;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function readConversationMetadata(db: SqliteReader): Map<string, SessionMeta> {
|
|
142
|
-
const out = new Map<string, SessionMeta>();
|
|
143
|
-
let rows: ConversationRow[];
|
|
144
|
-
try {
|
|
145
|
-
rows = db.query<ConversationRow>(
|
|
146
|
-
`SELECT sc.session_id, c.channel, c.kind, c.peer_id,
|
|
147
|
-
c.delivery_target, c.label
|
|
148
|
-
FROM session_conversations sc
|
|
149
|
-
JOIN conversations c ON c.conversation_id = sc.conversation_id
|
|
150
|
-
ORDER BY CASE sc.role WHEN 'primary' THEN 0 ELSE 1 END, sc.last_seen_at DESC`,
|
|
151
|
-
);
|
|
152
|
-
} catch {
|
|
153
|
-
return out;
|
|
154
|
-
}
|
|
155
|
-
for (const row of rows) {
|
|
156
|
-
if (out.has(row.session_id)) continue;
|
|
157
|
-
out.set(row.session_id, {
|
|
158
|
-
channel: str(row.channel),
|
|
159
|
-
label: str(row.label),
|
|
160
|
-
chatId: str(row.delivery_target) ?? str(row.peer_id),
|
|
161
|
-
chatType: str(row.kind),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** List every transcript-bearing session in all per-agent databases. */
|
|
168
|
-
export async function listOpenClawSqliteSessions(
|
|
169
|
-
home: string,
|
|
170
|
-
agentIds: string[],
|
|
171
|
-
): Promise<OpenClawSqliteSession[]> {
|
|
172
|
-
const sessions: OpenClawSqliteSession[] = [];
|
|
173
|
-
for (const { agentId, dbPath } of listOpenClawDatabases(home, agentIds)) {
|
|
174
|
-
const db = await openSqliteReadonly(dbPath);
|
|
175
|
-
if (!db) continue;
|
|
176
|
-
try {
|
|
177
|
-
let rows: WindowRow[];
|
|
178
|
-
try {
|
|
179
|
-
rows = db.query<WindowRow>(
|
|
180
|
-
`SELECT w.session_id, w.session_key, w.updated_at,
|
|
181
|
-
w.transcript_updated_at, w.ended_at, w.channel,
|
|
182
|
-
w.chat_type, w.display_name,
|
|
183
|
-
COALESCE((SELECT MAX(e.created_at)
|
|
184
|
-
FROM transcript_events e
|
|
185
|
-
WHERE e.session_id = w.session_id), 0) AS event_updated_at,
|
|
186
|
-
COALESCE((SELECT SUM(length(e.event_json) + 1)
|
|
187
|
-
FROM transcript_events e
|
|
188
|
-
WHERE e.session_id = w.session_id), 0) AS size_bytes
|
|
189
|
-
FROM session_windows w
|
|
190
|
-
WHERE EXISTS (SELECT 1 FROM transcript_events e
|
|
191
|
-
WHERE e.session_id = w.session_id)`,
|
|
192
|
-
);
|
|
193
|
-
} catch {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const nodes = readNodeMetadata(db);
|
|
198
|
-
const conversations = readConversationMetadata(db);
|
|
199
|
-
for (const row of rows) {
|
|
200
|
-
const node = nodes.get(row.session_id);
|
|
201
|
-
const conversation = conversations.get(row.session_id);
|
|
202
|
-
sessions.push({
|
|
203
|
-
sessionId: row.session_id,
|
|
204
|
-
agentId,
|
|
205
|
-
dbPath,
|
|
206
|
-
transcriptPath: syntheticPath(agentId, row.session_id),
|
|
207
|
-
mtimeMs: Math.max(
|
|
208
|
-
Number(row.updated_at) || 0,
|
|
209
|
-
Number(row.transcript_updated_at) || 0,
|
|
210
|
-
Number(row.ended_at) || 0,
|
|
211
|
-
Number(row.event_updated_at) || 0,
|
|
212
|
-
),
|
|
213
|
-
sizeBytes: Number(row.size_bytes) || 0,
|
|
214
|
-
channel:
|
|
215
|
-
str(row.channel) ??
|
|
216
|
-
conversation?.channel ??
|
|
217
|
-
node?.channel ??
|
|
218
|
-
"local",
|
|
219
|
-
label: str(row.display_name) ?? conversation?.label ?? node?.label,
|
|
220
|
-
chatId: conversation?.chatId ?? node?.chatId,
|
|
221
|
-
chatType:
|
|
222
|
-
str(row.chat_type) ?? conversation?.chatType ?? node?.chatType,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
} finally {
|
|
226
|
-
db.close();
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return sessions;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Read one SQLite transcript by UUID. The first matching agent wins. */
|
|
233
|
-
export async function readOpenClawSqliteTranscript(
|
|
234
|
-
home: string,
|
|
235
|
-
agentIds: string[],
|
|
236
|
-
sessionId: string,
|
|
237
|
-
): Promise<OpenClawSqliteTranscript | null> {
|
|
238
|
-
for (const { agentId, dbPath } of listOpenClawDatabases(home, agentIds)) {
|
|
239
|
-
const db = await openSqliteReadonly(dbPath);
|
|
240
|
-
if (!db) continue;
|
|
241
|
-
try {
|
|
242
|
-
let rows: EventRow[];
|
|
243
|
-
try {
|
|
244
|
-
rows = db.query<EventRow>(
|
|
245
|
-
`SELECT event_json
|
|
246
|
-
FROM transcript_events
|
|
247
|
-
WHERE session_id = ?
|
|
248
|
-
ORDER BY seq ASC`,
|
|
249
|
-
[sessionId],
|
|
250
|
-
);
|
|
251
|
-
} catch {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (rows.length === 0) continue;
|
|
255
|
-
const eventJsonLines = rows.map((row) => row.event_json);
|
|
256
|
-
const rawLines = eventJsonLines
|
|
257
|
-
.map(parseObject)
|
|
258
|
-
.filter((line): line is Record<string, unknown> => line !== undefined);
|
|
259
|
-
return {
|
|
260
|
-
agentId,
|
|
261
|
-
dbPath,
|
|
262
|
-
filePath: syntheticPath(agentId, sessionId),
|
|
263
|
-
rawLines,
|
|
264
|
-
eventJsonLines,
|
|
265
|
-
};
|
|
266
|
-
} finally {
|
|
267
|
-
db.close();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A desktop notification on Linux, from a process with no desktop.
|
|
3
|
-
*
|
|
4
|
-
* The scheduled audit runs as a child of a SYSTEM-scope service. Its
|
|
5
|
-
* environment was measured on a real machine and carries no
|
|
6
|
-
* `DBUS_SESSION_BUS_ADDRESS`, no `DISPLAY`, no `WAYLAND_DISPLAY` and no
|
|
7
|
-
* `XDG_RUNTIME_DIR` — the daemon starts at boot, before anyone logs in, which
|
|
8
|
-
* is the whole reason it survives logout.
|
|
9
|
-
*
|
|
10
|
-
* That turns out not to be a permission problem. `/run/user/<uid>/bus` exists
|
|
11
|
-
* whenever the user is logged in, the daemon already runs as that uid, and
|
|
12
|
-
* D-Bus's EXTERNAL auth is `SO_PEERCRED` — the kernel vouches for the uid, so
|
|
13
|
-
* there is no cookie, no session membership and no environment to inherit. The
|
|
14
|
-
* address just has to be CONSTRUCTED rather than read. Proven on a real bus
|
|
15
|
-
* with a fully scrubbed environment: `AUTH … OK` → accepted.
|
|
16
|
-
*
|
|
17
|
-
* ## Awaiting the reply is the entire correctness story
|
|
18
|
-
*
|
|
19
|
-
* Measured: a call sent with NO_REPLY_EXPECTED against a bus with no
|
|
20
|
-
* notification server returns exit 0, empty stdout, empty stderr — the
|
|
21
|
-
* notification silently evaporates. The same call awaiting its reply returns a
|
|
22
|
-
* named error. Ship the fire-and-forget variant and you get exactly the failure
|
|
23
|
-
* this feature exists to avoid: believing the user was told.
|
|
24
|
-
*
|
|
25
|
-
* With the reply awaited every failure is loud and fast (10-16ms measured):
|
|
26
|
-
* logged out → ENOENT on connect, stale socket → ECONNREFUSED, no desktop →
|
|
27
|
-
* `org.freedesktop.DBus.Error.ServiceUnknown`.
|
|
28
|
-
*
|
|
29
|
-
* One failure is NOT detectable from here and must not be papered over: on a
|
|
30
|
-
* locked screen the shell still owns the name and still returns an id, while
|
|
31
|
-
* the user sees nothing. `Notify` succeeding means the server accepted it,
|
|
32
|
-
* never that a human saw it — which is why the queue, not the toast, is the
|
|
33
|
-
* record of a finding.
|
|
34
|
-
*
|
|
35
|
-
* No dependency: the wire protocol is written by hand rather than pulling a
|
|
36
|
-
* D-Bus client into a package that installs into other people's machines.
|
|
37
|
-
*/
|
|
38
|
-
import { connect, type Socket } from "node:net";
|
|
39
|
-
|
|
40
|
-
/** Milliseconds allowed for the whole exchange. Generous — a live bus answers
|
|
41
|
-
* in ~10ms, and this runs off the hook path entirely. */
|
|
42
|
-
const TIMEOUT_MS = 2_000;
|
|
43
|
-
|
|
44
|
-
export type NotifyOutcome =
|
|
45
|
-
| { ok: true; id: number }
|
|
46
|
-
| { ok: false; reason: "no-session" | "no-server" | "refused" | "timeout" | "error"; detail: string };
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A D-Bus marshaller that knows where it is.
|
|
50
|
-
*
|
|
51
|
-
* Every alignment rule in the wire format is stated relative to the START OF
|
|
52
|
-
* THE MESSAGE, which is why this tracks an offset instead of padding buffers in
|
|
53
|
-
* isolation. The first version of this file did the latter — each helper padded
|
|
54
|
-
* its own result to the boundary it needed — and it produced a header-fields
|
|
55
|
-
* array two bytes too long, because the padding after the LAST element belongs
|
|
56
|
-
* to the message rather than to the array. Real `dbus-daemon` dropped the
|
|
57
|
-
* connection; the hand-written test server did not, because it mirrored the
|
|
58
|
-
* same assumption the encoder made. The byte diff against a real client's
|
|
59
|
-
* `Hello` is what settled it (0x70 vs 0x6e), and it is why the tests below now
|
|
60
|
-
* run against a real bus.
|
|
61
|
-
*/
|
|
62
|
-
class Marshal {
|
|
63
|
-
private chunks: Buffer[] = [];
|
|
64
|
-
private len = 0;
|
|
65
|
-
|
|
66
|
-
/** Pad to the next `n`-byte boundary, counted from the start of the message. */
|
|
67
|
-
align(n: number): void {
|
|
68
|
-
const pad = (n - (this.len % n)) % n;
|
|
69
|
-
if (pad) this.raw(Buffer.alloc(pad));
|
|
70
|
-
}
|
|
71
|
-
raw(b: Buffer): void {
|
|
72
|
-
this.chunks.push(b);
|
|
73
|
-
this.len += b.length;
|
|
74
|
-
}
|
|
75
|
-
byte(v: number): void {
|
|
76
|
-
this.raw(Buffer.from([v]));
|
|
77
|
-
}
|
|
78
|
-
u32(v: number): void {
|
|
79
|
-
this.align(4);
|
|
80
|
-
const b = Buffer.alloc(4);
|
|
81
|
-
b.writeUInt32LE(v, 0);
|
|
82
|
-
this.raw(b);
|
|
83
|
-
}
|
|
84
|
-
i32(v: number): void {
|
|
85
|
-
this.align(4);
|
|
86
|
-
const b = Buffer.alloc(4);
|
|
87
|
-
b.writeInt32LE(v, 0);
|
|
88
|
-
this.raw(b);
|
|
89
|
-
}
|
|
90
|
-
/** STRING / OBJECT_PATH: 4-aligned length, bytes, NUL. */
|
|
91
|
-
str(v: string): void {
|
|
92
|
-
const b = Buffer.from(v, "utf8");
|
|
93
|
-
this.u32(b.length);
|
|
94
|
-
this.raw(b);
|
|
95
|
-
this.byte(0);
|
|
96
|
-
}
|
|
97
|
-
/** SIGNATURE: a single length BYTE, bytes, NUL. No alignment. */
|
|
98
|
-
sig(v: string): void {
|
|
99
|
-
const b = Buffer.from(v, "utf8");
|
|
100
|
-
this.byte(b.length);
|
|
101
|
-
this.raw(b);
|
|
102
|
-
this.byte(0);
|
|
103
|
-
}
|
|
104
|
-
get length(): number {
|
|
105
|
-
return this.len;
|
|
106
|
-
}
|
|
107
|
-
buffer(): Buffer {
|
|
108
|
-
return Buffer.concat(this.chunks);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** One header field: a STRUCT of (BYTE code, VARIANT value), 8-aligned. */
|
|
113
|
-
type HeaderField = readonly [code: number, signature: string, value: string];
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Encode the header-fields array.
|
|
117
|
-
*
|
|
118
|
-
* Returns the declared length SEPARATELY from the bytes, because they are not
|
|
119
|
-
* the same number: the length stops at the end of the last element's content,
|
|
120
|
-
* while the message then pads to 8 before the body. Conflating them is the bug
|
|
121
|
-
* described on `Marshal`.
|
|
122
|
-
*/
|
|
123
|
-
function headerFields(fields: readonly HeaderField[]): { data: Buffer; declaredLength: number } {
|
|
124
|
-
// The array's data starts at message offset 16, which is already 8-aligned,
|
|
125
|
-
// so offsets inside this marshaller line up with the real message.
|
|
126
|
-
const m = new Marshal();
|
|
127
|
-
for (const [code, signature, value] of fields) {
|
|
128
|
-
m.align(8); // each STRUCT element starts on an 8-byte boundary
|
|
129
|
-
m.byte(code);
|
|
130
|
-
m.sig(signature);
|
|
131
|
-
// Every header field we send is a string-like: OBJECT_PATH, STRING or
|
|
132
|
-
// SIGNATURE. Nothing here needs a numeric variant.
|
|
133
|
-
if (signature === "g") m.sig(value);
|
|
134
|
-
else m.str(value);
|
|
135
|
-
}
|
|
136
|
-
// No trailing padding was added, so the buffer length IS the declared length.
|
|
137
|
-
return { data: m.buffer(), declaredLength: m.length };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** A METHOD_CALL: fixed header, header fields, 8-byte pad, then the body. */
|
|
141
|
-
function methodCall(serial: number, fields: readonly HeaderField[], body: Buffer): Buffer {
|
|
142
|
-
const { data, declaredLength } = headerFields(fields);
|
|
143
|
-
const head = new Marshal();
|
|
144
|
-
head.raw(Buffer.from([0x6c, 1, 0, 1])); // little-endian, METHOD_CALL, no flags, proto 1
|
|
145
|
-
head.u32(body.length);
|
|
146
|
-
head.u32(serial);
|
|
147
|
-
head.u32(declaredLength);
|
|
148
|
-
head.raw(data);
|
|
149
|
-
head.align(8); // the body always starts on an 8-byte boundary
|
|
150
|
-
return Buffer.concat([head.buffer(), body]);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The arguments of `org.freedesktop.Notifications.Notify`.
|
|
155
|
-
*
|
|
156
|
-
* Signature `susssasa{sv}i`: app_name, replaces_id, app_icon, summary, body,
|
|
157
|
-
* actions, hints, expire_timeout.
|
|
158
|
-
*/
|
|
159
|
-
function notifyBody(app: string, replacesId: number, summary: string, body: string): Buffer {
|
|
160
|
-
const m = new Marshal();
|
|
161
|
-
m.str(app);
|
|
162
|
-
m.u32(replacesId);
|
|
163
|
-
m.str(""); // app_icon — none; the app name is what identifies us
|
|
164
|
-
m.str(summary);
|
|
165
|
-
m.str(body);
|
|
166
|
-
m.u32(0); // actions: `as`, empty. Element alignment 4, already met.
|
|
167
|
-
m.u32(0); // hints: `a{sv}`, empty...
|
|
168
|
-
m.align(8); // ...and an array pads to its ELEMENT alignment even when
|
|
169
|
-
// empty. DICT_ENTRY is 8, so this padding is required and
|
|
170
|
-
// counts toward the body length.
|
|
171
|
-
m.i32(-1); // expire_timeout: -1 lets the server decide
|
|
172
|
-
return m.buffer();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** `org.freedesktop.DBus.Hello` — mandatory before any other method call. */
|
|
176
|
-
const HELLO_FIELDS: readonly HeaderField[] = [
|
|
177
|
-
[1, "o", "/org/freedesktop/DBus"],
|
|
178
|
-
[6, "s", "org.freedesktop.DBus"],
|
|
179
|
-
[2, "s", "org.freedesktop.DBus"],
|
|
180
|
-
[3, "s", "Hello"],
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
const NOTIFY_FIELDS: readonly HeaderField[] = [
|
|
184
|
-
[1, "o", "/org/freedesktop/Notifications"],
|
|
185
|
-
[6, "s", "org.freedesktop.Notifications"],
|
|
186
|
-
[2, "s", "org.freedesktop.Notifications"],
|
|
187
|
-
[3, "s", "Notify"],
|
|
188
|
-
[8, "g", "susssasa{sv}i"],
|
|
189
|
-
];
|
|
190
|
-
|
|
191
|
-
/** A parsed inbound message: only what the state machine dispatches on. */
|
|
192
|
-
interface Incoming {
|
|
193
|
-
type: number;
|
|
194
|
-
replySerial: number | null;
|
|
195
|
-
errorName: string | null;
|
|
196
|
-
bodyStart: number;
|
|
197
|
-
bodyLength: number;
|
|
198
|
-
total: number;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Read one whole message out of `buf`, or null when it has not all arrived.
|
|
203
|
-
*
|
|
204
|
-
* A framed reader rather than "assume one message per chunk", because a real
|
|
205
|
-
* bus does not oblige: it answers `Hello` with a METHOD_RETURN and then emits a
|
|
206
|
-
* `NameAcquired` SIGNAL, and the two can land in one chunk or two. The earlier
|
|
207
|
-
* version consumed whichever chunk came first and treated the NEXT one as its
|
|
208
|
-
* reply — which, for a SIGNAL, is neither an error nor a return, so it reported
|
|
209
|
-
* the notification as delivered with a garbage id.
|
|
210
|
-
*/
|
|
211
|
-
function readMessage(buf: Buffer): Incoming | null {
|
|
212
|
-
if (buf.length < 16) return null;
|
|
213
|
-
const bodyLength = buf.readUInt32LE(4);
|
|
214
|
-
const fieldsLength = buf.readUInt32LE(12);
|
|
215
|
-
const bodyStart = 16 + fieldsLength + ((8 - (fieldsLength % 8)) % 8);
|
|
216
|
-
const total = bodyStart + bodyLength;
|
|
217
|
-
if (buf.length < total) return null;
|
|
218
|
-
|
|
219
|
-
let replySerial: number | null = null;
|
|
220
|
-
let errorName: string | null = null;
|
|
221
|
-
// Walk the header fields far enough to find REPLY_SERIAL (5) and ERROR_NAME
|
|
222
|
-
// (4). Anything unparseable stops the walk rather than throwing: a header we
|
|
223
|
-
// cannot read is a message we ignore, not a crash on the audit's exit path.
|
|
224
|
-
let i = 16;
|
|
225
|
-
const end = 16 + fieldsLength;
|
|
226
|
-
while (i < end) {
|
|
227
|
-
i += (8 - ((i - 16) % 8)) % 8; // struct alignment, relative to the array start
|
|
228
|
-
if (i + 4 > end) break;
|
|
229
|
-
const code = buf[i];
|
|
230
|
-
const sigLen = buf[i + 1];
|
|
231
|
-
const sig = buf.toString("latin1", i + 2, i + 2 + sigLen);
|
|
232
|
-
i += 2 + sigLen + 1;
|
|
233
|
-
if (sig === "u") {
|
|
234
|
-
i += (4 - ((i - 16) % 4)) % 4;
|
|
235
|
-
if (i + 4 > end) break;
|
|
236
|
-
if (code === 5) replySerial = buf.readUInt32LE(i);
|
|
237
|
-
i += 4;
|
|
238
|
-
} else if (sig === "s" || sig === "o") {
|
|
239
|
-
i += (4 - ((i - 16) % 4)) % 4;
|
|
240
|
-
if (i + 4 > end) break;
|
|
241
|
-
const len = buf.readUInt32LE(i);
|
|
242
|
-
if (i + 4 + len > end) break;
|
|
243
|
-
if (code === 4) errorName = buf.toString("utf8", i + 4, i + 4 + len);
|
|
244
|
-
i += 4 + len + 1;
|
|
245
|
-
} else if (sig === "g") {
|
|
246
|
-
const len = buf[i];
|
|
247
|
-
i += 1 + len + 1;
|
|
248
|
-
} else {
|
|
249
|
-
break; // a type we do not send and cannot size; stop rather than guess
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return { type: buf[1], replySerial, errorName, bodyStart, bodyLength, total };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Where this process's session bus lives.
|
|
257
|
-
*
|
|
258
|
-
* Two callers with opposite environments, one rule. A human running
|
|
259
|
-
* `failproofai audit` in their terminal HAS `DBUS_SESSION_BUS_ADDRESS`, and on
|
|
260
|
-
* an unusual setup it is the only correct answer — so it wins when it is there
|
|
261
|
-
* and names a socket we can open. The scheduled audit, a child of a system
|
|
262
|
-
* service started before login, has nothing: for it the address is CONSTRUCTED
|
|
263
|
-
* from the uid, which works because D-Bus EXTERNAL auth is `SO_PEERCRED` and
|
|
264
|
-
* the kernel supplies the identity the environment could not.
|
|
265
|
-
*
|
|
266
|
-
* Only the `unix:path=` form is honored. The spec allows `unix:abstract=`,
|
|
267
|
-
* `tcp:` and semicolon-separated alternatives; a bus we cannot dial is better
|
|
268
|
-
* reported as "no session" than dialed wrong, and the fallback below already
|
|
269
|
-
* covers every desktop that ships systemd.
|
|
270
|
-
*/
|
|
271
|
-
export function resolveBusPath(env: Readonly<Record<string, string | undefined>>): string | null {
|
|
272
|
-
const declared = env.DBUS_SESSION_BUS_ADDRESS;
|
|
273
|
-
if (declared) {
|
|
274
|
-
for (const part of declared.split(";")) {
|
|
275
|
-
const m = /^unix:(?:.*,)?path=([^,]+)/.exec(part.trim());
|
|
276
|
-
if (m) return m[1];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
|
|
280
|
-
return uid >= 0 ? `/run/user/${uid}/bus` : null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Post a notification, awaiting the reply.
|
|
285
|
-
*
|
|
286
|
-
* `replacesId` reuses an earlier notification's id so repeated scans update one
|
|
287
|
-
* bubble instead of stacking a fresh alert every time — the difference between
|
|
288
|
-
* a reminder and a nag.
|
|
289
|
-
*/
|
|
290
|
-
export function notifyDesktop(
|
|
291
|
-
summary: string,
|
|
292
|
-
body: string,
|
|
293
|
-
replacesId = 0,
|
|
294
|
-
opts: { socketPath?: string } = {},
|
|
295
|
-
): Promise<NotifyOutcome> {
|
|
296
|
-
return new Promise((resolvePromise) => {
|
|
297
|
-
if (process.platform !== "linux" && !opts.socketPath) {
|
|
298
|
-
resolvePromise({ ok: false, reason: "error", detail: "linux only" });
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
|
|
302
|
-
const path = opts.socketPath ?? resolveBusPath(process.env as Record<string, string | undefined>);
|
|
303
|
-
if (!path || uid < 0) {
|
|
304
|
-
resolvePromise({ ok: false, reason: "no-session", detail: "no bus address" });
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
let settled = false;
|
|
309
|
-
const done = (o: NotifyOutcome) => {
|
|
310
|
-
if (settled) return;
|
|
311
|
-
settled = true;
|
|
312
|
-
clearTimeout(timer);
|
|
313
|
-
try { sock.destroy(); } catch { /* already gone */ }
|
|
314
|
-
resolvePromise(o);
|
|
315
|
-
};
|
|
316
|
-
const timer = setTimeout(() => done({ ok: false, reason: "timeout", detail: `${TIMEOUT_MS}ms` }), TIMEOUT_MS);
|
|
317
|
-
|
|
318
|
-
let sock: Socket;
|
|
319
|
-
try {
|
|
320
|
-
sock = connect(path);
|
|
321
|
-
} catch (err) {
|
|
322
|
-
done({ ok: false, reason: "no-session", detail: String(err) });
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let stage: "auth" | "hello" | "notify" = "auth";
|
|
327
|
-
let buf = Buffer.alloc(0);
|
|
328
|
-
|
|
329
|
-
sock.on("error", (err: NodeJS.ErrnoException) => {
|
|
330
|
-
// ENOENT: nobody is logged in, so /run/user/<uid> does not exist.
|
|
331
|
-
// ECONNREFUSED: a stale socket left behind by a dead bus.
|
|
332
|
-
done({
|
|
333
|
-
ok: false,
|
|
334
|
-
reason: err.code === "ENOENT" || err.code === "ECONNREFUSED" ? "no-session" : "error",
|
|
335
|
-
detail: err.code ?? String(err),
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
// A bus that hangs up mid-exchange is the signature of a message it could
|
|
340
|
-
// not parse. Without this the socket simply goes quiet and the whole thing
|
|
341
|
-
// reports a 2-second timeout — which points at a slow desktop rather than
|
|
342
|
-
// at us, and is exactly how the header-length bug survived its first tests.
|
|
343
|
-
sock.on("close", () => {
|
|
344
|
-
done({ ok: false, reason: "refused", detail: `bus closed the connection during ${stage}` });
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
sock.on("connect", () => {
|
|
348
|
-
// SASL EXTERNAL: the kernel already told the bus our uid over SO_PEERCRED,
|
|
349
|
-
// so the "credential" is just that uid in hex and no secret is exchanged.
|
|
350
|
-
const hexUid = Buffer.from(String(uid), "utf8").toString("hex");
|
|
351
|
-
sock.write(Buffer.concat([Buffer.from([0]), Buffer.from(`AUTH EXTERNAL ${hexUid}\r\n`, "utf8")]));
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
sock.on("data", (chunk: Buffer) => {
|
|
355
|
-
buf = Buffer.concat([buf, chunk]);
|
|
356
|
-
|
|
357
|
-
if (stage === "auth") {
|
|
358
|
-
const text = buf.toString("latin1");
|
|
359
|
-
const nl = text.indexOf("\r\n");
|
|
360
|
-
if (nl === -1) return;
|
|
361
|
-
if (!text.startsWith("OK")) {
|
|
362
|
-
done({ ok: false, reason: "refused", detail: text.slice(0, nl).trim().slice(0, 80) });
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
// Anything after the OK line is already binary protocol — keep it.
|
|
366
|
-
buf = buf.subarray(nl + 2);
|
|
367
|
-
stage = "hello";
|
|
368
|
-
sock.write(Buffer.from("BEGIN\r\n", "utf8"));
|
|
369
|
-
sock.write(methodCall(1, HELLO_FIELDS, Buffer.alloc(0)));
|
|
370
|
-
// Fall through: the reply may already be in `buf`.
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Consume whole messages only. The bus answers Hello and then emits a
|
|
374
|
-
// NameAcquired SIGNAL; both can arrive in one chunk, and neither is
|
|
375
|
-
// guaranteed to arrive whole.
|
|
376
|
-
for (;;) {
|
|
377
|
-
const msg = readMessage(buf);
|
|
378
|
-
if (!msg) return;
|
|
379
|
-
const frame = buf.subarray(0, msg.total);
|
|
380
|
-
buf = buf.subarray(msg.total);
|
|
381
|
-
|
|
382
|
-
// 1 = METHOD_CALL, 2 = METHOD_RETURN, 3 = ERROR, 4 = SIGNAL.
|
|
383
|
-
if (msg.type === 4 || msg.type === 1) continue; // NameAcquired and friends
|
|
384
|
-
|
|
385
|
-
if (stage === "hello") {
|
|
386
|
-
if (msg.type === 3) {
|
|
387
|
-
done({ ok: false, reason: "refused", detail: msg.errorName ?? "Hello failed" });
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
if (msg.replySerial !== 1) continue;
|
|
391
|
-
stage = "notify";
|
|
392
|
-
sock.write(methodCall(2, NOTIFY_FIELDS, notifyBody("failproofai", replacesId, summary, body)));
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Only OUR call's reply settles this. Matching on serial rather than on
|
|
397
|
-
// "the next message" is what keeps an unrelated signal from being read
|
|
398
|
-
// as a successful delivery.
|
|
399
|
-
if (msg.replySerial !== 2) continue;
|
|
400
|
-
|
|
401
|
-
if (msg.type === 3) {
|
|
402
|
-
const name = msg.errorName ?? "";
|
|
403
|
-
// The common one: logged in, but nothing is drawing notifications.
|
|
404
|
-
const noServer = name.includes("ServiceUnknown") || name.includes("NameHasNoOwner");
|
|
405
|
-
done({ ok: false, reason: noServer ? "no-server" : "error", detail: name || "error" });
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// METHOD_RETURN. The body is a single UINT32 id — read it back so a
|
|
410
|
-
// future call could replace this bubble rather than stack a new one.
|
|
411
|
-
const id =
|
|
412
|
-
msg.bodyLength >= 4 && frame.length >= msg.bodyStart + 4
|
|
413
|
-
? frame.readUInt32LE(msg.bodyStart)
|
|
414
|
-
: 0;
|
|
415
|
-
done({ ok: true, id });
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
}
|