failproofai 1.0.0-beta.12 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -3
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -3
- 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 +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 +2 -2
- 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 +3 -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 +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +2 -3
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -3
- 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 +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +2 -3
- 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 +3 -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/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 +3 -4
- 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 +3 -4
- 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 +2 -3
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1a7i2q4._.js → [root-of-the-server]__00ni9za._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_ps0jw._.js → [root-of-the-server]__0effm9m._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0lk_935._.js → [root-of-the-server]__0ix8gm7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0ky_n0r._.js → [root-of-the-server]__0ji11a-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0v2fpxe._.js → [root-of-the-server]__0ojwjxl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -3
- 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]__1rdruw4._.js → [root-of-the-server]__0_1tiya._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ha3gul._.js → [root-of-the-server]__0eeipso._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tyf0tc._.js → [root-of-the-server]__17itmx-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1io8n-9._.js → [root-of-the-server]__1dx-a-c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yw368c._.js → [root-of-the-server]__1n2vcv4._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1a2hcb_._.js → [root-of-the-server]__1tlvv9a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fahg9e._.js → _0vo2qyt._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0uz7o9y._.js → _0xej-0e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_04mekdy._.js → _10399ge._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0se484m._.js → _149k9oy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1vsgpqt._.js → _166jvu8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0r-dsre._.js → _1_hwp06._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w8sola._.js → _1mnhi_r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w_6p9g._.js → _1ror7k9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gmzkwj._.js → _1sdgprl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/{2_h--2ame3r83.js → 01s3vpzky_qew.js} +1 -1
- package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
- package/.next/standalone/.next/static/chunks/{23hp7hekegpe5.js → 0zbcd_n8ku2ov.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0cakk77ppmlo7.js → 1erji0ratofla.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40-zk44qsg5y-.js → 23hnlzqx44vbe.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1wb8cnmzx69rz.js → 2cwsiyhc7d2cw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_rdbv_k5j51p.js → 2y42ebzynyxdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1spk1bub0vd-h.js → 315lncj7dsp9c.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0-zh1umoq6ppc.js → 31_px0dfgdg9j.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2bie7ajv70p9b.js → 3l4-fu3xt75j8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{298v78ypf8edl.js → 3poscoxn69x9g.js} +1 -1
- package/.next/standalone/app/policies/hooks-client.tsx +4 -4
- package/.next/standalone/ino.ujh3/.failproofai/VERSION +4 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/last-version +1 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +1 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +186 -5
- package/dist/cli.mjs +1130 -1352
- package/dist/worker.mjs +107 -1000
- package/package.json +10 -10
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/cloud-connection.ts +4 -4
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-enrollment.ts +3 -3
- package/src/hooks/cloud-managed-policies.ts +60 -22
- package/src/hooks/configure-wizard.ts +124 -53
- package/src/hooks/custom-hooks-loader.ts +57 -8
- package/src/hooks/daemon-service.ts +66 -11
- package/src/hooks/flush-cli.ts +180 -0
- package/src/hooks/fp-config.ts +242 -118
- package/src/hooks/fp-home.ts +138 -22
- package/src/hooks/fp-reset.ts +357 -43
- package/src/hooks/handler.ts +11 -11
- package/src/hooks/harness-cli.ts +309 -0
- package/src/hooks/hook-activity-store.ts +5 -5
- package/src/hooks/manager.ts +35 -0
- package/src/hooks/onboarding-attempt.ts +22 -6
- package/src/hooks/tui.ts +64 -7
- package/src/hooks/uninstall-cli.ts +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +0 -5
- package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +0 -2
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{-3gDrXYxNRTVwrFnqXAWd → ziAsXfNE0cH_S7LHK-Py3}/_ssgManifest.js +0 -0
|
@@ -358,9 +358,43 @@ export async function loadAllCustomHooks(
|
|
|
358
358
|
// twice per event. Seeded by the explicit path below and consulted by both
|
|
359
359
|
// convention passes.
|
|
360
360
|
const loadedPaths = new Set<string>();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
|
|
362
|
+
// Cloud policies keyed by the artifact they resolve to — and artifacts are
|
|
363
|
+
// CONTENT-ADDRESSED, so two policies whose source is byte-identical share one
|
|
364
|
+
// path. Building this map with a plain `new Map(entries)` let the last one win
|
|
365
|
+
// silently, and since `loadedPaths` then loads that file exactly once, the
|
|
366
|
+
// other policy vanished: its id never appeared in `failproofai policies` or in
|
|
367
|
+
// the decision log, and — the part that matters — ITS EFFECT DECIDED NOTHING.
|
|
368
|
+
// An `enforce` policy sharing bytes with an `observe` one was downgraded to
|
|
369
|
+
// observation, which is a silent enforcement gap, and the reverse turned a
|
|
370
|
+
// measurement-only rollout into live denials.
|
|
371
|
+
//
|
|
372
|
+
// It could not happen before the flattening: `deployments/<n>/<id>.mjs` gave
|
|
373
|
+
// every policy its own path even when the bytes matched.
|
|
374
|
+
//
|
|
375
|
+
// One module instance is right — `customPolicies.add` is an unconditional
|
|
376
|
+
// push, so importing the file twice would register every hook twice and run
|
|
377
|
+
// the policy twice per event. So the collision is resolved rather than
|
|
378
|
+
// avoided, and it resolves toward ENFORCEMENT: over-enforcing is visible to
|
|
379
|
+
// whoever hits it, while under-enforcing is exactly the silent failure this
|
|
380
|
+
// codebase exists to remove. It is also announced, so an operator can give the
|
|
381
|
+
// two policies distinguishable source instead of living with the merge.
|
|
382
|
+
const cloudManagedByPath = new Map<string, CloudManagedPolicyArtifact>();
|
|
383
|
+
for (const policy of opts?.cloudManagedPolicies ?? []) {
|
|
384
|
+
const key = resolve(policy.path);
|
|
385
|
+
const existing = cloudManagedByPath.get(key);
|
|
386
|
+
if (!existing) {
|
|
387
|
+
cloudManagedByPath.set(key, policy);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
hookLogWarn(
|
|
391
|
+
`cloud-managed policies ${existing.id} and ${policy.id} have identical source, so they share ` +
|
|
392
|
+
`one artifact and load as one policy; enforcing it if either asks to enforce`,
|
|
393
|
+
);
|
|
394
|
+
if (existing.effect !== "enforce" && policy.effect === "enforce") {
|
|
395
|
+
cloudManagedByPath.set(key, policy);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
364
398
|
|
|
365
399
|
// 1. Explicit custom policy paths. Accept a string for callers/configs using
|
|
366
400
|
// the legacy singular form.
|
|
@@ -387,7 +421,7 @@ export async function loadAllCustomHooks(
|
|
|
387
421
|
};
|
|
388
422
|
if (cloudManaged) {
|
|
389
423
|
tagged.__cloudManaged = cloudManaged;
|
|
390
|
-
tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.
|
|
424
|
+
tagged.__policyId = `cloud:${cloudManaged.id}@${cloudManaged.version}:${hook.name}`;
|
|
391
425
|
} else {
|
|
392
426
|
tagged.__policyId = customPolicyId(absPath, hook.name);
|
|
393
427
|
}
|
|
@@ -415,9 +449,24 @@ export async function loadAllCustomHooks(
|
|
|
415
449
|
// leaks one file permanently rather than leaving one that the next load
|
|
416
450
|
// overwrites. Best-effort and age-gated, so it can never remove a tree
|
|
417
451
|
// another process is still importing.
|
|
452
|
+
// WHICH SCOPE this directory is depends on whether it IS the user directory.
|
|
453
|
+
// Run an agent CLI with the cwd set to $HOME and `<cwd>/.failproofai/policies`
|
|
454
|
+
// resolves to `~/.failproofai/policies` — the user scope itself. Layout 3 made
|
|
455
|
+
// that reachable by collapsing `customPoliciesDir()` onto `policiesDir()`;
|
|
456
|
+
// layout 2 nested the user directory one level deeper, so the two could never
|
|
457
|
+
// be the same path.
|
|
458
|
+
//
|
|
459
|
+
// Tagging it `project` there would make a policy's id depend on where the
|
|
460
|
+
// agent happened to be launched from — `convention:project:x-policies.mjs:foo`
|
|
461
|
+
// from $HOME and `convention:user:x-policies.mjs:foo` from anywhere else — and
|
|
462
|
+
// a disable is RECORDED AGAINST THAT ID. So a user who switched a policy off
|
|
463
|
+
// would find it firing again whenever they worked out of their home
|
|
464
|
+
// directory, with nothing to explain it. The files are the user's global
|
|
465
|
+
// policies; that the cwd also points at them is incidental.
|
|
466
|
+
const projectScope: "project" | "user" = projectDir === customPoliciesDir() ? "user" : "project";
|
|
418
467
|
if (conventionEnabled) {
|
|
419
468
|
void sweepStaleTmpArtifacts(projectDir);
|
|
420
|
-
warnSkippedPolicyFiles(projectDir,
|
|
469
|
+
warnSkippedPolicyFiles(projectDir, projectScope);
|
|
421
470
|
}
|
|
422
471
|
// The shim directory leaks the same way and is swept on the same terms. It is
|
|
423
472
|
// ours alone and outside the policy tree, so nothing else would ever reap it;
|
|
@@ -430,14 +479,14 @@ export async function loadAllCustomHooks(
|
|
|
430
479
|
for (const file of projectFiles) {
|
|
431
480
|
loadedPaths.add(file);
|
|
432
481
|
const hooksBefore = getCustomHooks().length;
|
|
433
|
-
await loadSingleFile(file, { conventionScope:
|
|
482
|
+
await loadSingleFile(file, { conventionScope: projectScope });
|
|
434
483
|
const newHooks = getCustomHooks().slice(hooksBefore);
|
|
435
484
|
for (const hook of newHooks) {
|
|
436
|
-
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(
|
|
485
|
+
(hook as CustomHook & { __policyId?: string }).__policyId = conventionPolicyId(projectScope, basename(file), hook.name);
|
|
437
486
|
}
|
|
438
487
|
if (newHooks.length > 0) {
|
|
439
488
|
conventionSources.push({
|
|
440
|
-
scope:
|
|
489
|
+
scope: projectScope,
|
|
441
490
|
file: basename(file),
|
|
442
491
|
hookNames: newHooks.map((h) => h.name),
|
|
443
492
|
});
|
|
@@ -1036,25 +1036,80 @@ export async function probeDaemon(): Promise<DaemonProbe> {
|
|
|
1036
1036
|
}
|
|
1037
1037
|
}
|
|
1038
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* "Is anything listening on the daemon socket?" — never throws.
|
|
1041
|
+
*
|
|
1042
|
+
* Split out because `waitForDaemonRunning` needs the connect check WITHOUT the
|
|
1043
|
+
* hook evaluation: it runs inside a settle loop, and a failed import or a
|
|
1044
|
+
* transient refusal there must read as "not yet", never as an error.
|
|
1045
|
+
*/
|
|
1046
|
+
async function daemonAcceptsConnectionsQuietly(): Promise<boolean> {
|
|
1047
|
+
try {
|
|
1048
|
+
const { daemonAcceptsConnections } = await import("./daemon-client");
|
|
1049
|
+
return await daemonAcceptsConnections();
|
|
1050
|
+
} catch {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1039
1055
|
/** Boolean form, for callers that only branch on healthy/not. */
|
|
1040
1056
|
export async function probeDaemonEndToEnd(): Promise<boolean> {
|
|
1041
1057
|
return (await probeDaemon()).ok;
|
|
1042
1058
|
}
|
|
1043
1059
|
|
|
1044
1060
|
/**
|
|
1045
|
-
* Waits for the service to report running
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1061
|
+
* Waits for the service to report running and to HOLD it — a `Type=simple` unit
|
|
1062
|
+
* is active the moment it forks, so one optimistic reading passes a daemon that
|
|
1063
|
+
* died at startup.
|
|
1064
|
+
*
|
|
1065
|
+
* WATCHED, not slept through. This used to sleep `SERVICE_SETTLE_MS` blind and
|
|
1066
|
+
* read the status once at the end, which was wrong in both directions on a
|
|
1067
|
+
* healthy machine and a broken one:
|
|
1068
|
+
*
|
|
1069
|
+
* • Healthy: the socket is up in ~13ms and answers a real hook in ~125ms, and
|
|
1070
|
+
* setup still sat there for the remaining ~600ms with the answer already in
|
|
1071
|
+
* hand. Setup runs this twice on the repair path (uninstall, reinstall), so
|
|
1072
|
+
* it was over a second of dead wait every time.
|
|
1073
|
+
* • Broken: a daemon that died at 100ms was not noticed until 750ms, because
|
|
1074
|
+
* nothing looked until the sleep was over.
|
|
1075
|
+
*
|
|
1076
|
+
* Now the window is polled. Leaving `running` at any point fails immediately,
|
|
1077
|
+
* and the wait ends early once the daemon has answered a real hook — a reply is
|
|
1078
|
+
* strictly stronger evidence of "did not die at startup" than "still active
|
|
1079
|
+
* after an arbitrary sleep", which is all the settle ever established.
|
|
1048
1080
|
*/
|
|
1049
|
-
|
|
1050
|
-
|
|
1081
|
+
export interface WaitForDaemonDeps {
|
|
1082
|
+
status?: () => DaemonServiceStatus;
|
|
1083
|
+
accepts?: () => Promise<boolean>;
|
|
1084
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1085
|
+
now?: () => number;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export async function waitForDaemonRunning(deps: WaitForDaemonDeps = {}): Promise<boolean> {
|
|
1089
|
+
const status = deps.status ?? daemonServiceStatus;
|
|
1090
|
+
const accepts = deps.accepts ?? daemonAcceptsConnectionsQuietly;
|
|
1091
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
1092
|
+
const now = deps.now ?? Date.now;
|
|
1093
|
+
|
|
1094
|
+
const deadline = now() + SERVICE_START_TIMEOUT_MS;
|
|
1095
|
+
for (;;) {
|
|
1096
|
+
if (status() === "running") break;
|
|
1097
|
+
if (now() >= deadline) return false;
|
|
1098
|
+
await sleep(SERVICE_START_POLL_MS);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const settleUntil = now() + SERVICE_SETTLE_MS;
|
|
1051
1102
|
for (;;) {
|
|
1052
|
-
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
1103
|
+
// A unit that has left `running` is dead now; there is nothing to wait out.
|
|
1104
|
+
if (status() !== "running") return false;
|
|
1105
|
+
// A daemon that accepted a connection is a daemon that got past startup and
|
|
1106
|
+
// bound its socket. Deliberately the CHEAP check, not a full hook
|
|
1107
|
+
// evaluation: `probeDaemon` runs the end-to-end one moments later, and
|
|
1108
|
+
// paying for it twice is what this rewrite exists to stop.
|
|
1109
|
+
if (await accepts()) return true;
|
|
1110
|
+
if (now() >= settleUntil) return true;
|
|
1111
|
+
await sleep(SERVICE_START_POLL_MS);
|
|
1055
1112
|
}
|
|
1056
|
-
await new Promise((r) => setTimeout(r, SERVICE_SETTLE_MS));
|
|
1057
|
-
return daemonServiceStatus() === "running";
|
|
1058
1113
|
}
|
|
1059
1114
|
|
|
1060
1115
|
// ── Upgrading a service definition that predates a variable ──────────────────
|
|
@@ -1125,7 +1180,7 @@ function installedExecStart(definition: string): string | null {
|
|
|
1125
1180
|
* True when a service IS installed but its definition predates
|
|
1126
1181
|
* `FAILPROOFAI_CLI_CMD`.
|
|
1127
1182
|
*
|
|
1128
|
-
* Deliberately a content check against the definition on disk, not a
|
|
1183
|
+
* Deliberately a content check against the definition on disk, not a version
|
|
1129
1184
|
* number mirrored into `config.toml`. The mirror is the tempting shape — it is
|
|
1130
1185
|
* how `daemon.installed_version` is modelled — but that field is declared,
|
|
1131
1186
|
* read and cleared and has never once been WRITTEN (both `setDaemonConfigured`
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `failproofai flush` — deliver what is already spooled, now.
|
|
3
|
+
*
|
|
4
|
+
* The collector is built to be unhurried on purpose. A batch is swept only once
|
|
5
|
+
* it is older than two minutes, at most 64 per pass, on a 60-second cadence —
|
|
6
|
+
* pacing that keeps a backlog from stampeding a server and is exactly wrong for
|
|
7
|
+
* one case: somebody standing at a dashboard waiting to see their own events.
|
|
8
|
+
* They have no way to tell "not delivered yet" from "not working", and the
|
|
9
|
+
* honest answer is usually the first one.
|
|
10
|
+
*
|
|
11
|
+
* WHY A REQUEST FILE, not the work itself. The uploader's concurrency limiter
|
|
12
|
+
* and in-flight set live in the running daemon. A second uploader started by
|
|
13
|
+
* the CLI would share neither, so both would POST the same batch — the exact
|
|
14
|
+
* double-delivery `delivery.rs` keeps one shared `Delivery` to prevent. So this
|
|
15
|
+
* writes a request the daemon drains on its next tick.
|
|
16
|
+
*
|
|
17
|
+
* What it does NOT hand off is the checking — same rule as `backfill-cli.ts`.
|
|
18
|
+
* Every precondition a person can get wrong is verified here, synchronously,
|
|
19
|
+
* before returning success. A CLI that prints "requested" while the daemon is
|
|
20
|
+
* stopped has told the user the opposite of what happened.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { failproofaiHome, spoolDir } from "./fp-home";
|
|
25
|
+
import { readConfig } from "./fp-config";
|
|
26
|
+
import { readIngestCredential } from "./collector-config";
|
|
27
|
+
import { daemonServiceStatus, isDaemonSupportedPlatform } from "./daemon-service";
|
|
28
|
+
|
|
29
|
+
/** How long `--wait` blocks by default. */
|
|
30
|
+
export const DEFAULT_FLUSH_TIMEOUT_SECS = 60;
|
|
31
|
+
|
|
32
|
+
export interface FlushOptions {
|
|
33
|
+
wait?: boolean;
|
|
34
|
+
timeoutSecs?: number;
|
|
35
|
+
home?: string;
|
|
36
|
+
/** Injected in tests so the wait loop does not really sleep. */
|
|
37
|
+
sleep?: (ms: number) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FlushResult {
|
|
41
|
+
exitCode: number;
|
|
42
|
+
lines: string[];
|
|
43
|
+
/** Batches still spooled when we stopped looking. */
|
|
44
|
+
pending: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Mirrored from the daemon's `paths::flush_request_path()`. */
|
|
48
|
+
export function flushRequestPath(home?: string): string {
|
|
49
|
+
return join(failproofaiHome(home), "state", "flush-request.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every directory the collector spools batches into. */
|
|
53
|
+
function spoolDirs(home?: string): string[] {
|
|
54
|
+
const root = spoolDir(home);
|
|
55
|
+
if (!existsSync(root)) return [];
|
|
56
|
+
try {
|
|
57
|
+
return readdirSync(root, { withFileTypes: true })
|
|
58
|
+
.filter((e) => e.isDirectory() && e.name !== "failed")
|
|
59
|
+
.map((e) => join(root, e.name));
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Batches awaiting delivery. `.tmp` files are half-written and not counted. */
|
|
66
|
+
export function pendingBatches(home?: string): number {
|
|
67
|
+
let n = 0;
|
|
68
|
+
for (const dir of spoolDirs(home)) {
|
|
69
|
+
try {
|
|
70
|
+
n += readdirSync(dir).filter((f) => f.endsWith(".jsonl")).length;
|
|
71
|
+
} catch {
|
|
72
|
+
// A directory that vanished mid-scan is one the collector just drained.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runFlushCommand(opts: FlushOptions = {}): Promise<FlushResult> {
|
|
79
|
+
const { home, wait = false } = opts;
|
|
80
|
+
const timeoutSecs = opts.timeoutSecs ?? DEFAULT_FLUSH_TIMEOUT_SECS;
|
|
81
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
82
|
+
const lines: string[] = [];
|
|
83
|
+
|
|
84
|
+
// Preconditions, in the order that produces the most useful message. Each of
|
|
85
|
+
// these makes a flush a no-op, and each has a different remedy.
|
|
86
|
+
const cfg = readConfig();
|
|
87
|
+
if (!cfg.collector?.hooks && !cfg.collector?.sessions) {
|
|
88
|
+
return {
|
|
89
|
+
exitCode: 1,
|
|
90
|
+
pending: 0,
|
|
91
|
+
lines: [
|
|
92
|
+
"Collection is off, so nothing is spooled to flush.",
|
|
93
|
+
"Turn it on with `failproofai config`.",
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!readIngestCredential()) {
|
|
99
|
+
return {
|
|
100
|
+
exitCode: 1,
|
|
101
|
+
pending: 0,
|
|
102
|
+
lines: [
|
|
103
|
+
"This machine is not connected, so there is nowhere to flush to.",
|
|
104
|
+
"Connect it with `failproofai config`.",
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Nothing on an unsupported platform will ever read the request file, so
|
|
110
|
+
// writing one and reporting success is a lie the user cannot detect: the
|
|
111
|
+
// command exits 0, the batches stay exactly where they were, and the only
|
|
112
|
+
// symptom is data that never arrives.
|
|
113
|
+
//
|
|
114
|
+
// Refusing here matches what `failproofai config` now does on the same
|
|
115
|
+
// platforms rather than completing unenforced — the two commands should not
|
|
116
|
+
// disagree about whether this machine has a daemon.
|
|
117
|
+
if (!isDaemonSupportedPlatform()) {
|
|
118
|
+
return {
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
pending: pendingBatches(home),
|
|
121
|
+
lines: [
|
|
122
|
+
`failproofaid does not run on ${process.platform}, and it is what delivers batches.`,
|
|
123
|
+
"Nothing would pick this request up, so nothing was written.",
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const status = daemonServiceStatus();
|
|
129
|
+
if (status !== "running") {
|
|
130
|
+
return {
|
|
131
|
+
exitCode: 1,
|
|
132
|
+
pending: pendingBatches(home),
|
|
133
|
+
lines: [
|
|
134
|
+
`failproofaid is ${status}, and it is what delivers batches.`,
|
|
135
|
+
"Start it with `failproofai config`.",
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const before = pendingBatches(home);
|
|
141
|
+
if (before === 0) {
|
|
142
|
+
return { exitCode: 0, pending: 0, lines: ["Nothing spooled — everything already delivered."] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const path = flushRequestPath(home);
|
|
146
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
147
|
+
writeFileSync(path, `${JSON.stringify({ requestedAtMs: Date.now() }, null, 2)}\n`, {
|
|
148
|
+
mode: 0o600,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
lines.push(`${before} batch${before === 1 ? "" : "es"} spooled. Requested delivery.`);
|
|
152
|
+
|
|
153
|
+
if (!wait) {
|
|
154
|
+
lines.push("The daemon picks this up within a few seconds.");
|
|
155
|
+
return { exitCode: 0, pending: before, lines };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `--wait` exists so a script can flush and then assert. Poll the spool
|
|
159
|
+
// rather than the server: the question is whether THIS machine still holds
|
|
160
|
+
// events, and the spool is the only place that can answer it locally.
|
|
161
|
+
const deadline = Date.now() + timeoutSecs * 1000;
|
|
162
|
+
let pending = before;
|
|
163
|
+
while (Date.now() < deadline) {
|
|
164
|
+
await sleep(1000);
|
|
165
|
+
pending = pendingBatches(home);
|
|
166
|
+
if (pending === 0) {
|
|
167
|
+
lines.push("Spool drained.");
|
|
168
|
+
return { exitCode: 0, pending: 0, lines };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// A timeout is not necessarily a failure — a large backlog legitimately takes
|
|
173
|
+
// longer than the budget — so say what is still outstanding rather than
|
|
174
|
+
// calling it broken.
|
|
175
|
+
lines.push(
|
|
176
|
+
`Still ${pending} batch${pending === 1 ? "" : "es"} spooled after ${timeoutSecs}s.`,
|
|
177
|
+
"Delivery continues in the background; re-run with a longer --timeout to keep watching.",
|
|
178
|
+
);
|
|
179
|
+
return { exitCode: 1, pending, lines };
|
|
180
|
+
}
|