@akasecurity/cli 0.9.10 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +12 -12
  2. package/dist/cli.js +1466 -766
  3. package/extension/manifest.json +1 -1
  4. package/native-host/host.js +756 -528
  5. package/native-host/scan-worker.js +153 -58
  6. package/native-host/sync.js +284 -103
  7. package/package.json +5 -5
  8. package/web-ui/package.json +1 -1
  9. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  10. package/web-ui/web-ui/.next/app-build-manifest.json +64 -64
  11. package/web-ui/web-ui/.next/app-path-routes-manifest.json +5 -5
  12. package/web-ui/web-ui/.next/build-manifest.json +2 -2
  13. package/web-ui/web-ui/.next/prerender-manifest.json +14 -14
  14. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  15. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  16. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  17. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  18. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  19. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  20. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  21. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  22. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  23. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  24. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  25. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  26. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  28. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  29. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  31. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  32. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  33. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  34. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  35. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  36. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  37. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  38. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  39. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  40. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  41. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +3 -3
  42. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  43. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  44. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  45. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  46. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  47. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  48. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  49. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  50. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  51. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  52. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  53. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -2
  54. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -1
  55. package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -1
  56. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  57. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  58. package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
  59. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  60. package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
  61. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/web-ui/web-ui/.next/server/app-paths-manifest.json +5 -5
  63. package/web-ui/web-ui/.next/server/chunks/471.js +950 -0
  64. package/web-ui/web-ui/.next/server/chunks/475.js +1 -1
  65. package/web-ui/web-ui/.next/server/chunks/7.js +1 -1
  66. package/web-ui/web-ui/.next/server/chunks/739.js +1 -0
  67. package/web-ui/web-ui/.next/server/chunks/766.js +6 -0
  68. package/web-ui/web-ui/.next/server/chunks/851.js +1 -1
  69. package/web-ui/web-ui/.next/server/chunks/905.js +959 -0
  70. package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -5
  71. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  72. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  73. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  74. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  75. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  76. package/web-ui/web-ui/.next/static/chunks/907-2ba9428e7818b656.js +1 -0
  77. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-c5a55618d187c906.js +1 -0
  78. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-b1c76b8c00e1ae2a.js +1 -0
  79. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-159167b47a005b33.js +1 -0
  80. package/web-ui/web-ui/.next/static/chunks/app/(app)/error-dead9507fc349f26.js +1 -0
  81. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-5e872a82af85ef64.js +1 -0
  82. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-0695d0006518b2cb.js +1 -0
  83. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-adec99f2f4bbc180.js +1 -0
  84. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-d013384d2ddb3036.js +1 -0
  85. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-42c1b48a01fdecb0.js +1 -0
  86. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-dc7ae7664dcdc3c5.js +1 -0
  87. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-9b6d7cc4ce9fb33e.js +1 -0
  88. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-f3ee9ecde06df153.js +1 -0
  89. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-24a003c709776317.js +1 -0
  90. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-4ea49ce2e765fffb.js +1 -0
  91. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-6c7b94b070de8986.js +1 -0
  92. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-f37ab8e1edc67663.js +1 -0
  93. package/web-ui/web-ui/.next/static/css/c6954dd65f2115e6.css +2 -0
  94. package/web-ui/web-ui/dist/scan-worker.js +153 -58
  95. package/web-ui/web-ui/.next/server/chunks/389.js +0 -903
  96. package/web-ui/web-ui/.next/server/chunks/667.js +0 -894
  97. package/web-ui/web-ui/.next/server/chunks/950.js +0 -6
  98. package/web-ui/web-ui/.next/static/chunks/907-207847c57011ba44.js +0 -1
  99. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e4f94a75c631ce86.js +0 -1
  100. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-d70fcc5612f380b7.js +0 -1
  101. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-e1823ce15bb6277d.js +0 -1
  102. package/web-ui/web-ui/.next/static/chunks/app/(app)/error-14165283b00c8626.js +0 -1
  103. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-0fcffe4b9dff201f.js +0 -1
  104. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-c95079a2acd284fd.js +0 -1
  105. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-4b9f034bd078f5d3.js +0 -1
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-b97fc083884ef341.js +0 -1
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-e96e7e947ed5f279.js +0 -1
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-83072d434c6b6b0e.js +0 -1
  109. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-b90748d139a546c9.js +0 -1
  110. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-7a46abe149271870.js +0 -1
  111. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-f4a1ee202d6e60a4.js +0 -1
  112. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-2ec6d863f1a31792.js +0 -1
  113. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-b388003fb0d3efd1.js +0 -1
  114. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-6bda74d234a1376a.js +0 -1
  115. package/web-ui/web-ui/.next/static/css/81a7ed955b02c959.css +0 -2
  116. /package/web-ui/web-ui/.next/static/{k7tk0MLkPITbi_x6JL01X → osEmGZG6yEV-XkQlklt-B}/_buildManifest.js +0 -0
  117. /package/web-ui/web-ui/.next/static/{k7tk0MLkPITbi_x6JL01X → osEmGZG6yEV-XkQlklt-B}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -552,7 +552,66 @@ function runInherit(command, args) {
552
552
  }
553
553
  }
554
554
 
555
+ // ../packages/local-ops/src/semver.ts
556
+ function parse(version2) {
557
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?$/.exec(version2.trim());
558
+ if (!match) return null;
559
+ return {
560
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
561
+ pre: match[4] ? match[4].split(".") : []
562
+ };
563
+ }
564
+ function isSemver(version2) {
565
+ return parse(version2) !== null;
566
+ }
567
+ function comparePre(a, b) {
568
+ if (a.length === 0 && b.length === 0) return 0;
569
+ if (a.length === 0) return 1;
570
+ if (b.length === 0) return -1;
571
+ const len = Math.min(a.length, b.length);
572
+ for (let i = 0; i < len; i++) {
573
+ const ai = a[i] ?? "";
574
+ const bi = b[i] ?? "";
575
+ if (ai === bi) continue;
576
+ const an = /^\d+$/.test(ai);
577
+ const bn = /^\d+$/.test(bi);
578
+ if (an && !bn) return -1;
579
+ if (!an && bn) return 1;
580
+ if (an && bn) return Number(ai) < Number(bi) ? -1 : 1;
581
+ return ai < bi ? -1 : 1;
582
+ }
583
+ return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
584
+ }
585
+ function compareSemver(a, b) {
586
+ const pa = parse(a);
587
+ const pb = parse(b);
588
+ if (!pa || !pb) return 0;
589
+ for (let i = 0; i < 3; i++) {
590
+ const av = pa.core[i] ?? 0;
591
+ const bv = pb.core[i] ?? 0;
592
+ if (av !== bv) return av < bv ? -1 : 1;
593
+ }
594
+ return comparePre(pa.pre, pb.pre);
595
+ }
596
+ function isNewer(latest, installed) {
597
+ return compareSemver(latest, installed) > 0;
598
+ }
599
+
555
600
  // ../packages/local-ops/src/cli-plugin-manager.ts
601
+ var LEADING_WRAP = /^[([{"'`]{0,2}[vV]?/;
602
+ var TRAILING_WRAP = /[)\]}"'`,;]{0,4}$/;
603
+ function versionTokenFrom(stdout) {
604
+ const [firstLine = ""] = stdout.split("\n");
605
+ for (const token of [...firstLine.trim().split(/\s+/), ...stdout.split(/\s+/)]) {
606
+ const candidate = token.replace(LEADING_WRAP, "").replace(TRAILING_WRAP, "");
607
+ if (isSemver(candidate)) return candidate;
608
+ }
609
+ return void 0;
610
+ }
611
+ function hostCliVersion(bin) {
612
+ const { ok, stdout } = runCapture(bin, ["--version"], 5e3);
613
+ return ok ? versionTokenFrom(stdout) : void 0;
614
+ }
556
615
  var HOST_VERBS = {
557
616
  claude: {
558
617
  install: (ref) => [["plugin", "install", ref]],
@@ -1002,7 +1061,7 @@ __export(external_exports, {
1002
1061
  optional: () => optional,
1003
1062
  output: () => output,
1004
1063
  overwrite: () => _overwrite,
1005
- parse: () => parse2,
1064
+ parse: () => parse3,
1006
1065
  parseAsync: () => parseAsync2,
1007
1066
  partialRecord: () => partialRecord,
1008
1067
  pipe: () => pipe,
@@ -1346,7 +1405,7 @@ __export(core_exports2, {
1346
1405
  memoizer: () => memoizer,
1347
1406
  mergeValues: () => mergeValues,
1348
1407
  meta: () => meta,
1349
- parse: () => parse,
1408
+ parse: () => parse2,
1350
1409
  parseAsync: () => parseAsync,
1351
1410
  parseURLObject: () => parseURLObject,
1352
1411
  prettifyError: () => prettifyError,
@@ -2549,7 +2608,7 @@ var _parse = (_Err) => {
2549
2608
  };
2550
2609
  return fn;
2551
2610
  };
2552
- var parse = /* @__PURE__ */ _parse($ZodRealError);
2611
+ var parse2 = /* @__PURE__ */ _parse($ZodRealError);
2553
2612
  var _parseAsync = (_Err) => {
2554
2613
  const fn = async (schema, value, _ctx, params) => {
2555
2614
  const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
@@ -5565,10 +5624,10 @@ var $ZodFunction = /* @__PURE__ */ $constructor("$ZodFunction", (inst, def) => {
5565
5624
  throw new Error("implement() must be called with a function");
5566
5625
  }
5567
5626
  return Object.defineProperty(function(...args) {
5568
- const parsedArgs = inst._def.input ? parse(inst._def.input, args) : args;
5627
+ const parsedArgs = inst._def.input ? parse2(inst._def.input, args) : args;
5569
5628
  const result = Reflect.apply(func, this, parsedArgs);
5570
5629
  if (inst._def.output) {
5571
- return parse(inst._def.output, result);
5630
+ return parse2(inst._def.output, result);
5572
5631
  }
5573
5632
  return result;
5574
5633
  }, "_zod", { value: inst._zod, enumerable: false });
@@ -17438,7 +17497,7 @@ var ZodRealError = /* @__PURE__ */ $constructor("ZodError", initializer2, void 0
17438
17497
  });
17439
17498
 
17440
17499
  // ../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/parse.js
17441
- var parse2 = /* @__PURE__ */ _parse(ZodRealError);
17500
+ var parse3 = /* @__PURE__ */ _parse(ZodRealError);
17442
17501
  var parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError);
17443
17502
  var safeParse2 = /* @__PURE__ */ _safeParse(ZodRealError);
17444
17503
  var safeParseAsync2 = /* @__PURE__ */ _safeParseAsync(ZodRealError);
@@ -17575,7 +17634,7 @@ var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
17575
17634
  util_exports.own(this, "~standard", value);
17576
17635
  },
17577
17636
  parse: function _parse2(data, params) {
17578
- return parse2(this, data, params, { callee: _parse2 });
17637
+ return parse3(this, data, params, { callee: _parse2 });
17579
17638
  },
17580
17639
  parseAsync: async function _parseAsync2(data, params) {
17581
17640
  return await parseAsync2(this, data, params, { callee: _parseAsync2 });
@@ -20601,6 +20660,101 @@ function epochMillisToIso(ms) {
20601
20660
  return new Date(ms).toISOString();
20602
20661
  }
20603
20662
 
20663
+ // ../packages/schema/src/security/recommendations.ts
20664
+ var SEVERITY_WEIGHT = {
20665
+ critical: 4,
20666
+ high: 3,
20667
+ medium: 2,
20668
+ low: 1
20669
+ };
20670
+ var SEVERITY_WEIGHT_BY_STRING = SEVERITY_WEIGHT;
20671
+ function severityWeight(severity) {
20672
+ return SEVERITY_WEIGHT_BY_STRING[severity] ?? 0;
20673
+ }
20674
+ var ADVICE = {
20675
+ secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
20676
+ pii: "Remove or mask personal data before it reaches the model.",
20677
+ financial: "Strip card and account numbers; share only non-sensitive references.",
20678
+ phi: "Remove protected health information \u2014 it should never reach an external model.",
20679
+ code_context: "Confirm this proprietary code context is safe to share.",
20680
+ code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
20681
+ config: "Review the setting \u2014 a hook conflict or an egress change applies to every session that follows.",
20682
+ custom: "Review against your organization\u2019s custom policy."
20683
+ };
20684
+ var REC_TEMPLATE = {
20685
+ secret: { title: "Exposed secret detected", action: "Rotate" },
20686
+ pii: { title: "Personal data in a prompt", action: "Remove" },
20687
+ financial: { title: "Financial data detected", action: "Strip" },
20688
+ phi: { title: "Health information detected", action: "Remove" },
20689
+ code_context: { title: "Proprietary code shared", action: "Review" },
20690
+ code_flaw: { title: "Insecure code pattern", action: "Fix" },
20691
+ config: { title: "Weakened configuration", action: "Review" },
20692
+ custom: { title: "Custom policy match", action: "Review" }
20693
+ };
20694
+ var MAX_RECOMMENDATIONS = 10;
20695
+ function bucketizeRecommendations(findings) {
20696
+ const byRule = /* @__PURE__ */ new Map();
20697
+ const buckets = /* @__PURE__ */ new Map();
20698
+ for (const f of findings) {
20699
+ const n = f.count ?? 1;
20700
+ byRule.set(f.ruleId, (byRule.get(f.ruleId) ?? 0) + n);
20701
+ const b = buckets.get(f.category) ?? {
20702
+ category: f.category,
20703
+ count: 0,
20704
+ categoryCount: 0,
20705
+ severity: f.severity,
20706
+ weight: 0,
20707
+ ruleId: f.ruleId
20708
+ };
20709
+ b.categoryCount += n;
20710
+ const w = severityWeight(f.severity);
20711
+ if (w > b.weight) {
20712
+ b.weight = w;
20713
+ b.severity = f.severity;
20714
+ b.ruleId = f.ruleId;
20715
+ }
20716
+ buckets.set(f.category, b);
20717
+ }
20718
+ for (const b of buckets.values()) b.count = byRule.get(b.ruleId) ?? 0;
20719
+ return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.categoryCount - a.categoryCount).slice(0, MAX_RECOMMENDATIONS);
20720
+ }
20721
+ function buildRecommendations(findings) {
20722
+ return bucketizeRecommendations(findings).map((b) => {
20723
+ const copy = recommendationCopy(b.category);
20724
+ return {
20725
+ severity: b.severity,
20726
+ title: copy.title,
20727
+ description: copy.advice,
20728
+ context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
20729
+ action: copy.action
20730
+ };
20731
+ });
20732
+ }
20733
+ function recommendationCopy(category) {
20734
+ const template = REC_TEMPLATE_BY_STRING[category] ?? {
20735
+ title: `${category} finding`,
20736
+ action: "Review"
20737
+ };
20738
+ return {
20739
+ ...template,
20740
+ advice: ADVICE_BY_STRING[category] ?? "Review this finding against your policy."
20741
+ };
20742
+ }
20743
+ var ADVICE_BY_STRING = ADVICE;
20744
+ var REC_TEMPLATE_BY_STRING = REC_TEMPLATE;
20745
+ function healthScore(summary) {
20746
+ const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
20747
+ const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
20748
+ return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
20749
+ }
20750
+ function findingStatus(summary) {
20751
+ return {
20752
+ score: healthScore(summary),
20753
+ unreviewed: { ...summary.bySeverity },
20754
+ openFindings: summary.findings
20755
+ };
20756
+ }
20757
+
20604
20758
  // ../packages/schema/src/token/cost-model.ts
20605
20759
  var PROVIDER_PLATFORM = /* @__PURE__ */ new Map([
20606
20760
  ["anthropic", "anthropic"],
@@ -20986,13 +21140,11 @@ var FindingGroup = external_exports.object({
20986
21140
  latestDetectedAt: external_exports.iso.datetime(),
20987
21141
  instances: external_exports.array(FindingInstance),
20988
21142
  // Derived from instances' statuses with open-dominates precedence (see
20989
- // buildFindingGroups). Undefined only when no instance carries a status.
21143
+ // foldGroupStatus). Undefined only when no instance carries a status.
20990
21144
  status: FindingStatus.optional(),
20991
- // The distinct people across the WHOLE group, not just the `instances`
20992
- // preview — from the store's whole-group aggregate when it supplies one,
20993
- // else folded from the rows (see buildFindingGroups). Undefined when no
20994
- // instance carries a user, or when the store supplied whole-group folds
20995
- // without one.
21145
+ // The distinct people across the WHOLE group, not just the instances
21146
+ // carried here. Undefined when no instance carries a user, or when the
21147
+ // store supplied whole-group folds without one.
20996
21148
  users: external_exports.array(FindingUser).optional()
20997
21149
  }).meta({ id: "FindingGroup" });
20998
21150
  var FindingStats = external_exports.object({
@@ -21021,21 +21173,31 @@ var FindingFacets = external_exports.object({
21021
21173
  // counted under no value.
21022
21174
  status: external_exports.array(FindingFacetItem),
21023
21175
  // Host tool (attributes.tool_name). Present only on the instance-level
21024
- // reads, which can filter by it; the grouped read omits the dimension
21176
+ // reads, which can filter by it; the type-level read omits the dimension
21025
21177
  // because a group spans tools.
21026
21178
  tool: external_exports.array(FindingFacetItem).optional()
21027
21179
  }).meta({ id: "FindingFacets" });
21028
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
21029
- var ListGroupedFindingsQuery = external_exports.object({
21180
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
21181
+ id: "FindingTypeSummary"
21182
+ });
21183
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
21184
+ var MAX_FINDING_TYPES_LIMIT = 100;
21185
+ var ListFindingTypesQuery = external_exports.object({
21030
21186
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
21031
- // FindingAction.
21187
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
21188
+ // firing version carries, and this list pages types.
21189
+ //
21190
+ // That is NOT a claim the findings of a type share it. A rule can hold several
21191
+ // definition versions at different severities, so a type kept by this filter
21192
+ // can hold findings that individually do not match — see totals.findings on
21193
+ // ListFindingTypesResponse, which counts them all.
21032
21194
  severity: external_exports.array(Severity).optional(),
21033
21195
  subtype: external_exports.array(external_exports.string()).optional(),
21034
21196
  provider: external_exports.array(FindingProvider).optional(),
21035
21197
  action: external_exports.array(FindingAction).optional(),
21036
- // Matches a group's DERIVED status (see FindingGroup.status), not its
21037
- // individual instances' — so a filtered group's Status column always reads
21038
- // one of the requested values.
21198
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
21199
+ // individual findings' — so a filtered row's status always reads one of the
21200
+ // requested values.
21039
21201
  status: external_exports.array(FindingStatus).optional(),
21040
21202
  q: external_exports.string().optional(),
21041
21203
  // Scope to findings whose event carries this session id (the Activity page's
@@ -21045,23 +21207,37 @@ var ListGroupedFindingsQuery = external_exports.object({
21045
21207
  // from a time-scoped page (Activity's range) can carry that scope. Absent
21046
21208
  // means all time — this list has no default window.
21047
21209
  from: external_exports.iso.datetime().optional(),
21048
- // A group or instance id that must appear in the page even when the cursor
21049
- // has already advanced past its sort position. This is what keeps the
21050
- // Findings page's one-shot ?finding= deep link resolving once the list
21051
- // paginates: the target group is appended out of sort order rather than
21052
- // scanning forward for it. Never affects totals, facets or the cursor.
21210
+ // A RULE id that must appear in the page even when the cursor has already
21211
+ // advanced past its sort position. This is what keeps the selected type
21212
+ // visible in the list once it paginates: the target is appended out of sort
21213
+ // order rather than scanned forward for. Never affects totals, facets or the
21214
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
21215
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
21216
+ // and so is not bounded by what any page happens to hold.
21053
21217
  includeId: external_exports.string().optional(),
21054
- groupBy: external_exports.literal("type").optional(),
21055
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
21218
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
21056
21219
  cursor: external_exports.string().optional()
21057
21220
  });
21058
- var ListGroupedFindingsResponse = external_exports.object({
21221
+ var ListFindingTypesResponse = external_exports.object({
21059
21222
  totals: external_exports.object({
21223
+ // Findings belonging to the matching TYPES — not findings that each match
21224
+ // the filters. The filters here select types, so a type that survives
21225
+ // contributes its whole instanceCount.
21226
+ //
21227
+ // `status` is the one exception, narrowed per finding via
21228
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
21229
+ // this can exceed what the instance read reports for the same filters: a
21230
+ // rule whose severity moved between versions is kept on its newest and
21231
+ // still counts its older findings. Narrowing the other three needs
21232
+ // per-dimension counts the aggregate does not carry today.
21060
21233
  findings: external_exports.number().int().nonnegative(),
21061
- groups: external_exports.number().int().nonnegative()
21234
+ // Counts TYPES, which is the unit this read pages. The instance read's
21235
+ // own totals count findings; the two deliberately answer different
21236
+ // questions and are never summed.
21237
+ types: external_exports.number().int().nonnegative()
21062
21238
  }),
21063
21239
  facets: FindingFacets,
21064
- items: external_exports.array(FindingGroup),
21240
+ items: external_exports.array(FindingTypeSummary),
21065
21241
  nextCursor: external_exports.string().nullable(),
21066
21242
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
21067
21243
  // many times that rule fired in the session's persisted transcript. Findings
@@ -21069,7 +21245,7 @@ var ListGroupedFindingsResponse = external_exports.object({
21069
21245
  // every firing, so the two numbers legitimately differ — this map lets a
21070
21246
  // session-scoped view show both.
21071
21247
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
21072
- }).meta({ id: "ListGroupedFindingsResponse" });
21248
+ }).meta({ id: "ListFindingTypesResponse" });
21073
21249
  var ApplyFindingActionRequest = external_exports.object({
21074
21250
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
21075
21251
  // it, so it is excluded from the request contract. The mapping helper
@@ -21099,12 +21275,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
21099
21275
  var MAX_FLAT_FINDINGS_LIMIT = 200;
21100
21276
  var ListFindingInstancesQuery = external_exports.object({
21101
21277
  severity: external_exports.array(Severity).optional(),
21102
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
21278
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
21279
+ // ONE of them is how the master/detail view scopes its right-hand panel.
21103
21280
  subtype: external_exports.array(external_exports.string()).optional(),
21104
21281
  provider: external_exports.array(FindingProvider).optional(),
21105
21282
  action: external_exports.array(FindingAction).optional(),
21106
21283
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
21107
- // the grouped query's group-level fold.
21284
+ // the types query's type-level fold.
21108
21285
  status: external_exports.array(FindingStatus).optional(),
21109
21286
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
21110
21287
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -21121,37 +21298,47 @@ var ListFindingInstancesQuery = external_exports.object({
21121
21298
  });
21122
21299
  var ListFindingInstancesResponse = external_exports.object({
21123
21300
  // Instances matching the filters across the whole scope, not just this
21124
- // page — cursor-independent, like the grouped list's totals.
21301
+ // page — cursor-independent, like the types list's totals.
21125
21302
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
21126
- // Counts in INSTANCES here, where the grouped response counts groups. Each
21303
+ // Counts in INSTANCES here, where the types response counts types. Each
21127
21304
  // dimension still excludes its own filter.
21128
21305
  facets: FindingFacets,
21129
21306
  items: external_exports.array(FindingInstanceDetail),
21130
21307
  nextCursor: external_exports.string().nullable()
21131
21308
  }).meta({ id: "ListFindingInstancesResponse" });
21132
- var FindingLocationFile = external_exports.object({
21133
- // Empty when the instances carried no file path (a prompt or a tool call
21134
- // with no file attribution).
21135
- file: external_exports.string(),
21136
- instanceCount: external_exports.number().int().nonnegative(),
21137
- maxSeverity: Severity,
21138
- latestDetectedAt: external_exports.iso.datetime(),
21139
- // Folded from the instances' derived statuses with the same
21140
- // open-dominates precedence a group uses.
21141
- status: FindingStatus.optional(),
21142
- // Distinct rules seen at this location, capped — the row shows them as
21143
- // chips, and the count is what conveys scale.
21144
- ruleIds: external_exports.array(external_exports.string())
21145
- }).meta({ id: "FindingLocationFile" });
21146
- var FindingLocationRepo = external_exports.object({
21309
+ var FindingLocationSummary = external_exports.object({
21310
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
21311
+ // because a location's identity is two values and a URL param carries one:
21312
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
21313
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
21314
+ // client's page dedupe — never decoded, and never a sort key.
21315
+ id: external_exports.string(),
21147
21316
  /** Empty when the instances carried no repo attribute. */
21148
21317
  repo: external_exports.string(),
21318
+ // Empty when the instances carried no file path (a prompt, or a tool call
21319
+ // with no file attribution). Both halves empty is a real location — usually
21320
+ // the largest one in a store — and is selectable like any other.
21321
+ file: external_exports.string(),
21149
21322
  instanceCount: external_exports.number().int().nonnegative(),
21323
+ // The WORST severity present, not the first row's. It is this list's primary
21324
+ // sort key, so it is also what explains why a row is where it is, and it is
21325
+ // how a reader decides what to open without opening everything.
21150
21326
  maxSeverity: Severity,
21151
21327
  latestDetectedAt: external_exports.iso.datetime(),
21328
+ // Folded from the instances' derived statuses with the same open-dominates
21329
+ // precedence a group uses, so it answers "is anything left to do here" and
21330
+ // not much more: a location holding 1 open among 40 resolved reads like one
21331
+ // holding 40 open. That loss is accepted — the panel beside this list
21332
+ // carries each finding's own status, and instanceCount sits next to the
21333
+ // badge.
21152
21334
  status: FindingStatus.optional(),
21153
- files: external_exports.array(FindingLocationFile)
21154
- }).meta({ id: "FindingLocationRepo" });
21335
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
21336
+ // tally rather than a sample and a row can say how many there are. Bounded
21337
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
21338
+ ruleIds: external_exports.array(external_exports.string())
21339
+ }).meta({ id: "FindingLocationSummary" });
21340
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
21341
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
21155
21342
  var ListFindingLocationsQuery = external_exports.object({
21156
21343
  severity: external_exports.array(Severity).optional(),
21157
21344
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -21164,18 +21351,42 @@ var ListFindingLocationsQuery = external_exports.object({
21164
21351
  q: external_exports.string().optional(),
21165
21352
  sessionId: external_exports.string().optional(),
21166
21353
  from: external_exports.iso.datetime().optional(),
21167
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
21354
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
21355
+ // even when the cursor has already advanced past its sort position — the
21356
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
21357
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
21358
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
21359
+ // into the thousands, a selection sitting off page 0 is the ordinary case
21360
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
21361
+ includeId: external_exports.string().optional(),
21362
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
21363
+ cursor: external_exports.string().optional()
21168
21364
  });
21169
21365
  var ListFindingLocationsResponse = external_exports.object({
21170
21366
  totals: external_exports.object({
21367
+ // Findings matching the filters across the whole scope. Unlike the types
21368
+ // read's same-named field this needs no caveat: the filters here narrow
21369
+ // per finding, so this is the sum of every row's instanceCount.
21171
21370
  findings: external_exports.number().int().nonnegative(),
21172
- repos: external_exports.number().int().nonnegative(),
21173
- files: external_exports.number().int().nonnegative()
21371
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
21372
+ // states. The facets beside it count FINDINGS (see below); a surface
21373
+ // showing both says which is which.
21374
+ locations: external_exports.number().int().nonnegative()
21174
21375
  }),
21175
- /** Sorted by max severity, then most recent. */
21176
- items: external_exports.array(FindingLocationRepo),
21177
- /** Whether `limit` truncated the repo list. */
21178
- hasMore: external_exports.boolean()
21376
+ // Counts in FINDINGS, where the types response counts types, each dimension
21377
+ // still excluding its own filter. Deliberately not locations: counting those
21378
+ // needs a set of location keys per dimension per value — memory tracking the
21379
+ // store times the vocabulary, in a read whose scan promises flat memory —
21380
+ // and the cheap per-location version is not an approximation but WRONG. A
21381
+ // location holding {claudecode, block} and {codex, warn} would survive
21382
+ // provider=claudecode AND action=warn, under which no single finding
21383
+ // matches, so the facet would contradict the instanceCount this whole view
21384
+ // rests on. Findings also keep the toolbar in the same unit as the page
21385
+ // tally and the panel it sits above.
21386
+ facets: FindingFacets,
21387
+ /** Sorted by max severity, then most recent, then (repo, file). */
21388
+ items: external_exports.array(FindingLocationSummary),
21389
+ nextCursor: external_exports.string().nullable()
21179
21390
  }).meta({ id: "ListFindingLocationsResponse" });
21180
21391
 
21181
21392
  // ../packages/schema/src/zod/meta.ts
@@ -22344,6 +22555,14 @@ var ControlPlaneErrorBody = external_exports.object({
22344
22555
  message: external_exports.string().optional()
22345
22556
  }).optional()
22346
22557
  });
22558
+ var RemoteFailureKind = external_exports.enum([
22559
+ "unauthorized",
22560
+ "forbidden",
22561
+ "route-absent",
22562
+ "invalid-request",
22563
+ "rejected",
22564
+ "unreachable"
22565
+ ]);
22347
22566
  var AttachDeviceRequest = external_exports.object({
22348
22567
  // This machine's own continuity id, so re-attaching ROTATES the credential
22349
22568
  // on one machine record instead of producing a second one. Client-minted
@@ -23049,139 +23268,62 @@ function deriveFindingStatus(row) {
23049
23268
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
23050
23269
  return "open";
23051
23270
  }
23052
- function distinctUsers(instances) {
23053
- const seen = /* @__PURE__ */ new Set();
23054
- const users = [];
23055
- for (const i of instances) {
23056
- if (i.user === void 0 || seen.has(i.user.id)) continue;
23057
- seen.add(i.user.id);
23058
- users.push(i.user);
23059
- }
23060
- return users;
23061
- }
23062
23271
  function sortUsers(users) {
23063
23272
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
23064
23273
  }
23065
- function buildFindingGroups(rows, opts = {}) {
23066
- const overrides = opts.overrides;
23274
+ function buildFindingTypes(aggregates, opts = {}) {
23067
23275
  const packNames = opts.packNames;
23068
- const aggregates = opts.aggregates;
23069
- const byRuleId = /* @__PURE__ */ new Map();
23070
- for (const row of rows) {
23071
- const existing = byRuleId.get(row.ruleId);
23072
- if (existing) existing.push(row);
23073
- else byRuleId.set(row.ruleId, [row]);
23074
- }
23075
- const groups = [];
23076
- for (const [ruleId, ruleRows] of byRuleId) {
23077
- const instances = ruleRows.map((r) => {
23078
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
23079
- return {
23080
- id: r.id,
23081
- provider: toApiProvider(r.sourceTool),
23082
- repo: r.repo,
23083
- file: r.file,
23084
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
23085
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
23086
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
23087
- ...r.user === void 0 ? {} : { user: r.user },
23088
- action: toApiAction(effectiveDbAction),
23089
- detectedAt: r.occurredAt,
23090
- confidence: r.confidence,
23091
- status: r.status
23092
- };
23093
- });
23094
- const agg = aggregates?.get(ruleId);
23095
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
23096
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
23097
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
23098
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
23099
- );
23100
- const seenProviders = /* @__PURE__ */ new Set();
23101
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
23102
- if (seenProviders.has(p)) return false;
23103
- seenProviders.add(p);
23104
- return true;
23105
- });
23106
- const actionSet = new Set(
23107
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
23108
- );
23276
+ const types = [];
23277
+ for (const [ruleId, agg] of aggregates) {
23278
+ const users = sortUsers(agg.users ?? []);
23279
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
23280
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
23109
23281
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
23110
- const severity = ruleRows[0]?.severity ?? "low";
23111
- const detection = {
23112
- id: ruleId,
23113
- name: packNames?.get(ruleId) ?? null
23114
- };
23115
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
23116
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
23117
- const match = {
23118
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
23119
- contextPrefix: ""
23120
- // empty (pending privacy review)
23121
- };
23122
- const status = foldGroupStatus(
23123
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
23124
- );
23125
- const group = {
23282
+ const apiCategory = toApiCategory(agg.category ?? "custom");
23283
+ const type = {
23126
23284
  id: ruleId,
23127
23285
  category: apiCategory,
23128
23286
  subtype: ruleId,
23129
23287
  // human label comes with pack metadata later
23130
- severity,
23131
- match,
23132
- detection,
23133
- policy,
23134
- instanceCount: agg?.instanceCount ?? instances.length,
23288
+ severity: agg.severity ?? "low",
23289
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
23290
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
23291
+ instanceCount: agg.instanceCount,
23135
23292
  providers,
23136
23293
  aggregateAction,
23137
- latestDetectedAt,
23138
- instances,
23139
- status,
23294
+ latestDetectedAt: agg.latestDetectedAt,
23295
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
23140
23296
  ...users.length > 0 ? { users } : {}
23141
23297
  };
23142
- if (agg) {
23143
- actionsCache.set(group, [...actionSet]);
23144
- if (agg.searchText !== void 0) {
23145
- haystackCache.set(group, buildHaystack(group, agg.searchText));
23146
- }
23298
+ actionsCache.set(type, [...actionSet]);
23299
+ if (agg.searchText !== void 0) {
23300
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
23147
23301
  }
23148
- groups.push(group);
23302
+ types.push(type);
23149
23303
  }
23150
- return groups;
23304
+ return types;
23151
23305
  }
23152
23306
  var haystackCache = /* @__PURE__ */ new WeakMap();
23153
- function buildHaystack(g, extra) {
23307
+ function buildHaystack(t, extra) {
23154
23308
  return [
23155
- g.subtype,
23156
- g.category,
23157
- g.match.maskedValue,
23158
- g.policy.name,
23159
- g.id,
23160
- ...g.instances.map((i) => i.repo),
23161
- ...g.instances.map((i) => i.file),
23162
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
23163
- ...g.instances.map((i) => i.id),
23164
- // The people: the whole group's list when the store folded one, plus the
23165
- // preview's own — the two overlap, and a haystack does not mind.
23166
- ...(g.users ?? []).map((u) => u.name),
23167
- ...g.instances.map((i) => i.user?.name ?? ""),
23309
+ t.subtype,
23310
+ t.category,
23311
+ t.policy.name,
23312
+ t.id,
23313
+ ...(t.users ?? []).map((u) => u.name),
23168
23314
  ...extra === void 0 ? [] : [extra]
23169
23315
  ].join(" ").toLowerCase();
23170
23316
  }
23171
- function groupHaystack(g) {
23172
- const cached2 = haystackCache.get(g);
23317
+ function typeHaystack(t) {
23318
+ const cached2 = haystackCache.get(t);
23173
23319
  if (cached2 !== void 0) return cached2;
23174
- const haystack = buildHaystack(g);
23175
- haystackCache.set(g, haystack);
23320
+ const haystack = buildHaystack(t);
23321
+ haystackCache.set(t, haystack);
23176
23322
  return haystack;
23177
23323
  }
23178
23324
  var actionsCache = /* @__PURE__ */ new WeakMap();
23179
- function groupActions(g) {
23180
- const cached2 = actionsCache.get(g);
23181
- if (cached2 !== void 0) return cached2;
23182
- const actions = [...new Set(g.instances.map((i) => i.action))];
23183
- actionsCache.set(g, actions);
23184
- return actions;
23325
+ function typeActions(t) {
23326
+ return actionsCache.get(t) ?? [];
23185
23327
  }
23186
23328
  function countInstancesByStatus(statusInputs, statuses) {
23187
23329
  const statusSet = new Set(statuses);
@@ -23192,8 +23334,8 @@ function countInstancesByStatus(statusInputs, statuses) {
23192
23334
  }
23193
23335
  return sum;
23194
23336
  }
23195
- function applyFindingFilters(groups, opts) {
23196
- let filtered = groups;
23337
+ function applyFindingFilters(types, opts) {
23338
+ let filtered = types;
23197
23339
  if (opts.severity && opts.severity.length > 0) {
23198
23340
  const sevSet = new Set(opts.severity);
23199
23341
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -23204,7 +23346,7 @@ function applyFindingFilters(groups, opts) {
23204
23346
  }
23205
23347
  if (opts.actions && opts.actions.length > 0) {
23206
23348
  const actionSet = new Set(opts.actions);
23207
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
23349
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
23208
23350
  }
23209
23351
  if (opts.subtype && opts.subtype.length > 0) {
23210
23352
  const subtypeSet = new Set(opts.subtype);
@@ -23216,7 +23358,7 @@ function applyFindingFilters(groups, opts) {
23216
23358
  }
23217
23359
  if (opts.q) {
23218
23360
  const q = opts.q.toLowerCase();
23219
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
23361
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
23220
23362
  }
23221
23363
  return filtered;
23222
23364
  }
@@ -23231,11 +23373,11 @@ function compareFindingGroupOrder(a, b) {
23231
23373
  if (recencyDiff !== 0) return recencyDiff;
23232
23374
  return a.id.localeCompare(b.id);
23233
23375
  }
23234
- function sortFindingGroups(groups) {
23235
- return [...groups].sort(compareFindingGroupOrder);
23376
+ function sortFindingTypes(types) {
23377
+ return [...types].sort(compareFindingGroupOrder);
23236
23378
  }
23237
- function computeFindingFacets(allGroups, opts) {
23238
- const forSeverity = applyFindingFilters(allGroups, {
23379
+ function computeFindingFacets(allTypes, opts) {
23380
+ const forSeverity = applyFindingFilters(allTypes, {
23239
23381
  providers: opts.providers,
23240
23382
  actions: opts.actions,
23241
23383
  statuses: opts.statuses,
@@ -23246,7 +23388,7 @@ function computeFindingFacets(allGroups, opts) {
23246
23388
  for (const g of forSeverity) {
23247
23389
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
23248
23390
  }
23249
- const forProvider = applyFindingFilters(allGroups, {
23391
+ const forProvider = applyFindingFilters(allTypes, {
23250
23392
  actions: opts.actions,
23251
23393
  statuses: opts.statuses,
23252
23394
  q: opts.q,
@@ -23257,7 +23399,7 @@ function computeFindingFacets(allGroups, opts) {
23257
23399
  for (const g of forProvider) {
23258
23400
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23259
23401
  }
23260
- const forAction = applyFindingFilters(allGroups, {
23402
+ const forAction = applyFindingFilters(allTypes, {
23261
23403
  providers: opts.providers,
23262
23404
  statuses: opts.statuses,
23263
23405
  q: opts.q,
@@ -23266,9 +23408,9 @@ function computeFindingFacets(allGroups, opts) {
23266
23408
  });
23267
23409
  const actionMap = /* @__PURE__ */ new Map();
23268
23410
  for (const g of forAction) {
23269
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23411
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23270
23412
  }
23271
- const forSubtype = applyFindingFilters(allGroups, {
23413
+ const forSubtype = applyFindingFilters(allTypes, {
23272
23414
  providers: opts.providers,
23273
23415
  actions: opts.actions,
23274
23416
  statuses: opts.statuses,
@@ -23277,7 +23419,7 @@ function computeFindingFacets(allGroups, opts) {
23277
23419
  });
23278
23420
  const subtypeMap = /* @__PURE__ */ new Map();
23279
23421
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23280
- const forStatus = applyFindingFilters(allGroups, {
23422
+ const forStatus = applyFindingFilters(allTypes, {
23281
23423
  providers: opts.providers,
23282
23424
  actions: opts.actions,
23283
23425
  q: opts.q,
@@ -23325,10 +23467,20 @@ function matchesDimension(row, opts, dimension) {
23325
23467
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23326
23468
  case "tools":
23327
23469
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23470
+ // An EMPTY value is a real filter here, not an absent one. The location
23471
+ // list buckets a finding whose event recorded no repo — or no file — under
23472
+ // the empty string, and selecting that bucket has to narrow the panel to
23473
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23474
+ // row omits the key, which every call site already does.
23475
+ //
23476
+ // Reading '' as unset is what this replaced, and it failed in the one place
23477
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23478
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23479
+ // — a row reading 3 findings beside a panel listing every finding there is.
23328
23480
  case "repo":
23329
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23481
+ return opts.repo === void 0 || row.repo === opts.repo;
23330
23482
  case "file":
23331
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23483
+ return opts.file === void 0 || row.file === opts.file;
23332
23484
  case "q":
23333
23485
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23334
23486
  }
@@ -23442,6 +23594,23 @@ function addToLocation(acc, row) {
23442
23594
  acc.statuses.push(row.status);
23443
23595
  acc.ruleIds.add(row.ruleId);
23444
23596
  }
23597
+ function compareLocationOrder(a, b) {
23598
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23599
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23600
+ if (rankA !== rankB) return rankA - rankB;
23601
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23602
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23603
+ }
23604
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23605
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23606
+ return 0;
23607
+ }
23608
+ function encodeLocationId(repo, file2) {
23609
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23610
+ }
23611
+ function encodePart(value) {
23612
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23613
+ }
23445
23614
 
23446
23615
  // ../packages/schema/src/zod/installed-pack.ts
23447
23616
  var InstalledPack = external_exports.object({
@@ -23899,7 +24068,7 @@ function isVaultConsentValid(consent) {
23899
24068
  // ../packages/schema/src/zod/local.ts
23900
24069
  var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23901
24070
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23902
- var HISTORY_SYNC_PAYLOAD_VERSION = 2;
24071
+ var HISTORY_SYNC_PAYLOAD_VERSION = 3;
23903
24072
  var RunMode = external_exports.enum(["standalone", "attached"]);
23904
24073
  var ControlPlaneConnection = external_exports.object({
23905
24074
  endpoint: external_exports.string().min(1),
@@ -23972,11 +24141,11 @@ var WorkspaceSettings = external_exports.object({
23972
24141
  // covers the current payload and must be re-granted.
23973
24142
  modelJudgeConsent: ModelJudgeConsent.optional(),
23974
24143
  // Records that the user consented to the DEFERRED send — the outbox — along
23975
- // with the payload shape and the endpoint they agreed to. Since payload v2
23976
- // that covers both the pre-attach backlog and undelivered captures (which
23977
- // carry prompt/reply text in `content`); the key name predates the widening.
23978
- // Absent until granted, and a grant for a different endpoint or an older
23979
- // payload no longer counts.
24144
+ // with the payload shape and the endpoint they agreed to. Since payload v3
24145
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
24146
+ // carry prompt/reply/tool-output text in `content`; the key name predates
24147
+ // both widenings. Absent until granted, and a grant for a different endpoint
24148
+ // or an older payload no longer counts.
23980
24149
  historySyncConsent: HistorySyncConsent.optional()
23981
24150
  });
23982
24151
  function defaultWorkspaceSettings() {
@@ -24110,6 +24279,9 @@ var ManagedSettingKey = external_exports.enum([
24110
24279
  "dataSharesInPlace",
24111
24280
  "redactFallback"
24112
24281
  ]).meta({ id: "ManagedSettingKey" });
24282
+ function isManagedSettingKey(value) {
24283
+ return ManagedSettingKey.safeParse(value).success;
24284
+ }
24113
24285
  var ManagedSettingsValues = external_exports.object({
24114
24286
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24115
24287
  controlPlane: external_exports.object({
@@ -24134,7 +24306,27 @@ var ManagedSettings = external_exports.object({
24134
24306
  // Which of those the user may not change. A key here with no matching value
24135
24307
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
24136
24308
  // the user may still override. The two are separable on purpose.
24137
- lockedFields: external_exports.array(ManagedSettingKey).default([])
24309
+ //
24310
+ // Parsed as NAMES rather than as the enum, and split below: a name this
24311
+ // build does not know is dropped from the locked set and reported, never a
24312
+ // reason to refuse the file. The same shape reaches an older build whenever
24313
+ // an administrator locks a key a newer build added, and refusing it there
24314
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
24315
+ // the fleets most likely to carry a version skew. A name outside the enum
24316
+ // is still never HONOURED: the lockable set stays explicit above.
24317
+ lockedFields: external_exports.array(external_exports.string()).default([])
24318
+ }).transform(({ lockedFields, ...rest }) => {
24319
+ const known = [];
24320
+ const unknown2 = [];
24321
+ for (const name of lockedFields) {
24322
+ if (isManagedSettingKey(name)) known.push(name);
24323
+ else unknown2.push(name);
24324
+ }
24325
+ return {
24326
+ ...rest,
24327
+ lockedFields: known,
24328
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
24329
+ };
24138
24330
  }).meta({ id: "ManagedSettings" });
24139
24331
  var NO_MANAGED_CONTEXT = { present: false, lockedFields: [] };
24140
24332
 
@@ -24262,7 +24454,11 @@ var FindingsTimeseriesPoint = external_exports.object({
24262
24454
  timestamp: external_exports.iso.date(),
24263
24455
  critical: external_exports.number().int().nonnegative(),
24264
24456
  high: external_exports.number().int().nonnegative(),
24265
- medium: external_exports.number().int().nonnegative()
24457
+ medium: external_exports.number().int().nonnegative(),
24458
+ // Optional and additive, so a producer written against the earlier
24459
+ // three-series contract keeps validating. A consumer plotting it resolves the
24460
+ // absent case itself — the chart point requires a number.
24461
+ low: external_exports.number().int().nonnegative().optional()
24266
24462
  }).meta({ id: "FindingsTimeseriesPoint" });
24267
24463
  var FindingsTimeseriesResponse = external_exports.object({
24268
24464
  range: TimeRange,
@@ -24288,6 +24484,10 @@ var ResolvedFeedItem = external_exports.object({
24288
24484
  findingKey: external_exports.string(),
24289
24485
  ruleId: external_exports.string(),
24290
24486
  severity: Severity,
24487
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24488
+ // identifies the file: a bare path matches the same name in every repo.
24489
+ // Optional and additive; empty when the event carried no repo.
24490
+ repo: external_exports.string().optional(),
24291
24491
  path: external_exports.string(),
24292
24492
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24293
24493
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -24622,51 +24822,6 @@ function pluginRef(agent) {
24622
24822
  return `${agent.pluginName}@${agent.marketplace}`;
24623
24823
  }
24624
24824
 
24625
- // ../packages/local-ops/src/semver.ts
24626
- function parse3(version2) {
24627
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?$/.exec(version2.trim());
24628
- if (!match) return null;
24629
- return {
24630
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
24631
- pre: match[4] ? match[4].split(".") : []
24632
- };
24633
- }
24634
- function isSemver(version2) {
24635
- return parse3(version2) !== null;
24636
- }
24637
- function comparePre(a, b) {
24638
- if (a.length === 0 && b.length === 0) return 0;
24639
- if (a.length === 0) return 1;
24640
- if (b.length === 0) return -1;
24641
- const len = Math.min(a.length, b.length);
24642
- for (let i = 0; i < len; i++) {
24643
- const ai = a[i] ?? "";
24644
- const bi = b[i] ?? "";
24645
- if (ai === bi) continue;
24646
- const an = /^\d+$/.test(ai);
24647
- const bn = /^\d+$/.test(bi);
24648
- if (an && !bn) return -1;
24649
- if (!an && bn) return 1;
24650
- if (an && bn) return Number(ai) < Number(bi) ? -1 : 1;
24651
- return ai < bi ? -1 : 1;
24652
- }
24653
- return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
24654
- }
24655
- function compareSemver(a, b) {
24656
- const pa = parse3(a);
24657
- const pb = parse3(b);
24658
- if (!pa || !pb) return 0;
24659
- for (let i = 0; i < 3; i++) {
24660
- const av = pa.core[i] ?? 0;
24661
- const bv = pb.core[i] ?? 0;
24662
- if (av !== bv) return av < bv ? -1 : 1;
24663
- }
24664
- return comparePre(pa.pre, pb.pre);
24665
- }
24666
- function isNewer(latest, installed) {
24667
- return compareSemver(latest, installed) > 0;
24668
- }
24669
-
24670
24825
  // ../packages/local-ops/src/updates.ts
24671
24826
  var CLI_PACKAGE = "@akasecurity/cli";
24672
24827
  function isRecord(value) {
@@ -25130,6 +25285,100 @@ function installAgentPlugin(agentId, mode = "capture") {
25130
25285
  return runSteps(resolved.cliBin, manager3.installSteps(resolved.ref), mode);
25131
25286
  }
25132
25287
 
25288
+ // ../packages/local-ops/src/background-schedule.ts
25289
+ import { createHash } from "crypto";
25290
+ import { mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
25291
+ import { homedir as homedir3 } from "os";
25292
+ import { posix } from "path";
25293
+ var BACKGROUND_SYNC_LABEL = "com.akasecurity.aka.background-sync";
25294
+ var BACKGROUND_SYNC_INTERVAL_SECONDS = 30 * 60;
25295
+ function launchAgentsDir(deps) {
25296
+ return posix.join((deps.homeDir ?? homedir3)(), "Library", "LaunchAgents");
25297
+ }
25298
+ function backgroundSyncLabel(base) {
25299
+ return `${BACKGROUND_SYNC_LABEL}.${createHash("sha256").update(base).digest("hex").slice(0, 12)}`;
25300
+ }
25301
+ function plistPath(base, deps) {
25302
+ return posix.join(launchAgentsDir(deps), `${backgroundSyncLabel(base)}.plist`);
25303
+ }
25304
+ function escapeXml(value) {
25305
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
25306
+ }
25307
+ function renderPlist(label, programArguments) {
25308
+ const args = programArguments.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
25309
+ return `<?xml version="1.0" encoding="UTF-8"?>
25310
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25311
+ <plist version="1.0">
25312
+ <dict>
25313
+ <key>Label</key>
25314
+ <string>${escapeXml(label)}</string>
25315
+ <key>ProgramArguments</key>
25316
+ <array>
25317
+ ${args}
25318
+ </array>
25319
+ <key>StartInterval</key>
25320
+ <integer>${String(BACKGROUND_SYNC_INTERVAL_SECONDS)}</integer>
25321
+ <key>RunAtLoad</key>
25322
+ <false/>
25323
+ <key>ProcessType</key>
25324
+ <string>Background</string>
25325
+ </dict>
25326
+ </plist>
25327
+ `;
25328
+ }
25329
+ function defaultReadFile(path) {
25330
+ try {
25331
+ return readFileSync3(path, "utf8");
25332
+ } catch {
25333
+ return null;
25334
+ }
25335
+ }
25336
+ function defaultRunLaunchctl(args) {
25337
+ return runCapture("launchctl", args).ok;
25338
+ }
25339
+ function guiDomain() {
25340
+ const getuid = process.getuid;
25341
+ return `gui/${String(typeof getuid === "function" ? getuid() : 0)}`;
25342
+ }
25343
+ function installBackgroundSync(base, deps = {}) {
25344
+ try {
25345
+ if ((deps.platform ?? process.platform) !== "darwin") return;
25346
+ const reinvoke = (deps.reinvoke ?? reinvokeArgv)("sync-history", ["--run", "--home", base]);
25347
+ if (reinvoke === null) return;
25348
+ const label = backgroundSyncLabel(base);
25349
+ const content = renderPlist(label, [reinvoke.command, ...reinvoke.args]);
25350
+ const path = plistPath(base, deps);
25351
+ if ((deps.readFile ?? defaultReadFile)(path) === content) return;
25352
+ (deps.mkdir ?? ((dir) => {
25353
+ mkdirSync(dir, { recursive: true });
25354
+ }))(launchAgentsDir(deps));
25355
+ (deps.writeFile ?? ((p, data) => {
25356
+ writeFileSync(p, data);
25357
+ }))(path, content);
25358
+ const domain2 = guiDomain();
25359
+ const launchctl = deps.runLaunchctl ?? defaultRunLaunchctl;
25360
+ launchctl(["bootout", `${domain2}/${label}`]);
25361
+ if (!launchctl(["bootstrap", domain2, path])) {
25362
+ process.stderr.write(`[aka] background-sync: launchctl bootstrap failed for ${label}
25363
+ `);
25364
+ }
25365
+ } catch {
25366
+ }
25367
+ }
25368
+ function uninstallBackgroundSync(base, deps = {}) {
25369
+ try {
25370
+ if ((deps.platform ?? process.platform) !== "darwin") return;
25371
+ (deps.runLaunchctl ?? defaultRunLaunchctl)([
25372
+ "bootout",
25373
+ `${guiDomain()}/${backgroundSyncLabel(base)}`
25374
+ ]);
25375
+ (deps.removeFile ?? ((p) => {
25376
+ rmSync(p, { force: true });
25377
+ }))(plistPath(base, deps));
25378
+ } catch {
25379
+ }
25380
+ }
25381
+
25133
25382
  // ../packages/local-ops/src/claude-plugin.ts
25134
25383
  var manager = createCliPluginManager("claude");
25135
25384
  var claudeAvailable = manager.available;
@@ -26949,7 +27198,7 @@ var CPU_CORROBORATION_SHARE = 0.2;
26949
27198
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
26950
27199
 
26951
27200
  // ../packages/persistence/src/attached-derived.ts
26952
- import { rmSync } from "fs";
27201
+ import { rmSync as rmSync2 } from "fs";
26953
27202
  import { join as join3 } from "path";
26954
27203
  var POLICY_CACHE_FILENAME = "policy-cache.json";
26955
27204
  var ATTACHED_SYNC_STATE_FILENAME = "attached-sync-state.json";
@@ -26966,14 +27215,14 @@ var ATTACHED_DERIVED_FILENAMES = [
26966
27215
  function clearAttachmentDerivedState(dataDir2) {
26967
27216
  for (const name of ATTACHED_DERIVED_FILENAMES) {
26968
27217
  try {
26969
- rmSync(join3(dataDir2, name), { force: true });
27218
+ rmSync2(join3(dataDir2, name), { force: true });
26970
27219
  } catch {
26971
27220
  }
26972
27221
  }
26973
27222
  }
26974
27223
 
26975
27224
  // ../packages/persistence/src/control-plane-credential.ts
26976
- import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync as readFileSync3, rmSync as rmSync3, statSync } from "fs";
27225
+ import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync as readFileSync4, rmSync as rmSync4, statSync } from "fs";
26977
27226
  import { join as join4 } from "path";
26978
27227
 
26979
27228
  // ../packages/persistence/src/paths.ts
@@ -26981,10 +27230,10 @@ import {
26981
27230
  chmodSync,
26982
27231
  linkSync,
26983
27232
  lstatSync,
26984
- mkdirSync,
27233
+ mkdirSync as mkdirSync2,
26985
27234
  renameSync,
26986
- rmSync as rmSync2,
26987
- writeFileSync
27235
+ rmSync as rmSync3,
27236
+ writeFileSync as writeFileSync2
26988
27237
  } from "fs";
26989
27238
  import { threadId } from "worker_threads";
26990
27239
  var DATA_DIR_MODE = 448;
@@ -27008,7 +27257,7 @@ function tightenDir(dir) {
27008
27257
  chmodBestEffort(dir, DATA_DIR_MODE);
27009
27258
  }
27010
27259
  function mkdirOwnerOnlySync(dir, recursive2 = false) {
27011
- mkdirSync(dir, { recursive: recursive2, mode: DATA_DIR_MODE });
27260
+ mkdirSync2(dir, { recursive: recursive2, mode: DATA_DIR_MODE });
27012
27261
  }
27013
27262
  function ensureDataDirSync(dir) {
27014
27263
  mkdirOwnerOnlySync(dir, true);
@@ -27024,12 +27273,12 @@ function tightenPerms(file2) {
27024
27273
  for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
27025
27274
  }
27026
27275
  function writeExclusiveOwnerOnlySync(file2, data) {
27027
- writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
27276
+ writeFileSync2(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
27028
27277
  }
27029
27278
  function writeOwnerOnlyFileSync(file2, data) {
27030
27279
  const tmp = `${file2}.${String(process.pid)}.tmp`;
27031
27280
  try {
27032
- rmSync2(tmp, { force: true });
27281
+ rmSync3(tmp, { force: true });
27033
27282
  } catch {
27034
27283
  }
27035
27284
  try {
@@ -27037,7 +27286,7 @@ function writeOwnerOnlyFileSync(file2, data) {
27037
27286
  renameSync(tmp, file2);
27038
27287
  } finally {
27039
27288
  try {
27040
- rmSync2(tmp, { force: true });
27289
+ rmSync3(tmp, { force: true });
27041
27290
  } catch {
27042
27291
  }
27043
27292
  }
@@ -27066,7 +27315,7 @@ var KeyUnclaimableError = class extends Error {
27066
27315
  function createOwnerOnlyFileSync(file2, data) {
27067
27316
  const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
27068
27317
  try {
27069
- rmSync2(tmp, { force: true });
27318
+ rmSync3(tmp, { force: true });
27070
27319
  } catch {
27071
27320
  }
27072
27321
  let created;
@@ -27075,7 +27324,7 @@ function createOwnerOnlyFileSync(file2, data) {
27075
27324
  created = publishByLink(tmp, file2, data);
27076
27325
  } finally {
27077
27326
  try {
27078
- rmSync2(tmp, { force: true });
27327
+ rmSync3(tmp, { force: true });
27079
27328
  } catch {
27080
27329
  }
27081
27330
  }
@@ -27144,7 +27393,7 @@ function readControlPlaneCredentialFile(settingsDir2, connection) {
27144
27393
  if (gate === "absent") return { usable: false, reason: "absent" };
27145
27394
  if (gate === "untrusted") return { usable: false, reason: "untrusted-file" };
27146
27395
  try {
27147
- raw = readFileSync3(file2, "utf8");
27396
+ raw = readFileSync4(file2, "utf8");
27148
27397
  } catch (err) {
27149
27398
  const code = err.code;
27150
27399
  return { usable: false, reason: code === "ENOENT" ? "absent" : "unreadable" };
@@ -27170,6 +27419,10 @@ function readControlPlaneCredentialFile(settingsDir2, connection) {
27170
27419
  }
27171
27420
  return { usable: true, credential: result.data };
27172
27421
  }
27422
+ function readControlPlaneCredential(settingsDir2, connection) {
27423
+ const read = readControlPlaneCredentialFile(settingsDir2, connection);
27424
+ return read.usable ? read.credential : null;
27425
+ }
27173
27426
  function writeControlPlaneCredential(settingsDir2, credential) {
27174
27427
  if (!isSafeEndpoint(credential.endpoint)) {
27175
27428
  throw new Error(
@@ -27186,7 +27439,7 @@ function writeControlPlaneCredential(settingsDir2, credential) {
27186
27439
  function removeControlPlaneCredential(settingsDir2) {
27187
27440
  const file2 = controlPlaneCredentialPath(settingsDir2);
27188
27441
  const existed = lstatSync2(file2, { throwIfNoEntry: false }) !== void 0;
27189
- rmSync3(file2, { force: true });
27442
+ rmSync4(file2, { force: true });
27190
27443
  return existed;
27191
27444
  }
27192
27445
 
@@ -27196,9 +27449,9 @@ import { dirname as dirname4, join as join9, sep as sep2 } from "path";
27196
27449
  import { DatabaseSync } from "node:sqlite";
27197
27450
 
27198
27451
  // ../packages/persistence/src/ids.ts
27199
- import { createHash } from "crypto";
27452
+ import { createHash as createHash2 } from "crypto";
27200
27453
  function sha256Hex(input2) {
27201
- return createHash("sha256").update(input2).digest("hex");
27454
+ return createHash2("sha256").update(input2).digest("hex");
27202
27455
  }
27203
27456
  function inventoryId(objectType, identityKey) {
27204
27457
  return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
@@ -27238,7 +27491,7 @@ function captureWireId(sessionId, contentHash, filePath = null) {
27238
27491
 
27239
27492
  // ../packages/persistence/src/internal/snapshot.ts
27240
27493
  import { randomUUID } from "crypto";
27241
- import { existsSync as existsSync3, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync2 } from "fs";
27494
+ import { existsSync as existsSync3, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync2 } from "fs";
27242
27495
  import { basename, dirname as dirname3, join as join5 } from "path";
27243
27496
  function backupPath(file2, tag) {
27244
27497
  return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
@@ -27249,7 +27502,7 @@ var STAGED_NAME_SUFFIX = `.bak${SNAPSHOT_STAGING_SUFFIX}`;
27249
27502
  var SNAPSHOT_STAGING_COPY = "copy";
27250
27503
  function createSnapshotStaging(backup) {
27251
27504
  const stage = `${backup}${SNAPSHOT_STAGING_SUFFIX}`;
27252
- rmSync4(stage, { recursive: true, force: true });
27505
+ rmSync5(stage, { recursive: true, force: true });
27253
27506
  mkdirOwnerOnlySync(stage);
27254
27507
  tightenDir(stage);
27255
27508
  return { stage, copy: join5(stage, SNAPSHOT_STAGING_COPY) };
@@ -27278,7 +27531,7 @@ function reapStalePartials(file2) {
27278
27531
  try {
27279
27532
  const idle = idleMs(staging);
27280
27533
  if (idle !== null && idle > STALE_PARTIAL_MS) {
27281
- rmSync4(staging, { recursive: true, force: true });
27534
+ rmSync5(staging, { recursive: true, force: true });
27282
27535
  }
27283
27536
  } catch {
27284
27537
  }
@@ -27292,13 +27545,13 @@ function snapshotStore(db, backup) {
27292
27545
  renameSync2(copy, backup);
27293
27546
  } catch (error61) {
27294
27547
  try {
27295
- rmSync4(stage, { recursive: true, force: true });
27548
+ rmSync5(stage, { recursive: true, force: true });
27296
27549
  } catch {
27297
27550
  }
27298
27551
  throw error61;
27299
27552
  }
27300
27553
  try {
27301
- rmSync4(stage, { recursive: true, force: true });
27554
+ rmSync5(stage, { recursive: true, force: true });
27302
27555
  } catch {
27303
27556
  }
27304
27557
  }
@@ -27313,7 +27566,7 @@ function moveStoreAside(file2, backup) {
27313
27566
  renameSync2(sidecar, moved);
27314
27567
  undo.push([moved, sidecar]);
27315
27568
  } catch {
27316
- rmSync4(sidecar, { force: true });
27569
+ rmSync5(sidecar, { force: true });
27317
27570
  }
27318
27571
  }
27319
27572
  } catch (error61) {
@@ -27329,14 +27582,14 @@ function moveStoreAside(file2, backup) {
27329
27582
  }
27330
27583
  function discardStore(file2, backup) {
27331
27584
  try {
27332
- rmSync4(file2, { force: true });
27585
+ rmSync5(file2, { force: true });
27333
27586
  for (const sidecar of dbSidecars(file2)) {
27334
- rmSync4(sidecar, { force: true });
27587
+ rmSync5(sidecar, { force: true });
27335
27588
  }
27336
27589
  } catch (error61) {
27337
27590
  if (existsSync3(file2)) {
27338
27591
  try {
27339
- rmSync4(backup, { force: true });
27592
+ rmSync5(backup, { force: true });
27340
27593
  } catch {
27341
27594
  }
27342
27595
  }
@@ -28280,7 +28533,8 @@ var SqliteActivityRepository = class {
28280
28533
  SELECT 1 FROM audit_events d
28281
28534
  WHERE d.root_session_id = audit_events.id
28282
28535
  AND (d.content LIKE ? ESCAPE '\\'
28283
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
28536
+ OR coalesce(json_extract(d.attributes, '$.detail'),
28537
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
28284
28538
  );
28285
28539
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
28286
28540
  }
@@ -29618,23 +29872,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
29618
29872
  )`;
29619
29873
 
29620
29874
  // ../packages/persistence/src/repositories/findings.ts
29621
- var PREVIEW_INSTANCES_PER_GROUP = 200;
29622
- var DEFAULT_LOCATIONS_LIMIT = 100;
29623
- var LOCATION_RULE_IDS_CAP = 20;
29624
- function compareLocationOrder(a, b) {
29625
- return compareFindingGroupOrder(
29626
- {
29627
- severity: a.maxSeverity,
29628
- latestDetectedAt: a.latestDetectedAt,
29629
- id: ""
29630
- },
29631
- {
29632
- severity: b.maxSeverity,
29633
- latestDetectedAt: b.latestDetectedAt,
29634
- id: ""
29635
- }
29636
- );
29637
- }
29638
29875
  var CONCAT_SEP = ",";
29639
29876
  var TUPLE_SEP = "|";
29640
29877
  function splitConcat(value) {
@@ -29686,13 +29923,48 @@ function decodeGroupCursor(cursor) {
29686
29923
  return null;
29687
29924
  }
29688
29925
  function firstAfter(sorted, cursor) {
29689
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
29926
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
29690
29927
  return index === -1 ? sorted.length : index;
29691
29928
  }
29692
29929
  function findDeepLinked(sorted, page, id) {
29693
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
29694
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
29930
+ if (page.some((t) => t.id === id)) return void 0;
29931
+ return sorted.find((t) => t.id === id);
29932
+ }
29933
+ function encodeLocationCursor(location) {
29934
+ const payload = {
29935
+ sev: location.maxSeverity,
29936
+ t: location.latestDetectedAt,
29937
+ r: location.repo,
29938
+ f: location.file
29939
+ };
29940
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
29695
29941
  }
29942
+ function decodeLocationCursor(cursor) {
29943
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
29944
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
29945
+ return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
29946
+ }
29947
+ return null;
29948
+ }
29949
+ function firstLocationAfter(sorted, cursor) {
29950
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
29951
+ return index === -1 ? sorted.length : index;
29952
+ }
29953
+ function findDeepLinkedLocation(sorted, page, id) {
29954
+ if (page.some((l) => l.id === id)) return void 0;
29955
+ return sorted.find((l) => l.id === id);
29956
+ }
29957
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
29958
+ d.severity AS severity, f.masked_match AS masked_match,
29959
+ f.action_taken AS action_taken, f.confidence AS confidence,
29960
+ e.started_at AS occurred_at,
29961
+ e.source_tool AS source_tool,
29962
+ e.repo AS repo,
29963
+ e.file_path AS file,
29964
+ e.tool_name AS tool_name,
29965
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
29966
+ e.event_type AS kind, f.finding_key AS finding_key,
29967
+ ${latestResolutionStatusSql("f")} AS latest_status`;
29696
29968
  var DAY_MS3 = 864e5;
29697
29969
  var SqliteFindingsRepository = class {
29698
29970
  constructor(db) {
@@ -29813,30 +30085,26 @@ var SqliteFindingsRepository = class {
29813
30085
  );
29814
30086
  }
29815
30087
  /**
29816
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
29817
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
29818
- * attributes bag, rule_id/category/severity from the definition), scoped to
29819
- * the four capture kinds (audit_events also holds structural/reconciler/scan
29820
- * rows this list must never surface), groups by ruleId, computes
29821
- * per-filter-excluded facets, applies the requested filters, and sorts by
29822
- * severity then recency. Filtering
29823
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
29824
- * reflect the full filtered set; `items` is the requested
29825
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
29826
- * filter, `totals.findings` counts only instances whose derived status was
29827
- * requested, and each item's instance preview is narrowed the same way.
30088
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
30089
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
30090
+ * list must never surface), with per-filter-excluded facets, the requested
30091
+ * filters applied, and sorted by severity then recency. Filtering and faceting
30092
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
30093
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
30094
+ * Under a `status` filter, `totals.findings` counts only findings whose
30095
+ * derived status was requested.
30096
+ *
30097
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
30098
+ * folding EVERY finding into the numbers a type row and the filters need
30099
+ * (count, severity, category, providers, actions, statuses, latest, search
30100
+ * text). The findings OF a type come from listFindingInstances scoped to
30101
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
29828
30102
  *
29829
- * Two reads, neither of which materializes a row per finding:
29830
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
29831
- * the group and the filters need (count, providers, actions, statuses,
29832
- * latest, search text);
29833
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
29834
- * populate `instances` for the table's expanded rows.
29835
30103
  * The aggregates carry raw DB values and are translated by the same
29836
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
29837
- * rule is ever restated in SQL.
30104
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
30105
+ * status rule is ever restated in SQL.
29838
30106
  */
29839
- listGroupedFindings(query) {
30107
+ listFindingTypes(query) {
29840
30108
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
29841
30109
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
29842
30110
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -29849,12 +30117,7 @@ var SqliteFindingsRepository = class {
29849
30117
  predicate,
29850
30118
  params: sessionParams
29851
30119
  });
29852
- const rows = this.previewRows(aggregates, {
29853
- sessionId: query.sessionId,
29854
- from: query.from
29855
- });
29856
- const groupable = rows.map(toFlatFindingRow);
29857
- const allGroups = buildFindingGroups(groupable, { aggregates });
30120
+ const allTypes = buildFindingTypes(aggregates);
29858
30121
  const filterOpts = {
29859
30122
  severity: query.severity,
29860
30123
  providers: query.provider,
@@ -29863,30 +30126,25 @@ var SqliteFindingsRepository = class {
29863
30126
  subtype: query.subtype,
29864
30127
  q: query.q
29865
30128
  };
29866
- const facets = computeFindingFacets(allGroups, filterOpts);
29867
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
30129
+ const facets = computeFindingFacets(allTypes, filterOpts);
30130
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
29868
30131
  const statusFilter = query.status ?? [];
29869
30132
  const totals = {
29870
- findings: sorted.reduce((acc, g) => {
29871
- if (statusFilter.length === 0) return acc + g.instanceCount;
29872
- const agg = aggregates.get(g.id);
29873
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
30133
+ findings: sorted.reduce((acc, t) => {
30134
+ if (statusFilter.length === 0) return acc + t.instanceCount;
30135
+ const agg = aggregates.get(t.id);
30136
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
29874
30137
  }, 0),
29875
- groups: sorted.length
30138
+ types: sorted.length
29876
30139
  };
29877
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
30140
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
29878
30141
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
29879
30142
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
29880
30143
  const page = sorted.slice(start, start + limit);
29881
30144
  const lastOnPage = page.at(-1);
29882
30145
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
29883
30146
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
29884
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
29885
- const narrow = (g) => statusSet ? {
29886
- ...g,
29887
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
29888
- } : g;
29889
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
30147
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
29890
30148
  return Promise.resolve({
29891
30149
  totals,
29892
30150
  facets,
@@ -29897,7 +30155,7 @@ var SqliteFindingsRepository = class {
29897
30155
  }
29898
30156
  /**
29899
30157
  * One row per rule_id, folding EVERY instance of the group into the values
29900
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
30158
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
29901
30159
  * distinct rule_ids (the installed packs' rules), not by the store's size.
29902
30160
  *
29903
30161
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -29991,13 +30249,25 @@ var SqliteFindingsRepository = class {
29991
30249
  });
29992
30250
  }
29993
30251
  /**
29994
- * The same findings folded by location: repository, then file within it.
30252
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
29995
30253
  *
29996
30254
  * The grouping keys come from the capturing event's attributes, which is what
29997
- * the local store relates a finding to — there is no finding↔asset row to
29998
- * group by instead. A repo or file the event did not record folds into the
29999
- * empty-string bucket, which the view renders but does not link, since no
30000
- * filter can name it.
30255
+ * the local store relates a finding to; there is no finding↔asset row to group
30256
+ * by instead. A repo or file the event did not record folds into the
30257
+ * empty-string bucket, which is a real location like any other: it is listed,
30258
+ * it is selectable, and its `?loc=` token is as good as any other row's.
30259
+ *
30260
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
30261
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
30262
+ * list was rebuilt to remove — and two-level pagination inside an
30263
+ * expand/collapse table is what pushed that view to master/detail in the first
30264
+ * place.
30265
+ *
30266
+ * Every filter narrows the FINDINGS and the locations fall out of what
30267
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
30268
+ * reports for the same filters scoped to that pair. The view depends on it:
30269
+ * one toolbar sits over both panels precisely because a location owns none of
30270
+ * its fields.
30001
30271
  */
30002
30272
  listFindingLocations(query) {
30003
30273
  const opts = {
@@ -30009,13 +30279,16 @@ var SqliteFindingsRepository = class {
30009
30279
  tools: query.tool,
30010
30280
  q: query.q
30011
30281
  };
30012
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
30282
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
30283
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
30013
30284
  const byRepo = /* @__PURE__ */ new Map();
30285
+ const accumulator = createInstanceFacetAccumulator(opts);
30014
30286
  let total = 0;
30015
30287
  for (const row of this.scanFindingRows({
30016
30288
  sessionId: query.sessionId,
30017
30289
  from: query.from
30018
30290
  })) {
30291
+ accumulator.add(row);
30019
30292
  if (!matchesInstanceFilters(row, opts)) continue;
30020
30293
  total += 1;
30021
30294
  let files = byRepo.get(row.repo);
@@ -30030,103 +30303,35 @@ var SqliteFindingsRepository = class {
30030
30303
  }
30031
30304
  addToLocation(acc, row);
30032
30305
  }
30033
- let fileCount = 0;
30034
- const repos = [...byRepo.entries()].map(([repo, files]) => {
30035
- fileCount += files.size;
30036
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
30037
- file: file2,
30038
- instanceCount: acc.instanceCount,
30039
- maxSeverity: acc.maxSeverity,
30040
- latestDetectedAt: acc.latestDetectedAt,
30041
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
30042
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
30043
- })).sort(compareLocationOrder);
30044
- const rollup = fileRows.reduce(
30045
- (a, f) => ({
30046
- instanceCount: a.instanceCount + f.instanceCount,
30047
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
30048
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
30049
- }),
30050
- {
30051
- instanceCount: 0,
30052
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
30053
- latestDetectedAt: ""
30054
- }
30055
- );
30056
- const statuses = fileRows.map((f) => f.status);
30057
- const folded = foldGroupStatus(statuses);
30058
- return {
30059
- repo,
30060
- instanceCount: rollup.instanceCount,
30061
- maxSeverity: rollup.maxSeverity,
30062
- latestDetectedAt: rollup.latestDetectedAt,
30063
- ...folded === void 0 ? {} : { status: folded },
30064
- files: fileRows
30065
- };
30066
- });
30067
- repos.sort(compareLocationOrder);
30306
+ const sorted = [];
30307
+ for (const [repo, files] of byRepo) {
30308
+ for (const [file2, acc] of files) {
30309
+ const status = foldGroupStatus(acc.statuses);
30310
+ sorted.push({
30311
+ id: encodeLocationId(repo, file2),
30312
+ repo,
30313
+ file: file2,
30314
+ instanceCount: acc.instanceCount,
30315
+ maxSeverity: acc.maxSeverity,
30316
+ latestDetectedAt: acc.latestDetectedAt,
30317
+ ...status === void 0 ? {} : { status },
30318
+ ruleIds: [...acc.ruleIds]
30319
+ });
30320
+ }
30321
+ }
30322
+ sorted.sort(compareLocationOrder);
30323
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
30324
+ const page = sorted.slice(start, start + limit);
30325
+ const lastOnPage = page.at(-1);
30326
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
30327
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
30068
30328
  return Promise.resolve({
30069
- totals: { findings: total, repos: repos.length, files: fileCount },
30070
- items: repos.slice(0, limit),
30071
- hasMore: repos.length > limit
30329
+ totals: { findings: total, locations: sorted.length },
30330
+ facets: accumulator.facets(),
30331
+ items: [...page, ...deepLinked ? [deepLinked] : []],
30332
+ nextCursor
30072
30333
  });
30073
30334
  }
30074
- /**
30075
- * Each group's newest instances, for the table's expanded rows.
30076
- *
30077
- * ONE index-ordered scan with early termination, and the shape is the point.
30078
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
30079
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
30080
- * through a temp B-tree to keep a bounded preview of each group, and then
30081
- * sorts the survivors again for the page order. Both sorts grow with the
30082
- * store while the answer does not.
30083
- *
30084
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
30085
- * (or the session or window index the scope names — see `findingScanSql`),
30086
- * which is already the order the page wants, and keeps rows per rule until
30087
- * each rule has as many as it can show. The aggregate the caller already holds
30088
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
30089
- * per rule, summed, is the number of rows this scan has to find, and it stops
30090
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
30091
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
30092
- * store with many firing rules widens it. The bound that DOES hold
30093
- * unconditionally is the sorted form's floor: this scan visits at most as
30094
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
30095
- * sorted, and stops the moment every rule has its cap, where the sorted form
30096
- * sorts the whole scope regardless. The true worst case — the rarest rule's
30097
- * wanted instances sitting at the tail of the scope — is one pass over
30098
- * everything in scope with a block sort of the id tie-break only, never a
30099
- * sort of the scope, which is still that floor.
30100
- *
30101
- * A row whose rule the aggregate did not see is skipped: the two statements
30102
- * run without a shared snapshot, so a capture landing between them can add a
30103
- * rule here that has no counts there, and the counts are what the group is
30104
- * built from.
30105
- */
30106
- previewRows(aggregates, scope) {
30107
- const wanted = /* @__PURE__ */ new Map();
30108
- let remaining = 0;
30109
- for (const [ruleId, agg] of aggregates) {
30110
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
30111
- wanted.set(ruleId, n);
30112
- remaining += n;
30113
- }
30114
- const rows = [];
30115
- if (remaining === 0) return rows;
30116
- const { sql, params } = this.findingScanSql(scope);
30117
- const taken = /* @__PURE__ */ new Map();
30118
- for (const r of iterateRows(this.db.prepare(sql), params)) {
30119
- const want = wanted.get(r.rule_id);
30120
- if (want === void 0) continue;
30121
- const have = taken.get(r.rule_id) ?? 0;
30122
- if (have >= want) continue;
30123
- taken.set(r.rule_id, have + 1);
30124
- rows.push(r);
30125
- remaining -= 1;
30126
- if (remaining === 0) break;
30127
- }
30128
- return rows;
30129
- }
30130
30335
  /**
30131
30336
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
30132
30337
  *
@@ -30153,6 +30358,33 @@ var SqliteFindingsRepository = class {
30153
30358
  yield toFlatFindingRow(r);
30154
30359
  }
30155
30360
  }
30361
+ /**
30362
+ * One finding by its own id, or null when no such row exists.
30363
+ *
30364
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
30365
+ * the store — and, unlike anything derived from a list page, it resolves a
30366
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
30367
+ * deep link needs: the id it carries may name a finding thousands of rows
30368
+ * older than anything a first page holds.
30369
+ *
30370
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
30371
+ * RESOLVES an id; whether that row would survive the list's current filters is
30372
+ * a different question, and hiding the target because a filter excludes it is
30373
+ * worse than showing it.
30374
+ *
30375
+ * `groupId` on the result IS the rule id, so this one read answers both "which
30376
+ * type should the list select?" and "what does the drawer show?".
30377
+ */
30378
+ findingInstance(id) {
30379
+ const row = this.db.prepare(
30380
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
30381
+ FROM inspection_findings f
30382
+ JOIN audit_events e ON e.id = f.audit_event_id
30383
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
30384
+ WHERE f.id = ?`
30385
+ ).get(id);
30386
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
30387
+ }
30156
30388
  /**
30157
30389
  * The one statement both instance-level scans run: every finding in scope,
30158
30390
  * joined to its event and definition, newest first.
@@ -30186,17 +30418,7 @@ var SqliteFindingsRepository = class {
30186
30418
  conditions.push("e.started_at >= ?");
30187
30419
  params.push(isoToEpochMillis(scope.from));
30188
30420
  }
30189
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
30190
- d.severity AS severity, f.masked_match AS masked_match,
30191
- f.action_taken AS action_taken, f.confidence AS confidence,
30192
- e.started_at AS occurred_at,
30193
- e.source_tool AS source_tool,
30194
- e.repo AS repo,
30195
- e.file_path AS file,
30196
- e.tool_name AS tool_name,
30197
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
30198
- e.event_type AS kind, f.finding_key AS finding_key,
30199
- ${latestResolutionStatusSql("f")} AS latest_status
30421
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
30200
30422
  FROM audit_events e
30201
30423
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
30202
30424
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -30210,6 +30432,26 @@ var SqliteFindingsRepository = class {
30210
30432
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
30211
30433
  const rows = this.db.prepare(
30212
30434
  `SELECT rule_id,
30435
+ -- BARE columns beside max(latest_at), which is deliberate and
30436
+ -- is SQLite's documented behaviour: with a single min()/max()
30437
+ -- in an aggregate query, every bare column takes its value from
30438
+ -- the row that produced the extremum. So these are the severity
30439
+ -- and category of the definition whose finding is NEWEST, which
30440
+ -- is what the row-based build they replaced read off its first
30441
+ -- (newest-first) row.
30442
+ --
30443
+ -- min() is WRONG here and was the defect: inspection_definitions
30444
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
30445
+ -- mints a new row), so a rule whose severity moved between
30446
+ -- versions has several, and min() picks the ALPHABETICALLY
30447
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
30448
+ -- That is arbitrary in direction, and it feeds the badge, the
30449
+ -- filter, the facet counts and the primary sort key.
30450
+ --
30451
+ -- Adding a second min()/max() aggregate here would make these
30452
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
30453
+ severity,
30454
+ category,
30213
30455
  sum(tuple_count) AS instance_count,
30214
30456
  max(latest_at) AS latest_at,
30215
30457
  group_concat(source_tools) AS source_tools,
@@ -30220,6 +30462,14 @@ var SqliteFindingsRepository = class {
30220
30462
  group_concat(tool_names) AS tool_names
30221
30463
  FROM (
30222
30464
  SELECT d.rule_id AS rule_id,
30465
+ -- Severity and category are columns of the DEFINITION, and
30466
+ -- a rule can have SEVERAL definitions (one per version), so
30467
+ -- these are grouped on below and resolved to the newest
30468
+ -- firing version by the outer query's bare-column select.
30469
+ -- They ride the aggregate because the type build has no rows
30470
+ -- to read them off \u2014 see buildFindingTypes.
30471
+ d.severity AS severity,
30472
+ d.category AS category,
30223
30473
  e.event_type || '${TUPLE_SEP}' ||
30224
30474
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
30225
30475
  coalesce(latest.status, '') AS status_tuple,
@@ -30234,7 +30484,7 @@ var SqliteFindingsRepository = class {
30234
30484
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
30235
30485
  ON latest.finding_key = f.finding_key
30236
30486
  ${scope.predicate}
30237
- GROUP BY d.rule_id, status_tuple
30487
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
30238
30488
  )
30239
30489
  GROUP BY rule_id`
30240
30490
  ).all(scope.params);
@@ -30243,6 +30493,8 @@ var SqliteFindingsRepository = class {
30243
30493
  r.rule_id,
30244
30494
  {
30245
30495
  instanceCount: r.instance_count,
30496
+ severity: r.severity,
30497
+ category: r.category,
30246
30498
  sourceTools: splitConcat(r.source_tools),
30247
30499
  actionsTaken: splitConcat(r.actions_taken),
30248
30500
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -30259,7 +30511,7 @@ var SqliteFindingsRepository = class {
30259
30511
  latestDetectedAt: epochMillisToIso(r.latest_at),
30260
30512
  // Free text only — joined and substring-matched, so group_concat's
30261
30513
  // commas need no unpicking (a repo/path containing one still matches).
30262
- // Left undefined (not '') when unfetched, so buildFindingGroups can
30514
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
30263
30515
  // tell "no q this request" from "a group with no repo/file at all"
30264
30516
  // and skip priming a haystack nothing will read.
30265
30517
  ...withSearchText ? {
@@ -30411,6 +30663,12 @@ var SqliteHistorySyncRepository = class {
30411
30663
  this.markOwedStmt = db.prepare(
30412
30664
  `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
30413
30665
  );
30666
+ this.markCaptureBacklogOwedStmt = db.prepare(
30667
+ `UPDATE audit_events SET outbox_owed = 1
30668
+ WHERE synced_at IS NULL
30669
+ AND event_type IN (${CAPTURE_TYPE_LIST})
30670
+ AND started_at < :before`
30671
+ );
30414
30672
  this.stampStmt = db.prepare(
30415
30673
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
30416
30674
  );
@@ -30459,7 +30717,9 @@ var SqliteHistorySyncRepository = class {
30459
30717
  );
30460
30718
  this.disownCapturesStmt = db.prepare(
30461
30719
  `UPDATE audit_events SET outbox_owed = NULL
30462
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
30720
+ WHERE outbox_owed IS NOT NULL
30721
+ AND event_type IN (${CAPTURE_TYPE_LIST})
30722
+ AND started_at < :attachedAt`
30463
30723
  );
30464
30724
  this.rearmStmt = db.prepare(
30465
30725
  `UPDATE audit_events SET synced_at = NULL
@@ -30535,6 +30795,7 @@ var SqliteHistorySyncRepository = class {
30535
30795
  freezeBoundaryStmt;
30536
30796
  captureRowsStmt;
30537
30797
  markOwedStmt;
30798
+ markCaptureBacklogOwedStmt;
30538
30799
  captureSkipCountStmt;
30539
30800
  disownCapturesStmt;
30540
30801
  partitionStmt;
@@ -30598,6 +30859,23 @@ var SqliteHistorySyncRepository = class {
30598
30859
  markCaptureOwed(id) {
30599
30860
  this.markOwedStmt.run({ id });
30600
30861
  }
30862
+ /**
30863
+ * Mark every capture already on disk as owed, as of `before`.
30864
+ *
30865
+ * The consent-time backfill, called once from `aka attach` when a human
30866
+ * grants existing-history consent — never from an ongoing drain pass, and
30867
+ * never inferred from a boundary that could later move. `before` is the
30868
+ * caller's own "now" at the moment consent was granted, so what this marks
30869
+ * is exactly the backlog the consent prompt already counted, not whatever a
30870
+ * later re-attach or key rotation might widen it to.
30871
+ *
30872
+ * Returns how many rows matched, for the caller to log or test against. Not a
30873
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
30874
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
30875
+ */
30876
+ markCaptureBacklogOwed(before) {
30877
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
30878
+ }
30601
30879
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
30602
30880
  markSynced(ids, atMs) {
30603
30881
  this.stampAll(ids, atMs);
@@ -30714,15 +30992,42 @@ var SqliteHistorySyncRepository = class {
30714
30992
  *
30715
30993
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
30716
30994
  * machine has just left are undelivered as far as the new one is concerned.
30717
- * All three in one transaction, so a crash between them cannot leave stamps
30718
- * attributed to the wrong deployment, or a boundary that belongs to another.
30995
+ * All four in one transaction, so a crash between them cannot leave stamps
30996
+ * attributed to the wrong deployment, a boundary that belongs to another, or
30997
+ * a disown with no re-mark to follow it.
30719
30998
  *
30720
30999
  * The boundary is written HERE and only here, which is what freezes it: a
30721
31000
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
30722
31001
  * unchanged, so this never runs and the backlog does not widen back over rows
30723
31002
  * the live path has since delivered.
31003
+ *
31004
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
31005
+ * granted existing-history consent for the deployment this call is arming —
31006
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
31007
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
31008
+ * apart. Passed only when that grant is valid, since this method has no way
31009
+ * to check consent itself and must not mark a row owed for a machine that
31010
+ * never agreed to it. Applied AFTER the disown above, in the SAME
31011
+ * transaction: what the disown clears is every marker below `backlogBefore`,
31012
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
31013
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
31014
+ * on the cleared side of that bound — and the re-mark in the same
31015
+ * transaction is what puts those rows back. A crash between the two cannot
31016
+ * strand the ledger disowned with nothing re-marked — the transaction either
31017
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
31018
+ * committed re-enters this method on the very next pass. Omit it (the
31019
+ * structural-only tests do) to exercise the disown in isolation.
31020
+ *
31021
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
31022
+ * touching a marker the NEW deployment's OWN live path has already set: B's
31023
+ * live path can mark a capture owed from the moment `aka attach` writes the
31024
+ * descriptor, before the drain's first pass ever reaches this method, and
31025
+ * such a row sits at or after the bound rather than below it. What keeps the
31026
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
31027
+ * bound — disown runs first, re-mark second, both inside the one
31028
+ * transaction above.
30724
31029
  */
30725
- rearmFor(fingerprint, backlogBefore) {
31030
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
30726
31031
  this.ensureRowStmt.run();
30727
31032
  withTransaction(
30728
31033
  this.db,
@@ -30730,7 +31035,10 @@ var SqliteHistorySyncRepository = class {
30730
31035
  const previous = getRow(this.fingerprintStmt)?.fingerprint;
30731
31036
  this.rearmStmt.run();
30732
31037
  if (previous !== null && previous !== void 0 && previous !== fingerprint) {
30733
- this.disownCapturesStmt.run();
31038
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
31039
+ }
31040
+ if (backfillCapturesBefore !== void 0) {
31041
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
30734
31042
  }
30735
31043
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
30736
31044
  },
@@ -30928,19 +31236,19 @@ var SqliteInspectionFindingsRepository = class {
30928
31236
  };
30929
31237
 
30930
31238
  // ../packages/persistence/src/repositories/installed-packs.ts
30931
- import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
31239
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
30932
31240
 
30933
31241
  // ../packages/persistence/src/policy-floor.ts
30934
- import { readFileSync as readFileSync7 } from "fs";
31242
+ import { readFileSync as readFileSync8 } from "fs";
30935
31243
  import { join as join8 } from "path";
30936
31244
 
30937
31245
  // ../packages/persistence/src/local-layout.ts
30938
31246
  import { renameSync as renameSync3 } from "fs";
30939
31247
  import { mkdir } from "fs/promises";
30940
- import { homedir as homedir3 } from "os";
31248
+ import { homedir as homedir4 } from "os";
30941
31249
  import { join as join6 } from "path";
30942
31250
  function defaultDataDir() {
30943
- return join6(homedir3(), ".aka");
31251
+ return join6(homedir4(), ".aka");
30944
31252
  }
30945
31253
  function settingsDir(base = defaultDataDir()) {
30946
31254
  return join6(base, "settings");
@@ -30963,7 +31271,7 @@ function ensureLayoutDirSync(dir = defaultDataDir()) {
30963
31271
  }
30964
31272
 
30965
31273
  // ../packages/persistence/src/settings.ts
30966
- import { readFileSync as readFileSync6 } from "fs";
31274
+ import { readFileSync as readFileSync7 } from "fs";
30967
31275
  import { join as join7 } from "path";
30968
31276
 
30969
31277
  // ../packages/persistence/src/file-lock.ts
@@ -30972,10 +31280,10 @@ import {
30972
31280
  closeSync,
30973
31281
  existsSync as existsSync4,
30974
31282
  openSync,
30975
- readFileSync as readFileSync4,
30976
- rmSync as rmSync5,
31283
+ readFileSync as readFileSync5,
31284
+ rmSync as rmSync6,
30977
31285
  statSync as statSync3,
30978
- writeFileSync as writeFileSync2
31286
+ writeFileSync as writeFileSync3
30979
31287
  } from "fs";
30980
31288
  import { hostname as hostname3 } from "os";
30981
31289
  var LOCK_SUFFIX = ".lock";
@@ -31008,7 +31316,7 @@ function lockPathFor(file2) {
31008
31316
  function readLockBody(lock) {
31009
31317
  let raw;
31010
31318
  try {
31011
- raw = readFileSync4(lock, "utf8");
31319
+ raw = readFileSync5(lock, "utf8");
31012
31320
  } catch {
31013
31321
  return null;
31014
31322
  }
@@ -31039,7 +31347,7 @@ function directoryAcceptsCreates(lock) {
31039
31347
  return false;
31040
31348
  } finally {
31041
31349
  try {
31042
- rmSync5(probe, { force: true });
31350
+ rmSync6(probe, { force: true });
31043
31351
  } catch {
31044
31352
  }
31045
31353
  }
@@ -31065,14 +31373,14 @@ function tryAcquire(lock, file2) {
31065
31373
  }
31066
31374
  const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
31067
31375
  try {
31068
- writeFileSync2(fd, `${JSON.stringify(body)}
31376
+ writeFileSync3(fd, `${JSON.stringify(body)}
31069
31377
  `);
31070
31378
  } catch {
31071
31379
  try {
31072
31380
  closeSync(fd);
31073
31381
  } catch {
31074
31382
  }
31075
- rmSync5(lock, { force: true });
31383
+ rmSync6(lock, { force: true });
31076
31384
  return null;
31077
31385
  }
31078
31386
  try {
@@ -31108,13 +31416,13 @@ function breakIfStale(lock, staleMs) {
31108
31416
  }
31109
31417
  try {
31110
31418
  if (!existsSync4(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
31111
- rmSync5(lock, { force: true });
31419
+ rmSync6(lock, { force: true });
31112
31420
  return true;
31113
31421
  } catch {
31114
31422
  return false;
31115
31423
  } finally {
31116
31424
  try {
31117
- rmSync5(breaker2, { force: true });
31425
+ rmSync6(breaker2, { force: true });
31118
31426
  } catch {
31119
31427
  }
31120
31428
  }
@@ -31123,7 +31431,7 @@ var BREAKER_ABANDONED_MS = 1e4;
31123
31431
  function reapAbandonedBreaker(breaker2) {
31124
31432
  try {
31125
31433
  if (Date.now() - statSync3(breaker2).mtimeMs >= BREAKER_ABANDONED_MS) {
31126
- rmSync5(breaker2, { force: true });
31434
+ rmSync6(breaker2, { force: true });
31127
31435
  }
31128
31436
  } catch {
31129
31437
  }
@@ -31134,7 +31442,7 @@ function abandonWindow(staleMs) {
31134
31442
  function release(lock, token) {
31135
31443
  try {
31136
31444
  if (readLockBody(lock)?.token !== token) return;
31137
- rmSync5(lock, { force: true });
31445
+ rmSync6(lock, { force: true });
31138
31446
  } catch {
31139
31447
  }
31140
31448
  }
@@ -31181,25 +31489,26 @@ function isThenable(value) {
31181
31489
  }
31182
31490
 
31183
31491
  // ../packages/persistence/src/managed-settings.ts
31184
- import { readFileSync as readFileSync5 } from "fs";
31185
- import { posix, win32 } from "path";
31492
+ import { readFileSync as readFileSync6 } from "fs";
31493
+ import { posix as posix2, win32 } from "path";
31186
31494
  function managedSettingsPaths(platform3 = process.platform) {
31187
31495
  if (platform3 === "darwin") {
31188
31496
  return [
31189
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31190
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31497
+ posix2.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31498
+ posix2.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31191
31499
  ];
31192
31500
  }
31193
31501
  if (platform3 === "win32") {
31194
31502
  return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31195
31503
  }
31196
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31504
+ return [posix2.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31197
31505
  }
31198
- function readManagedSettings(paths = managedSettingsPaths()) {
31506
+ var testOnlyManagedPaths = null;
31507
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
31199
31508
  for (const path of paths) {
31200
31509
  let text;
31201
31510
  try {
31202
- text = readFileSync5(path, "utf8");
31511
+ text = readFileSync6(path, "utf8");
31203
31512
  } catch {
31204
31513
  continue;
31205
31514
  }
@@ -31215,7 +31524,8 @@ function managedContextOf(managed) {
31215
31524
  return {
31216
31525
  present: true,
31217
31526
  ...managed.organization === void 0 ? {} : { organization: managed.organization },
31218
- lockedFields: managed.lockedFields
31527
+ lockedFields: managed.lockedFields,
31528
+ ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedFields: managed.unknownLockedFields }
31219
31529
  };
31220
31530
  }
31221
31531
  function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
@@ -31368,7 +31678,7 @@ function applyOnboarding(answers, base = defaultDataDir(), managedOverride) {
31368
31678
  function readJson(file2) {
31369
31679
  let text;
31370
31680
  try {
31371
- text = readFileSync6(file2, "utf8");
31681
+ text = readFileSync7(file2, "utf8");
31372
31682
  } catch {
31373
31683
  return null;
31374
31684
  }
@@ -31408,7 +31718,7 @@ var PolicyFloorError = class extends Error {
31408
31718
  };
31409
31719
  function readCachedPolicyBundle(base = defaultDataDir()) {
31410
31720
  try {
31411
- const raw = readFileSync7(join8(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
31721
+ const raw = readFileSync8(join8(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
31412
31722
  const parsed2 = JSON.parse(raw);
31413
31723
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
31414
31724
  return PolicyBundle.parse(parsed2.bundle);
@@ -31532,7 +31842,7 @@ function inventorySignature(packs2) {
31532
31842
  return packs2.map((p) => `${p.namespace}/${p.packId}@${p.version}#${hashRules(p.rulesJson)}`).sort().join(",");
31533
31843
  }
31534
31844
  function hashRules(rulesJson) {
31535
- return createHash2("sha256").update(rulesJson).digest("hex");
31845
+ return createHash3("sha256").update(rulesJson).digest("hex");
31536
31846
  }
31537
31847
  function parseVersion(v) {
31538
31848
  const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
@@ -33780,7 +34090,7 @@ function toUtcDateString(ms) {
33780
34090
  return new Date(ms).toISOString().slice(0, 10);
33781
34091
  }
33782
34092
  function isTimeseriesSeverity(s) {
33783
- return s === "critical" || s === "high" || s === "medium";
34093
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
33784
34094
  }
33785
34095
  var SqliteSecurityRepository = class {
33786
34096
  constructor(db, now = () => Date.now()) {
@@ -33909,12 +34219,16 @@ var SqliteSecurityRepository = class {
33909
34219
  const now = this.now();
33910
34220
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
33911
34221
  const rows = this.findingsInRange(windowStart, now);
33912
- const points = Array.from({ length: numBuckets }, (_, i) => ({
33913
- timestamp: toUtcDateString(windowStart + i * bucketMs),
33914
- critical: 0,
33915
- high: 0,
33916
- medium: 0
33917
- }));
34222
+ const points = Array.from(
34223
+ { length: numBuckets },
34224
+ (_, i) => ({
34225
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
34226
+ critical: 0,
34227
+ high: 0,
34228
+ medium: 0,
34229
+ low: 0
34230
+ })
34231
+ );
33918
34232
  for (const r of rows) {
33919
34233
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
33920
34234
  const bucket = points[idx];
@@ -34132,6 +34446,7 @@ var SqliteSecurityRepository = class {
34132
34446
  `SELECT f.finding_key AS finding_key,
34133
34447
  d.rule_id AS rule_id,
34134
34448
  d.severity AS severity,
34449
+ e.repo AS repo,
34135
34450
  e.file_path AS path,
34136
34451
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
34137
34452
  latest.resolved_at AS latest_resolved_at
@@ -34151,6 +34466,7 @@ var SqliteSecurityRepository = class {
34151
34466
  const items = rows.map((r) => ({
34152
34467
  findingKey: r.finding_key,
34153
34468
  ruleId: r.rule_id,
34469
+ repo: r.repo ?? "",
34154
34470
  severity: r.severity,
34155
34471
  path: r.path ?? "",
34156
34472
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -34160,13 +34476,66 @@ var SqliteSecurityRepository = class {
34160
34476
  }));
34161
34477
  return Promise.resolve({ items });
34162
34478
  }
34479
+ /**
34480
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
34481
+ *
34482
+ * Scoped by status rather than by time, because the card this feeds is a to-do
34483
+ * list: a secret committed three weeks ago and never rotated is still the most
34484
+ * important thing to fix, and any window hides it. It carried a "newest N
34485
+ * findings" cap and then a range; the first meant a different span on every
34486
+ * machine, and the second reported "no recommendations" over live exposure.
34487
+ *
34488
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
34489
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
34490
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
34491
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
34492
+ * The two answer different questions and only this one has to match a link.
34493
+ *
34494
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
34495
+ * whole-store scope costs a grouped scan rather than a row per finding.
34496
+ */
34497
+ recommendationInputs() {
34498
+ const rows = allRows(
34499
+ this.db.prepare(
34500
+ `SELECT d.rule_id AS rule_id,
34501
+ d.category AS category,
34502
+ d.severity AS severity,
34503
+ COUNT(*) AS count
34504
+ FROM inspection_findings f
34505
+ JOIN audit_events e ON e.id = f.audit_event_id
34506
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
34507
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
34508
+ ON latest.finding_key = f.finding_key
34509
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
34510
+ AND e.event_type = 'code_change'
34511
+ AND (
34512
+ f.finding_key IS NULL
34513
+ OR latest.status IS NULL
34514
+ OR latest.status NOT IN ('resolved', 'dismissed')
34515
+ )
34516
+ GROUP BY d.rule_id, d.category, d.severity`
34517
+ )
34518
+ );
34519
+ return Promise.resolve(
34520
+ rows.map((r) => ({
34521
+ ruleId: r.rule_id,
34522
+ category: r.category,
34523
+ severity: r.severity,
34524
+ count: r.count
34525
+ }))
34526
+ );
34527
+ }
34163
34528
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
34164
34529
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
34165
34530
  // numeric and the JS aggregations bucket/split on ms directly.
34166
34531
  findingsInRange(fromMs, toMs) {
34167
34532
  const rows = allRows(
34168
34533
  this.db.prepare(
34169
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
34534
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
34535
+ // joined for `severity`, so they are two more columns off a row this read
34536
+ // already fetches. They feed the recommended-actions rollup.
34537
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
34538
+ d.rule_id AS rule_id, d.category AS category
34170
34539
  FROM inspection_findings f
34171
34540
  JOIN audit_events e ON e.id = f.audit_event_id
34172
34541
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -34179,7 +34548,9 @@ var SqliteSecurityRepository = class {
34179
34548
  return rows.map((r) => ({
34180
34549
  occurredAt: r.occurred_at,
34181
34550
  severity: r.severity,
34182
- actionTaken: r.action_taken
34551
+ actionTaken: r.action_taken,
34552
+ ruleId: r.rule_id,
34553
+ category: r.category
34183
34554
  }));
34184
34555
  }
34185
34556
  };
@@ -35349,19 +35720,56 @@ function openLocalDatabase(dir) {
35349
35720
  };
35350
35721
  }
35351
35722
 
35723
+ // ../packages/persistence/src/egress-wire.ts
35724
+ import { createHash as createHash4 } from "crypto";
35725
+ function hashProjectKey(projectKey) {
35726
+ return createHash4("sha256").update(projectKey, "utf8").digest("hex");
35727
+ }
35728
+ function toIngestHit(hit) {
35729
+ return {
35730
+ host: hit.host,
35731
+ kind: hit.kind,
35732
+ name: hit.name,
35733
+ category: hit.category,
35734
+ trust: hit.trust,
35735
+ network: hit.network,
35736
+ method: hit.method,
35737
+ transport: hit.transport,
35738
+ url: hit.url,
35739
+ template: hit.template,
35740
+ dataClass: hit.dataClass,
35741
+ site: {
35742
+ file: hit.site.file,
35743
+ line: hit.site.line,
35744
+ dynamic: hit.site.dynamic,
35745
+ vendored: hit.site.vendored
35746
+ }
35747
+ };
35748
+ }
35749
+ function toEgressIngestRequest(input2) {
35750
+ const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
35751
+ const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
35752
+ return {
35753
+ projectKey: hashProjectKey(input2.projectKey),
35754
+ project: input2.project,
35755
+ reconcile,
35756
+ hits: hits.map(toIngestHit)
35757
+ };
35758
+ }
35759
+
35352
35760
  // ../packages/persistence/src/finding-key.ts
35353
- import { createHash as createHash3 } from "crypto";
35761
+ import { createHash as createHash5 } from "crypto";
35354
35762
  function normalizeFilePath(filePath) {
35355
35763
  return filePath.replaceAll("\\", "/");
35356
35764
  }
35357
35765
  function computeFindingKey(input2) {
35358
35766
  const normalizedPath = normalizeFilePath(input2.filePath);
35359
- return createHash3("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
35767
+ return createHash5("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
35360
35768
  }
35361
35769
 
35362
35770
  // ../packages/persistence/src/fingerprint.ts
35363
35771
  import { createHmac, randomBytes } from "crypto";
35364
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
35772
+ import { existsSync as existsSync5, readFileSync as readFileSync9 } from "fs";
35365
35773
  import { join as join10 } from "path";
35366
35774
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
35367
35775
  var EXCEPTION_KEY_FILENAME = "exception.key";
@@ -35466,7 +35874,7 @@ function occupantMessage(file2, kind) {
35466
35874
  function readFingerprintKey(dataDir2) {
35467
35875
  let raw;
35468
35876
  try {
35469
- raw = readFileSync8(keyFilePath(dataDir2), "utf8");
35877
+ raw = readFileSync9(keyFilePath(dataDir2), "utf8");
35470
35878
  } catch (err) {
35471
35879
  if (err.code === "ENOENT") return null;
35472
35880
  throw err instanceof Error ? err : new Error(String(err));
@@ -35501,15 +35909,31 @@ function keyStateOf(key) {
35501
35909
  return key === null ? { status: "absent" } : { status: "present", version: key.version };
35502
35910
  }
35503
35911
 
35504
- // ../packages/persistence/src/history-preview.ts
35912
+ // ../packages/persistence/src/history-backfill.ts
35505
35913
  import { existsSync as existsSync6 } from "fs";
35506
35914
  import { join as join11 } from "path";
35915
+ function seedCaptureBacklogOwed(dataDir2, beforeMs) {
35916
+ if (!existsSync6(join11(dataDir2, DB_FILENAME))) return;
35917
+ try {
35918
+ const db = openLocalDatabase(dataDir2);
35919
+ try {
35920
+ db.historySync.markCaptureBacklogOwed(beforeMs);
35921
+ } finally {
35922
+ db.close();
35923
+ }
35924
+ } catch {
35925
+ }
35926
+ }
35927
+
35928
+ // ../packages/persistence/src/history-preview.ts
35929
+ import { existsSync as existsSync7 } from "fs";
35930
+ import { join as join12 } from "path";
35507
35931
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
35508
35932
  var PREVIEW_BUSY_TIMEOUT_MS = 250;
35509
35933
  var MS_PER_DAY = 864e5;
35510
35934
  function readLocalHistoryPreview(dataDir2, nowMs = Date.now()) {
35511
- const file2 = join11(dataDir2, DB_FILENAME);
35512
- if (!existsSync6(file2)) return { sessions: 0, days: 0 };
35935
+ const file2 = join12(dataDir2, DB_FILENAME);
35936
+ if (!existsSync7(file2)) return { sessions: 0, days: 0 };
35513
35937
  let db;
35514
35938
  try {
35515
35939
  db = new DatabaseSync3(file2, { readOnly: true });
@@ -35535,8 +35959,8 @@ function readLocalHistoryPreview(dataDir2, nowMs = Date.now()) {
35535
35959
  }
35536
35960
 
35537
35961
  // ../packages/persistence/src/store-symlinks.ts
35538
- import { existsSync as existsSync7, lstatSync as lstatSync3, readlinkSync, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
35539
- import { dirname as dirname5, join as join12, resolve } from "path";
35962
+ import { existsSync as existsSync8, lstatSync as lstatSync3, readlinkSync, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
35963
+ import { dirname as dirname5, join as join13, resolve } from "path";
35540
35964
  var STORE_DB = "the store database (including the prompt corpus)";
35541
35965
  var STORE_SETTINGS = "your settings file";
35542
35966
  function storeContents(home) {
@@ -35545,7 +35969,7 @@ function storeContents(home) {
35545
35969
  [settingsDir(home), STORE_SETTINGS],
35546
35970
  [dataDir(home), STORE_DB],
35547
35971
  [keysDir(home), "the vault key"],
35548
- [join12(settingsDir(home), "settings.json"), STORE_SETTINGS],
35972
+ [join13(settingsDir(home), "settings.json"), STORE_SETTINGS],
35549
35973
  [dbPath(home), STORE_DB]
35550
35974
  ]);
35551
35975
  }
@@ -35563,7 +35987,7 @@ function symlinkedStorePaths(home, platform3 = process.platform) {
35563
35987
  holds,
35564
35988
  // existsSync follows the link, so a target that is gone reads as
35565
35989
  // absent here while lstat above still sees the link itself.
35566
- missing: !existsSync7(path),
35990
+ missing: !existsSync8(path),
35567
35991
  mode: targetMode(path, platform3)
35568
35992
  }
35569
35993
  ];
@@ -35699,8 +36123,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
35699
36123
  // ../packages/persistence/src/vault/key-provider.ts
35700
36124
  import { execFileSync as execFileSync2 } from "child_process";
35701
36125
  import { randomBytes as randomBytes2 } from "crypto";
35702
- import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
35703
- import { join as join13 } from "path";
36126
+ import { chmodSync as chmodSync3, readFileSync as readFileSync10, renameSync as renameSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
36127
+ import { join as join14 } from "path";
35704
36128
  var VAULT_OCCUPANT_REASON = {
35705
36129
  symlink: "the path is a symlink; remove it so a keyring can be created",
35706
36130
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -35799,16 +36223,16 @@ function claimRotationLock(lock, owner) {
35799
36223
  throw asError(err);
35800
36224
  }
35801
36225
  try {
35802
- writeFileSync3(join13(lock, LOCK_OWNER_FILE), `${owner}
36226
+ writeFileSync4(join14(lock, LOCK_OWNER_FILE), `${owner}
35803
36227
  `, { mode: DATA_FILE_MODE });
35804
36228
  return true;
35805
36229
  } catch (err) {
35806
- rmSync6(lock, { recursive: true, force: true });
36230
+ rmSync7(lock, { recursive: true, force: true });
35807
36231
  throw asError(err);
35808
36232
  }
35809
36233
  }
35810
36234
  function acquireRotationLock(keysDir2) {
35811
- const lock = join13(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
36235
+ const lock = join14(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
35812
36236
  const owner = randomBytes2(16).toString("hex");
35813
36237
  if (claimRotationLock(lock, owner)) return { lock, owner };
35814
36238
  let held;
@@ -35829,17 +36253,17 @@ function acquireRotationLock(keysDir2) {
35829
36253
  if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
35830
36254
  throw new Error(ROTATION_IN_PROGRESS, { cause: err });
35831
36255
  }
35832
- rmSync6(aside, { recursive: true, force: true });
36256
+ rmSync7(aside, { recursive: true, force: true });
35833
36257
  if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
35834
36258
  return { lock, owner };
35835
36259
  }
35836
36260
  function releaseRotationLock(lease) {
35837
36261
  try {
35838
- if (readFileSync9(join13(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
36262
+ if (readFileSync10(join14(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
35839
36263
  } catch {
35840
36264
  return;
35841
36265
  }
35842
- rmSync6(lease.lock, { recursive: true, force: true });
36266
+ rmSync7(lease.lock, { recursive: true, force: true });
35843
36267
  }
35844
36268
  function withRotationLock(keysDir2, work) {
35845
36269
  ensureDataDirSync(keysDir2);
@@ -35856,7 +36280,7 @@ var FileKeyProvider = class {
35856
36280
  this.#keysDir = keysDir2;
35857
36281
  }
35858
36282
  get filePath() {
35859
- return join13(this.#keysDir, VAULT_KEY_FILENAME);
36283
+ return join14(this.#keysDir, VAULT_KEY_FILENAME);
35860
36284
  }
35861
36285
  loadOrCreate() {
35862
36286
  return asAsync(() => {
@@ -35886,7 +36310,7 @@ var FileKeyProvider = class {
35886
36310
  #read() {
35887
36311
  let raw;
35888
36312
  try {
35889
- raw = readFileSync9(this.filePath, "utf8");
36313
+ raw = readFileSync10(this.filePath, "utf8");
35890
36314
  } catch (err) {
35891
36315
  if (err.code === "ENOENT") return null;
35892
36316
  throw err instanceof Error ? err : new Error(String(err));
@@ -36521,12 +36945,12 @@ var SecretVault = class {
36521
36945
  };
36522
36946
 
36523
36947
  // ../packages/persistence/src/warn-era-cap.ts
36524
- import { existsSync as existsSync8, writeFileSync as writeFileSync4 } from "fs";
36525
- import { join as join14 } from "path";
36948
+ import { existsSync as existsSync9, writeFileSync as writeFileSync5 } from "fs";
36949
+ import { join as join15 } from "path";
36526
36950
 
36527
36951
  // ../packages/plugin-sdk/src/config.ts
36528
- import { existsSync as existsSync9 } from "fs";
36529
- import { join as join15 } from "path";
36952
+ import { existsSync as existsSync10 } from "fs";
36953
+ import { join as join16 } from "path";
36530
36954
 
36531
36955
  // ../packages/plugin-sdk/src/provider-env.ts
36532
36956
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -36547,9 +36971,9 @@ var providerEnvShape = {
36547
36971
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
36548
36972
 
36549
36973
  // ../packages/plugin-sdk/src/config-inventory.ts
36550
- import { readdirSync as readdirSync3, readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
36551
- import { homedir as homedir4 } from "os";
36552
- import { basename as basename3, join as join17 } from "path";
36974
+ import { readdirSync as readdirSync3, readFileSync as readFileSync12, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
36975
+ import { homedir as homedir5 } from "os";
36976
+ import { basename as basename3, join as join18 } from "path";
36553
36977
 
36554
36978
  // ../rules/code-flaws/auth-jwt-no-verify.json
36555
36979
  var auth_jwt_no_verify_default = {
@@ -38584,8 +39008,8 @@ function bundledDetections() {
38584
39008
  }
38585
39009
 
38586
39010
  // ../packages/plugin-sdk/src/repo.ts
38587
- import { existsSync as existsSync10, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
38588
- import { basename as basename2, dirname as dirname6, isAbsolute, join as join16, sep as sep3 } from "path";
39011
+ import { existsSync as existsSync11, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
39012
+ import { basename as basename2, dirname as dirname6, isAbsolute, join as join17, sep as sep3 } from "path";
38589
39013
  function resolveRepoIdentity(cwd) {
38590
39014
  try {
38591
39015
  const root = findGitRoot(cwd);
@@ -38623,36 +39047,36 @@ function resolveHeadRoot(cwd) {
38623
39047
  function findGitRoot(start) {
38624
39048
  let dir = start;
38625
39049
  for (; ; ) {
38626
- if (existsSync10(join16(dir, ".git"))) return dir;
39050
+ if (existsSync11(join17(dir, ".git"))) return dir;
38627
39051
  const parent = dirname6(dir);
38628
39052
  if (parent === dir) return void 0;
38629
39053
  dir = parent;
38630
39054
  }
38631
39055
  }
38632
39056
  function resolveGitContext(root) {
38633
- const dotGit = join16(root, ".git");
39057
+ const dotGit = join17(root, ".git");
38634
39058
  try {
38635
39059
  if (statSync6(dotGit).isDirectory()) {
38636
- return { configPath: join16(dotGit, "config"), headRoot: root };
39060
+ return { configPath: join17(dotGit, "config"), headRoot: root };
38637
39061
  }
38638
39062
  } catch {
38639
39063
  return void 0;
38640
39064
  }
38641
39065
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
38642
39066
  if (!target) return void 0;
38643
- const gitdir = isAbsolute(target) ? target : join16(root, target);
38644
- if (existsSync10(join16(gitdir, "config"))) {
38645
- return { configPath: join16(gitdir, "config"), headRoot: root };
39067
+ const gitdir = isAbsolute(target) ? target : join17(root, target);
39068
+ if (existsSync11(join17(gitdir, "config"))) {
39069
+ return { configPath: join17(gitdir, "config"), headRoot: root };
38646
39070
  }
38647
- const commonRaw = safeRead(join16(gitdir, "commondir"))?.trim();
39071
+ const commonRaw = safeRead(join17(gitdir, "commondir"))?.trim();
38648
39072
  if (!commonRaw) return void 0;
38649
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join16(gitdir, commonRaw);
39073
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join17(gitdir, commonRaw);
38650
39074
  const headRoot = basename2(commonGitDir) === ".git" ? dirname6(commonGitDir) : root;
38651
- return { configPath: join16(commonGitDir, "config"), headRoot };
39075
+ return { configPath: join17(commonGitDir, "config"), headRoot };
38652
39076
  }
38653
39077
  function safeRead(path) {
38654
39078
  try {
38655
- return readFileSync10(path, "utf8");
39079
+ return readFileSync11(path, "utf8");
38656
39080
  } catch {
38657
39081
  return void 0;
38658
39082
  }
@@ -38690,20 +39114,104 @@ function slugFromUrl(url2) {
38690
39114
  }
38691
39115
 
38692
39116
  // ../packages/plugin-sdk/src/events.ts
38693
- import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
39117
+ import { createHash as createHash6, randomUUID as randomUUID13 } from "crypto";
38694
39118
 
38695
39119
  // ../packages/plugin-sdk/src/isolated-scan.ts
38696
- import { existsSync as existsSync11 } from "fs";
39120
+ import { existsSync as existsSync12 } from "fs";
38697
39121
  import { fileURLToPath as fileURLToPath2 } from "url";
38698
39122
  import { Worker } from "worker_threads";
38699
39123
 
39124
+ // ../packages/plugin-sdk/src/host-floor.ts
39125
+ import { readFileSync as readFileSync14 } from "fs";
39126
+ import { join as join20 } from "path";
39127
+
39128
+ // ../packages/plugin-sdk/src/model-governance.ts
39129
+ import {
39130
+ closeSync as closeSync2,
39131
+ fstatSync,
39132
+ mkdirSync as mkdirSync3,
39133
+ openSync as openSync2,
39134
+ readFileSync as readFileSync13,
39135
+ readSync,
39136
+ writeFileSync as writeFileSync6
39137
+ } from "fs";
39138
+ import { join as join19 } from "path";
39139
+ var TAIL_BYTES = 256 * 1024;
39140
+
39141
+ // ../packages/plugin-sdk/src/host-floor.ts
39142
+ var HOST_FEATURE = {
39143
+ ModelSwitch: "model-switch",
39144
+ VaultPointerDisplay: "vault-pointer-display"
39145
+ };
39146
+ var HOST_FLOORS = {
39147
+ [HOST_FEATURE.ModelSwitch]: {
39148
+ label: "model-switch protection",
39149
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
39150
+ since: "2.1.251"
39151
+ },
39152
+ [HOST_FEATURE.VaultPointerDisplay]: {
39153
+ label: "vault pointer display",
39154
+ hookEvents: ["MessageDisplay"],
39155
+ since: "2.1.152"
39156
+ }
39157
+ };
39158
+ function hostFloorGaps(hostVersion) {
39159
+ if (hostVersion === void 0) return [];
39160
+ const gaps = [];
39161
+ for (const [feature, row] of Object.entries(HOST_FLOORS)) {
39162
+ if (compareBinaryVersions(hostVersion, row.since) < 0) {
39163
+ gaps.push({ feature, label: row.label, since: row.since });
39164
+ }
39165
+ }
39166
+ return gaps;
39167
+ }
39168
+ function requiredHostVersion(gaps) {
39169
+ let highest;
39170
+ for (const gap of gaps) {
39171
+ if (highest === void 0 || compareBinaryVersions(gap.since, highest) > 0) highest = gap.since;
39172
+ }
39173
+ return highest;
39174
+ }
39175
+ var MAX_TESTED_HOST = "2.1.260";
39176
+ function hostCeilingNotice(hostVersion) {
39177
+ if (hostVersion === void 0) return null;
39178
+ if (compareBinaryVersions(hostVersion, MAX_TESTED_HOST) <= 0) return null;
39179
+ return `This Claude Code (${hostVersion}) is newer than AKA has been tested against (${MAX_TESTED_HOST}). If something here looks wrong, that is the likely cause \u2014 we'll look into it.`;
39180
+ }
39181
+ function hostCompatibilityLines(cache) {
39182
+ if (cache === null) return [];
39183
+ const lines = [`Claude Code: ${cache.version} (last seen)`];
39184
+ const gaps = hostFloorGaps(cache.version);
39185
+ const required2 = requiredHostVersion(gaps);
39186
+ if (required2 !== void 0) {
39187
+ lines.push(` inactive: ${gaps.map((g) => g.label).join(", ")}`);
39188
+ lines.push(` update Claude Code to ${required2} or newer to turn them on`);
39189
+ }
39190
+ const ceiling = hostCeilingNotice(cache.version);
39191
+ if (ceiling !== null) lines.push(` ${ceiling}`);
39192
+ return lines;
39193
+ }
39194
+ var HOST_VERSION_MARKER = "host-version.json";
39195
+ function readHostVersionCache(dataDir2) {
39196
+ try {
39197
+ const parsed2 = JSON.parse(readFileSync14(join20(dataDir2, HOST_VERSION_MARKER), "utf8"));
39198
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
39199
+ const { version: version2, observedAt } = parsed2;
39200
+ if (typeof version2 !== "string" || !isParseableBinaryVersion(version2)) return null;
39201
+ if (typeof observedAt !== "number" || !Number.isFinite(observedAt)) return null;
39202
+ return { version: version2, observedAt };
39203
+ } catch {
39204
+ return null;
39205
+ }
39206
+ }
39207
+
38700
39208
  // ../packages/plugin-sdk/src/ignore-layers.ts
38701
39209
  var import_ignore = __toESM(require_ignore(), 1);
38702
- import { readFileSync as readFileSync12 } from "fs";
38703
- import { join as join18 } from "path";
39210
+ import { readFileSync as readFileSync15 } from "fs";
39211
+ import { join as join21 } from "path";
38704
39212
  function readIgnoreLayer(dir, filename, anchorLen) {
38705
39213
  try {
38706
- return { matcher: (0, import_ignore.default)().add(readFileSync12(join18(dir, filename), "utf8")), anchorLen };
39214
+ return { matcher: (0, import_ignore.default)().add(readFileSync15(join21(dir, filename), "utf8")), anchorLen };
38707
39215
  } catch {
38708
39216
  return void 0;
38709
39217
  }
@@ -38733,22 +39241,9 @@ function withLayer(layers, layer) {
38733
39241
  // ../packages/plugin-sdk/src/inventory-resolver.ts
38734
39242
  import { arch, hostname as hostname4, platform, release as release2 } from "os";
38735
39243
 
38736
- // ../packages/plugin-sdk/src/model-governance.ts
38737
- import {
38738
- closeSync as closeSync2,
38739
- fstatSync,
38740
- mkdirSync as mkdirSync2,
38741
- openSync as openSync2,
38742
- readFileSync as readFileSync13,
38743
- readSync,
38744
- writeFileSync as writeFileSync5
38745
- } from "fs";
38746
- import { join as join19 } from "path";
38747
- var TAIL_BYTES = 256 * 1024;
38748
-
38749
39244
  // ../packages/plugin-sdk/src/nudge.ts
38750
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
38751
- import { join as join20 } from "path";
39245
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
39246
+ import { join as join22 } from "path";
38752
39247
 
38753
39248
  // ../packages/plugin-sdk/src/paths.ts
38754
39249
  import { readdirSync as readdirSync4, realpathSync as realpathSync4 } from "fs";
@@ -38785,8 +39280,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
38785
39280
  }
38786
39281
 
38787
39282
  // ../packages/plugin-sdk/src/project-files.ts
38788
- import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
38789
- import { basename as basename5, join as join21 } from "path";
39283
+ import { existsSync as existsSync13, readdirSync as readdirSync5 } from "fs";
39284
+ import { basename as basename5, join as join23 } from "path";
38790
39285
  var SKIP_DIRS = /* @__PURE__ */ new Set([
38791
39286
  ".git",
38792
39287
  "node_modules",
@@ -38883,8 +39378,8 @@ function resolveProjectFiles(cwd, opts = {}) {
38883
39378
  }
38884
39379
  if (entry.isDirectory()) {
38885
39380
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, dirRel, entry.name, true)) continue;
38886
- const fullPath = join21(dir, entry.name);
38887
- if (existsSync12(join21(fullPath, ".git"))) continue;
39381
+ const fullPath = join23(dir, entry.name);
39382
+ if (existsSync13(join23(fullPath, ".git"))) continue;
38888
39383
  if (depth >= bounds.maxDepth) {
38889
39384
  walk.omitted = true;
38890
39385
  continue;
@@ -38956,8 +39451,8 @@ import { randomUUID as randomUUID14 } from "crypto";
38956
39451
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
38957
39452
 
38958
39453
  // ../packages/plugin-sdk/src/throttle.ts
38959
- import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
38960
- import { join as join22 } from "path";
39454
+ import { mkdirSync as mkdirSync5, statSync as statSync8, writeFileSync as writeFileSync8 } from "fs";
39455
+ import { join as join24 } from "path";
38961
39456
 
38962
39457
  // ../packages/local-ops/src/egress-record.ts
38963
39458
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
@@ -38991,24 +39486,68 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
38991
39486
  if (file2.startsWith(".") || file2.includes("/.")) continue;
38992
39487
  files.push({ ...hit, file: file2, vendored: isVendoredPath(file2) });
38993
39488
  }
38994
- const summary = db.shares.recordProjectEgress({
39489
+ const input2 = {
38995
39490
  projectKey,
38996
39491
  project,
38997
39492
  projectId,
38998
39493
  reconcile: { mode: "walk", walkedPrefix: toPosix(relative(root, abs)) },
38999
39494
  hits: resolveEgress(files)
39000
- });
39001
- return { ...summary, project };
39495
+ };
39496
+ const summary = db.shares.recordProjectEgress(input2);
39497
+ return { ...summary, project: input2.project, input: input2 };
39002
39498
  } catch {
39003
39499
  return null;
39004
39500
  }
39005
39501
  }
39006
39502
 
39007
39503
  // ../packages/local-ops/src/fs-scan.ts
39008
- import { createHash as createHash5, randomUUID as randomUUID15 } from "crypto";
39009
- import { readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync10 } from "fs";
39010
- import { basename as basename6, extname, join as join23, resolve as resolve3 } from "path";
39504
+ import { createHash as createHash7, randomUUID as randomUUID15 } from "crypto";
39505
+ import { readdirSync as readdirSync6, readFileSync as readFileSync17, realpathSync as realpathSync5, statSync as statSync10 } from "fs";
39506
+ import { homedir as homedir6 } from "os";
39507
+ import { basename as basename6, extname, join as join25, resolve as resolve3, sep as sep5 } from "path";
39011
39508
  var AKAIGNORE_FILENAME = ".akaignore";
39509
+ function protectedPaths(home, akaHome) {
39510
+ const akaHomeDirname = basename6(defaultDataDir());
39511
+ return [
39512
+ ...new Set(
39513
+ [
39514
+ resolve3(home, ".claude", "ide"),
39515
+ resolve3(home, ".claude", ".credentials.json"),
39516
+ resolve3(home, ".codex", "auth.json"),
39517
+ // The home this invocation was pointed at, the machine's own default,
39518
+ // and the default under whichever OS home this call was given — a
39519
+ // caller that redirects `home` gets that home's AKA directory covered
39520
+ // with it. All three are the same path for a caller on the real home.
39521
+ resolve3(akaHome),
39522
+ defaultDataDir(),
39523
+ resolve3(home, akaHomeDirname)
39524
+ ].map(canonicalize)
39525
+ )
39526
+ ];
39527
+ }
39528
+ function canonicalize(path) {
39529
+ try {
39530
+ return realpathSync5.native(path);
39531
+ } catch {
39532
+ return resolve3(path);
39533
+ }
39534
+ }
39535
+ function isProtected(canonical, roots) {
39536
+ return roots.some((root) => canonical === root || canonical.startsWith(root + sep5));
39537
+ }
39538
+ var PROTECTED_TARGET_ERROR_CODE = "AKA_PROTECTED_SCAN_TARGET";
39539
+ var ProtectedTargetError = class extends Error {
39540
+ code = PROTECTED_TARGET_ERROR_CODE;
39541
+ path;
39542
+ constructor(path) {
39543
+ super(`refusing to scan ${path}: it holds credentials this scanner must never read`);
39544
+ this.name = "ProtectedTargetError";
39545
+ this.path = path;
39546
+ }
39547
+ };
39548
+ function isProtectedTarget(err) {
39549
+ return typeof err === "object" && err !== null && err.code === PROTECTED_TARGET_ERROR_CODE;
39550
+ }
39012
39551
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
39013
39552
  "node_modules",
39014
39553
  ".git",
@@ -39020,21 +39559,24 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
39020
39559
  "out"
39021
39560
  ]);
39022
39561
  var MAX_BYTES = 1e6;
39023
- function* collectFiles(target) {
39562
+ function* collectFiles(target, opts = {}) {
39563
+ const protectedRoots = protectedPaths(opts.home ?? homedir6(), opts.akaHome ?? defaultDataDir());
39564
+ const canonicalTarget = canonicalize(target);
39024
39565
  let st;
39025
39566
  try {
39026
39567
  st = statSync10(target);
39027
39568
  } catch {
39028
39569
  return;
39029
39570
  }
39571
+ if (isProtected(canonicalTarget, protectedRoots)) throw new ProtectedTargetError(target);
39030
39572
  if (st.isFile()) {
39031
39573
  if (st.size <= MAX_BYTES) yield { path: target, gitignored: false };
39032
39574
  return;
39033
39575
  }
39034
39576
  if (!st.isDirectory()) return;
39035
- yield* visit2(target, "", [], [], false);
39577
+ yield* visit2(target, canonicalTarget, "", [], [], false, protectedRoots);
39036
39578
  }
39037
- function* visit2(dir, dirRel, markLayers, skipLayers, inIgnoredDir) {
39579
+ function* visit2(dir, dirCanonical, dirRel, markLayers, skipLayers, inIgnoredDir, protectedRoots) {
39038
39580
  let dirents;
39039
39581
  try {
39040
39582
  dirents = readdirSync6(dir, { withFileTypes: true });
@@ -39051,15 +39593,26 @@ function* visit2(dir, dirRel, markLayers, skipLayers, inIgnoredDir) {
39051
39593
  readIgnoreLayer(dir, AKAIGNORE_FILENAME, dirRel.length)
39052
39594
  );
39053
39595
  for (const entry of dirents) {
39054
- const path = join23(dir, entry.name);
39596
+ const path = join25(dir, entry.name);
39597
+ const canonical = dirCanonical === dir ? path : join25(dirCanonical, entry.name);
39055
39598
  if (entry.isDirectory()) {
39599
+ if (isProtected(canonical, protectedRoots)) continue;
39056
39600
  const skipState = evaluateIgnore(dirSkipLayers, dirRel, entry.name, true);
39057
39601
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
39058
39602
  continue;
39059
39603
  }
39060
39604
  const dirIgnored = inIgnoredDir || evaluateIgnore(dirMarkLayers, dirRel, entry.name, true) === "ignored";
39061
- yield* visit2(path, childRel(dirRel, entry.name), dirMarkLayers, dirSkipLayers, dirIgnored);
39605
+ yield* visit2(
39606
+ path,
39607
+ canonical,
39608
+ childRel(dirRel, entry.name),
39609
+ dirMarkLayers,
39610
+ dirSkipLayers,
39611
+ dirIgnored,
39612
+ protectedRoots
39613
+ );
39062
39614
  } else if (entry.isFile()) {
39615
+ if (isProtected(canonical, protectedRoots)) continue;
39063
39616
  if (evaluateIgnore(dirSkipLayers, dirRel, entry.name, false) === "ignored") continue;
39064
39617
  let size;
39065
39618
  try {
@@ -39102,10 +39655,12 @@ async function scanPathIntoStore(db, target, opts = {}) {
39102
39655
  }
39103
39656
  return fingerprintKey;
39104
39657
  }
39105
- for (const { path: file2, gitignored } of collectFiles(resolve3(target))) {
39658
+ for (const { path: file2, gitignored } of collectFiles(resolve3(target), {
39659
+ akaHome: opts.akaHome
39660
+ })) {
39106
39661
  let text;
39107
39662
  try {
39108
- text = readFileSync15(file2, "utf8");
39663
+ text = readFileSync17(file2, "utf8");
39109
39664
  } catch {
39110
39665
  continue;
39111
39666
  }
@@ -39128,7 +39683,7 @@ async function scanPathIntoStore(db, target, opts = {}) {
39128
39683
  sourceTool: opts.sourceTool ?? SOURCE_TOOL.Cli,
39129
39684
  kind: "code_change",
39130
39685
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
39131
- contentHash: createHash5("sha256").update(text).digest("hex"),
39686
+ contentHash: createHash7("sha256").update(text).digest("hex"),
39132
39687
  // Only the enforced spans. `redact` folds overlapping findings into one
39133
39688
  // disjoint region, so narrowing its input narrows those regions too —
39134
39689
  // which is the intended reading rather than a hazard: an enforced span is
@@ -39202,19 +39757,53 @@ function recordProjectInventory(db, target) {
39202
39757
  }
39203
39758
  }
39204
39759
 
39760
+ // ../packages/local-ops/src/shares-forward.ts
39761
+ var FORWARD_FAILURE_LINES = {
39762
+ unauthorized: "key rejected; re-attach with a valid plugin key",
39763
+ forbidden: "key is valid but not permitted for Data Shares ingest; a key minted before Data Shares ingest existed needs a re-attach, otherwise ask your org admin",
39764
+ "route-absent": "the deployment predates Data Shares ingest; upgrade it, then re-run the scan",
39765
+ "invalid-request": "this build assembled a request the contract refuses; please report it",
39766
+ rejected: "the deployment refused the request body; this build and the deployment are out of step \u2014 upgrade one of them",
39767
+ unreachable: "control plane unreachable (timeout or server error); the next scan retries"
39768
+ };
39769
+ async function forwardProjectEgress(base, input2, deps) {
39770
+ let endpointName = null;
39771
+ try {
39772
+ const settings = readWorkspaceSettings(base);
39773
+ const connection = settings.controlPlane;
39774
+ if (!isAttached(settings) || connection === void 0) return { status: "not-attached" };
39775
+ const endpoint = controlPlaneName(connection);
39776
+ endpointName = endpoint;
39777
+ if (deps.enabled === false) return { status: "disabled", endpoint, reason: "opt-out" };
39778
+ if (!settings.dataSharesInPlace) {
39779
+ return { status: "disabled", endpoint, reason: "data-shares-off" };
39780
+ }
39781
+ const credential = readControlPlaneCredential(settingsDir(base), connection);
39782
+ if (credential === null) return { status: "no-credential", endpoint };
39783
+ const request = toEgressIngestRequest(input2);
39784
+ const result = await deps.send(
39785
+ { endpoint: connection.endpoint, apiKey: credential.apiKey },
39786
+ request
39787
+ );
39788
+ return result.ok ? { status: "forwarded", endpoint, callSites: request.hits.length } : { status: "failed", endpoint, kind: result.kind };
39789
+ } catch {
39790
+ return endpointName === null ? { status: "not-attached" } : { status: "failed", endpoint: endpointName, kind: "unreachable" };
39791
+ }
39792
+ }
39793
+
39205
39794
  // ../packages/local-ops/src/update-cache.ts
39206
39795
  import { spawn } from "child_process";
39207
- import { existsSync as existsSync13, readFileSync as readFileSync16, rmSync as rmSync7 } from "fs";
39208
- import { join as join24 } from "path";
39796
+ import { existsSync as existsSync14, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
39797
+ import { join as join26 } from "path";
39209
39798
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
39210
39799
  function cachePath(home) {
39211
- return join24(dataDir(home), "update-check.json");
39800
+ return join26(dataDir(home), "update-check.json");
39212
39801
  }
39213
39802
  function readCache(home) {
39214
39803
  const path = cachePath(home);
39215
- if (!existsSync13(path)) return null;
39804
+ if (!existsSync14(path)) return null;
39216
39805
  try {
39217
- const raw = JSON.parse(readFileSync16(path, "utf8"));
39806
+ const raw = JSON.parse(readFileSync18(path, "utf8"));
39218
39807
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
39219
39808
  return raw;
39220
39809
  } catch {
@@ -39222,7 +39811,7 @@ function readCache(home) {
39222
39811
  }
39223
39812
  }
39224
39813
  function writeCache(home, cache) {
39225
- if (!existsSync13(dataDir(home))) return;
39814
+ if (!existsSync14(dataDir(home))) return;
39226
39815
  try {
39227
39816
  writeOwnerOnlyFileSync(cachePath(home), `${JSON.stringify(cache, null, 2)}
39228
39817
  `);
@@ -39234,7 +39823,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
39234
39823
  }
39235
39824
  function clearCache(home) {
39236
39825
  try {
39237
- rmSync7(cachePath(home), { force: true });
39826
+ rmSync8(cachePath(home), { force: true });
39238
39827
  } catch {
39239
39828
  }
39240
39829
  }
@@ -39336,7 +39925,13 @@ var COMMAND_SPECS = [
39336
39925
  name: "scan",
39337
39926
  argHint: "[path]",
39338
39927
  summary: "Scan a file or directory and record findings (default: .)",
39339
- completesFiles: true
39928
+ completesFiles: true,
39929
+ flags: [
39930
+ {
39931
+ name: "--no-forward",
39932
+ summary: "Record locally only; skip forwarding the Data Shares register to an attached deployment."
39933
+ }
39934
+ ]
39340
39935
  },
39341
39936
  { name: "stats", summary: "Print findings / enforcement / detections from the local store" },
39342
39937
  {
@@ -39410,12 +40005,15 @@ var GLOBAL_FLAGS = ["--home", "--no-update-check", "--version", "--help"];
39410
40005
  function commandsHelp() {
39411
40006
  const label = (s) => s.argHint ? `${s.name} ${s.argHint}` : s.name;
39412
40007
  const width = Math.max(...COMMAND_SPECS.map((s) => label(s).length)) + 1;
39413
- return COMMAND_SPECS.map((s) => ` ${label(s).padEnd(width)}${s.summary}`).join("\n");
40008
+ return COMMAND_SPECS.flatMap((s) => [
40009
+ ` ${label(s).padEnd(width)}${s.summary}`,
40010
+ // Under its own command, indented past the summary column, because a
40011
+ // command-only flag listed in the shared Options block would read as one
40012
+ // every command takes.
40013
+ ...(s.flags ?? []).map((f) => ` ${"".padEnd(width)}${f.name} ${f.summary}`)
40014
+ ]).join("\n");
39414
40015
  }
39415
40016
 
39416
- // ../packages/plugin-runtime/src/attached/egress-wire.ts
39417
- import { createHash as createHash6 } from "crypto";
39418
-
39419
40017
  // ../packages/remote/src/http.ts
39420
40018
  import { request as httpRequest } from "http";
39421
40019
  import { request as httpsRequest } from "https";
@@ -39695,6 +40293,7 @@ function createRemoteClient(options) {
39695
40293
  url: url2(ROUTES.shares),
39696
40294
  body: JSON.stringify(validated.data)
39697
40295
  });
40296
+ if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
39698
40297
  okBody(response);
39699
40298
  },
39700
40299
  async pollCommand() {
@@ -39746,16 +40345,67 @@ function createAttachClient(options) {
39746
40345
  };
39747
40346
  }
39748
40347
 
40348
+ // ../packages/remote/src/failure-kind.ts
40349
+ function statusOf(err) {
40350
+ if (typeof err !== "object" || err === null || !("status" in err)) return null;
40351
+ const { status } = err;
40352
+ if (typeof status !== "number" || !Number.isInteger(status)) return null;
40353
+ return status >= 100 && status <= 599 ? status : null;
40354
+ }
40355
+ function nameOf(err) {
40356
+ if (typeof err !== "object" || err === null || !("name" in err)) return null;
40357
+ return typeof err.name === "string" ? err.name : null;
40358
+ }
40359
+ function classifyRemoteFailure(err) {
40360
+ switch (nameOf(err)) {
40361
+ case "RemoteRouteAbsent":
40362
+ return "route-absent";
40363
+ case "RemoteRequestInvalid":
40364
+ return "invalid-request";
40365
+ case "RemoteResponseInvalid":
40366
+ return "rejected";
40367
+ default:
40368
+ break;
40369
+ }
40370
+ const status = statusOf(err);
40371
+ if (status === null) return "unreachable";
40372
+ switch (status) {
40373
+ case 401:
40374
+ return "unauthorized";
40375
+ case 403:
40376
+ return "forbidden";
40377
+ case 429:
40378
+ return "unreachable";
40379
+ case 404:
40380
+ return "unreachable";
40381
+ default:
40382
+ return status >= 400 && status <= 499 ? "rejected" : "unreachable";
40383
+ }
40384
+ }
40385
+
40386
+ // ../packages/remote/src/shares-sender.ts
40387
+ var SHARES_FORWARD_TIMEOUT_MS = 15e3;
40388
+ function createSharesSender(timeoutMs = SHARES_FORWARD_TIMEOUT_MS) {
40389
+ return async (connection, request) => {
40390
+ try {
40391
+ await createRemoteClient({ ...connection, timeoutMs }).recordProjectEgress(request);
40392
+ return { ok: true };
40393
+ } catch (err) {
40394
+ return { ok: false, kind: classifyRemoteFailure(err) };
40395
+ }
40396
+ };
40397
+ }
40398
+
39749
40399
  // ../packages/plugin-runtime/src/attached/forward-drops.ts
39750
- import { readFileSync as readFileSync17 } from "fs";
39751
- import { join as join25 } from "path";
40400
+ import { readFileSync as readFileSync19 } from "fs";
40401
+ import { join as join27 } from "path";
39752
40402
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
39753
40403
  function forwardDropsPath(dataDir2) {
39754
- return join25(dataDir2, FORWARD_DROPS_FILENAME);
40404
+ return join27(dataDir2, FORWARD_DROPS_FILENAME);
39755
40405
  }
39756
40406
  function readForwardDrops(dataDir2) {
39757
40407
  try {
39758
- const parsed2 = JSON.parse(readFileSync17(forwardDropsPath(dataDir2), "utf8"));
40408
+ const parsed2 = JSON.parse(readFileSync19(forwardDropsPath(dataDir2), "utf8"));
39759
40409
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
39760
40410
  const record2 = parsed2;
39761
40411
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -39773,9 +40423,9 @@ function readForwardDrops(dataDir2) {
39773
40423
 
39774
40424
  // ../packages/plugin-runtime/src/attached/forward-policy.ts
39775
40425
  import { randomUUID as randomUUID16 } from "crypto";
39776
- import { readFileSync as readFileSync18 } from "fs";
40426
+ import { readFileSync as readFileSync20 } from "fs";
39777
40427
  import { readFile, rename, writeFile } from "fs/promises";
39778
- import { join as join26 } from "path";
40428
+ import { join as join28 } from "path";
39779
40429
  var BREAKER_COOLDOWN_MS = 3e4;
39780
40430
  var FAILURES = /* @__PURE__ */ new Set([
39781
40431
  "unauthorized",
@@ -39799,15 +40449,15 @@ function parseBreakerState(raw, nowMs) {
39799
40449
  }
39800
40450
  function readForwardHealth(dir, nowMs = Date.now()) {
39801
40451
  try {
39802
- return parseBreakerState(readFileSync18(join26(dir, STATE_FILENAME), "utf8"), nowMs);
40452
+ return parseBreakerState(readFileSync20(join28(dir, STATE_FILENAME), "utf8"), nowMs);
39803
40453
  } catch {
39804
40454
  return null;
39805
40455
  }
39806
40456
  }
39807
40457
 
39808
40458
  // ../packages/plugin-runtime/src/attached/history-state.ts
39809
- import { readFileSync as readFileSync19 } from "fs";
39810
- import { join as join27 } from "path";
40459
+ import { readFileSync as readFileSync21 } from "fs";
40460
+ import { join as join29 } from "path";
39811
40461
  var HISTORY_SYNC_STATE_FILENAME = ATTACHED_HISTORY_SYNC_STATE_FILENAME;
39812
40462
  var PHASES = /* @__PURE__ */ new Set(["filling", "complete"]);
39813
40463
  var OUTCOMES = /* @__PURE__ */ new Set([
@@ -39818,7 +40468,7 @@ var OUTCOMES = /* @__PURE__ */ new Set([
39818
40468
  ]);
39819
40469
  var SPEC_VERSION = 1;
39820
40470
  function historySyncStatePath(dataDir2) {
39821
- return join27(dataDir2, HISTORY_SYNC_STATE_FILENAME);
40471
+ return join29(dataDir2, HISTORY_SYNC_STATE_FILENAME);
39822
40472
  }
39823
40473
  function writeHistorySyncState(dataDir2, state) {
39824
40474
  try {
@@ -39831,7 +40481,7 @@ function writeHistorySyncState(dataDir2, state) {
39831
40481
  }
39832
40482
  function readHistorySyncState(dataDir2) {
39833
40483
  try {
39834
- const parsed2 = JSON.parse(readFileSync19(historySyncStatePath(dataDir2), "utf8"));
40484
+ const parsed2 = JSON.parse(readFileSync21(historySyncStatePath(dataDir2), "utf8"));
39835
40485
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
39836
40486
  const r = parsed2;
39837
40487
  if (r.specVersion !== SPEC_VERSION) return null;
@@ -39863,7 +40513,7 @@ function isNullableCount(v) {
39863
40513
  }
39864
40514
 
39865
40515
  // ../packages/plugin-runtime/src/attached/history-sync.ts
39866
- import { createHash as createHash7 } from "crypto";
40516
+ import { createHash as createHash8 } from "crypto";
39867
40517
  import { hostname as hostname5 } from "os";
39868
40518
 
39869
40519
  // ../packages/plugin-runtime/src/attached/capture-rebuild.ts
@@ -40027,7 +40677,7 @@ var CAPTURE_BATCH_SIZE = INGEST_BATCH_MAX;
40027
40677
  var STRUCTURAL_BUDGET_SHARE = 0.7;
40028
40678
  var BATCH_SIZE = AUDIT_EVENT_BATCH_MAX;
40029
40679
  function endpointFingerprint(endpoint) {
40030
- return createHash7("sha256").update(endpoint).digest("hex");
40680
+ return createHash8("sha256").update(endpoint).digest("hex");
40031
40681
  }
40032
40682
  var didNotRun = (reason) => ({
40033
40683
  attempted: false,
@@ -40044,9 +40694,12 @@ async function runHistorySync(deps) {
40044
40694
  if (!isAttached(settings)) return didNotRun("not-attached");
40045
40695
  const connection = settings.controlPlane;
40046
40696
  if (connection === void 0) return didNotRun("not-attached");
40047
- if (!isHistorySyncConsentValid(settings.historySyncConsent, connection.endpoint)) {
40697
+ const consent = settings.historySyncConsent;
40698
+ if (!isHistorySyncConsentValid(consent, connection.endpoint) || consent === void 0) {
40048
40699
  return didNotRun("no-consent");
40049
40700
  }
40701
+ const consentAcknowledgedAtMs = Date.parse(consent.acknowledgedAt);
40702
+ if (!Number.isFinite(consentAcknowledgedAtMs)) return didNotRun("attachment-unreadable");
40050
40703
  const state = readControlPlaneCredentialFile(deps.settingsDir, connection);
40051
40704
  if (!state.usable) return didNotRun("credential-unusable");
40052
40705
  const nowMs = now();
@@ -40062,7 +40715,7 @@ async function runHistorySync(deps) {
40062
40715
  const recorded = ledger.deployment();
40063
40716
  let backlogBefore;
40064
40717
  if (recorded.fingerprint !== fingerprint) {
40065
- ledger.rearmFor(fingerprint, attachedAtMs);
40718
+ ledger.rearmFor(fingerprint, attachedAtMs, consentAcknowledgedAtMs);
40066
40719
  backlogBefore = attachedAtMs;
40067
40720
  } else if (recorded.backlogBefore === void 0) {
40068
40721
  ledger.freezeBoundary(attachedAtMs);
@@ -40438,12 +41091,12 @@ import { fileURLToPath as fileURLToPath3 } from "url";
40438
41091
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
40439
41092
 
40440
41093
  // ../packages/plugin-runtime/src/attached/plugin-block.ts
40441
- import { readFileSync as readFileSync20 } from "fs";
41094
+ import { readFileSync as readFileSync22 } from "fs";
40442
41095
 
40443
41096
  // ../packages/plugin-runtime/src/attached/policy-store.ts
40444
41097
  import { randomUUID as randomUUID17 } from "crypto";
40445
41098
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
40446
- import { join as join28 } from "path";
41099
+ import { join as join30 } from "path";
40447
41100
 
40448
41101
  // ../packages/plugin-runtime/src/attached/atomic-publish.ts
40449
41102
  import { rename as rename2 } from "fs/promises";
@@ -40467,7 +41120,7 @@ async function publishByRename(tmp, file2, move = rename2) {
40467
41120
 
40468
41121
  // ../packages/plugin-runtime/src/attached/policy-store.ts
40469
41122
  function createPolicyStore(dir = dataDir()) {
40470
- const file2 = join28(dir, "policy-cache.json");
41123
+ const file2 = join30(dir, "policy-cache.json");
40471
41124
  async function read() {
40472
41125
  try {
40473
41126
  const raw = await readFile2(file2, "utf8");
@@ -40546,11 +41199,11 @@ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
40546
41199
  // ../packages/plugin-runtime/src/attached/posture-store.ts
40547
41200
  import { randomUUID as randomUUID18 } from "crypto";
40548
41201
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
40549
- import { join as join29 } from "path";
41202
+ import { join as join31 } from "path";
40550
41203
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
40551
41204
  function createPostureStore(dir = settingsDir(), legacyDir) {
40552
- const file2 = join29(dir, "posture-state.json");
40553
- const legacyFile = legacyDir === void 0 ? null : join29(legacyDir, "posture-state.json");
41205
+ const file2 = join31(dir, "posture-state.json");
41206
+ const legacyFile = legacyDir === void 0 ? null : join31(legacyDir, "posture-state.json");
40554
41207
  async function persist(state) {
40555
41208
  await ensureDataDir(dir);
40556
41209
  const tmp = `${file2}.${randomUUID18()}.tmp`;
@@ -40622,8 +41275,8 @@ async function readDeviceIdentity(dir) {
40622
41275
  }
40623
41276
 
40624
41277
  // ../packages/plugin-runtime/src/attached/sync-state.ts
40625
- import { readFileSync as readFileSync21 } from "fs";
40626
- import { join as join30 } from "path";
41278
+ import { readFileSync as readFileSync23 } from "fs";
41279
+ import { join as join32 } from "path";
40627
41280
  var SYNC_STATE_FILENAME = ATTACHED_SYNC_STATE_FILENAME;
40628
41281
  var OUTCOMES2 = /* @__PURE__ */ new Set([
40629
41282
  "ok",
@@ -40634,11 +41287,11 @@ var OUTCOMES2 = /* @__PURE__ */ new Set([
40634
41287
  "invalid-bundle"
40635
41288
  ]);
40636
41289
  function syncStatePath(dataDir2) {
40637
- return join30(dataDir2, SYNC_STATE_FILENAME);
41290
+ return join32(dataDir2, SYNC_STATE_FILENAME);
40638
41291
  }
40639
41292
  function readSyncState(dataDir2) {
40640
41293
  try {
40641
- const parsed2 = JSON.parse(readFileSync21(syncStatePath(dataDir2), "utf8"));
41294
+ const parsed2 = JSON.parse(readFileSync23(syncStatePath(dataDir2), "utf8"));
40642
41295
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
40643
41296
  const record2 = parsed2;
40644
41297
  if (typeof record2.outcome !== "string" || !OUTCOMES2.has(record2.outcome)) return null;
@@ -41001,7 +41654,7 @@ async function attachByDeviceCode(deps) {
41001
41654
  ...deps.label === void 0 ? {} : { label: deps.label }
41002
41655
  });
41003
41656
  } catch (err) {
41004
- if (statusOf3(err) === 404) return { kind: "not-offered" };
41657
+ if (statusOf(err) === 404) return { kind: "not-offered" };
41005
41658
  return { kind: "not-offered", reason: describe3(err) };
41006
41659
  }
41007
41660
  deps.io.out(
@@ -41037,7 +41690,7 @@ async function attachByDeviceCode(deps) {
41037
41690
  try {
41038
41691
  answer = await deps.client.poll(grant2.deviceCode);
41039
41692
  } catch (err) {
41040
- if (statusOf3(err) === 429) {
41693
+ if (statusOf(err) === 429) {
41041
41694
  waitSeconds = Math.min(waitSeconds * 2, 60);
41042
41695
  continue;
41043
41696
  }
@@ -41105,13 +41758,9 @@ async function confirmAndReturn(deps, apiKey) {
41105
41758
  }
41106
41759
  return { kind: "attached", apiKey, identity };
41107
41760
  }
41108
- function statusOf3(err) {
41109
- const status = err?.status;
41110
- return typeof status === "number" ? status : void 0;
41111
- }
41112
41761
  function describe3(err) {
41113
- const status = statusOf3(err);
41114
- if (status !== void 0) return `the deployment answered ${String(status)}.`;
41762
+ const status = statusOf(err);
41763
+ if (status !== null) return `the deployment answered ${String(status)}.`;
41115
41764
  return "the deployment could not be reached.";
41116
41765
  }
41117
41766
  function stringField(body, key) {
@@ -41273,6 +41922,7 @@ ${USAGE}`);
41273
41922
  }
41274
41923
  const historyConsent = await askAboutHistory(io, args.syncHistory, base, endpoint, identity);
41275
41924
  const previous = readControlPlaneCredentialFile(settingsDir(base));
41925
+ const attachedAt = /* @__PURE__ */ new Date();
41276
41926
  try {
41277
41927
  writeControlPlaneCredential(settingsDir(base), {
41278
41928
  specVersion: 1,
@@ -41285,7 +41935,7 @@ ${USAGE}`);
41285
41935
  runMode: "attached",
41286
41936
  controlPlane: {
41287
41937
  endpoint,
41288
- attachedAt: (/* @__PURE__ */ new Date()).toISOString(),
41938
+ attachedAt: attachedAt.toISOString(),
41289
41939
  ...args.label === void 0 ? {} : { label: args.label }
41290
41940
  },
41291
41941
  // SPELLED, never omitted. `undefined` on an optional key is how this
@@ -41310,6 +41960,10 @@ ${USAGE}`);
41310
41960
  exit(1);
41311
41961
  return;
41312
41962
  }
41963
+ if (historyConsent !== void 0) {
41964
+ seedCaptureBacklogOwed(dataDir(base), attachedAt.getTime());
41965
+ }
41966
+ (deps.installBackgroundSync ?? installBackgroundSync)(base);
41313
41967
  io.out(
41314
41968
  [
41315
41969
  `Attached to ${args.label ?? endpoint}.`,
@@ -41349,6 +42003,10 @@ async function askAboutHistory(io, flag, base, endpoint, identity) {
41349
42003
  `Verified against ${identity.tenantName}.`,
41350
42004
  "",
41351
42005
  "Activity from here on is sent to that deployment automatically.",
42006
+ // The register a scan records crosses under this same attachment, and
42007
+ // `aka scan` is a verb people reasonably think of as local — so consent
42008
+ // is where it gets named, and named with what it does and does not carry.
42009
+ "So is the Data Shares register a scan records \u2014 destinations and call sites, never source text.",
41352
42010
  ...backlog === void 0 ? [] : [backlog],
41353
42011
  "AKA can also keep anything a live send fails to deliver, instead of",
41354
42012
  "dropping it.",
@@ -41356,9 +42014,10 @@ async function askAboutHistory(io, flag, base, endpoint, identity) {
41356
42014
  ...backlog === void 0 ? [] : [
41357
42015
  "What that history sends: which sessions ran, when, in which project,",
41358
42016
  " repo and git branch; token usage and model per call;",
41359
- " which tools were called, with their inputs truncated",
41360
- " and every detected secret already masked; and what",
41361
- " AKA detected in those tool inputs.",
42017
+ " which tools were called, with their inputs truncated;",
42018
+ " what AKA detected in those tool inputs; and the",
42019
+ " prompts, assistant replies and tool results",
42020
+ " themselves.",
41362
42021
  ""
41363
42022
  ],
41364
42023
  // The masking half is CONDITIONAL and has to read that way. A drained
@@ -41366,16 +42025,25 @@ async function askAboutHistory(io, flag, base, endpoint, identity) {
41366
42025
  // where the policy assigned its detection is redact or block — so copy
41367
42026
  // promising that every detected secret is masked first would be false
41368
42027
  // under monitor and warn, which is every detection until the user
41369
- // promotes one. Saying so is the point of asking.
41370
- "And, for anything a live send could not deliver \u2014 the deployment was",
41371
- "unreachable, or refused the key \u2014 what was captured, which for a prompt,",
41372
- "an assistant reply or a tool result INCLUDES ITS TEXT. What is masked in",
41373
- "that text follows the policy assigned to the detection that flagged the",
41374
- "value: it is masked before it is stored or sent only where that policy",
41375
- "is redact or block. Under monitor or warn the value goes as it was seen,",
41376
- "and every detection ships on monitor, so on a default install nothing in",
41377
- "that text is masked. Everything outside a flagged span goes as written",
41378
- "either way.",
42028
+ // promotes one. Saying so is the point of asking, and it now covers BOTH
42029
+ // buckets alike: once granted, that history is sent through the same
42030
+ // outbox mark and the same drain as an undelivered live send, so the
42031
+ // same masking rule applies to both.
42032
+ ...backlog === void 0 ? [
42033
+ "For anything a live send could not deliver \u2014 the deployment was",
42034
+ "unreachable, or refused the key \u2014 what was captured, which for a"
42035
+ ] : [
42036
+ "For that history, and for anything a later live send could not",
42037
+ "deliver \u2014 the deployment was unreachable, or refused the key \u2014 what",
42038
+ "was captured, which for a"
42039
+ ],
42040
+ "prompt, an assistant reply or a tool result INCLUDES ITS TEXT. What is",
42041
+ "masked in that text follows the policy assigned to the detection that",
42042
+ "flagged the value: it is masked before it is stored or sent",
42043
+ "only where that policy is redact or block. Under monitor or warn the",
42044
+ "value goes as it was seen, and every detection ships on monitor, so on",
42045
+ "a default install nothing in that text is masked. Everything outside a",
42046
+ "flagged span goes as written either way.",
41379
42047
  "",
41380
42048
  "Saying no does not stop live sending \u2014 that is part of being attached.",
41381
42049
  "It means an undelivered item is dropped rather than kept and retried.",
@@ -41418,6 +42086,7 @@ function runDetach(argv, deps = {}) {
41418
42086
  }
41419
42087
  removeControlPlaneCredential(settingsDir(base));
41420
42088
  clearDerived(dataDir(base));
42089
+ (deps.uninstallBackgroundSync ?? uninstallBackgroundSync)(base);
41421
42090
  io.out(
41422
42091
  had ? "Detached. This machine records locally only; that deployment is sent nothing.\n" : "This machine was not attached; nothing to do.\n"
41423
42092
  );
@@ -41452,6 +42121,9 @@ async function runStatus(argv, deps = {}) {
41452
42121
  io.out(attached ? `${block}
41453
42122
  ${await renderPolicyLine(dataDir2)}
41454
42123
  ` : `${block}
42124
+ `);
42125
+ const hostLines = hostCompatibilityLines(readHostVersionCache(dataDir2));
42126
+ if (hostLines.length > 0) io.out(`${hostLines.join("\n")}
41455
42127
  `);
41456
42128
  }
41457
42129
  function managedRefusal(base, endpoint, label, managedOverride) {
@@ -41503,7 +42175,7 @@ async function runDeviceAttach(input2) {
41503
42175
  }
41504
42176
 
41505
42177
  // src/commands/check-updates.ts
41506
- import { existsSync as existsSync14 } from "fs";
42178
+ import { existsSync as existsSync15 } from "fs";
41507
42179
  import { parseArgs } from "util";
41508
42180
  function runCheckUpdates(argv) {
41509
42181
  const { values } = parseArgs({ args: argv, options: HOME_OPTION });
@@ -41526,7 +42198,7 @@ function runCheckUpdates(argv) {
41526
42198
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
41527
42199
  `);
41528
42200
  }
41529
- if (existsSync14(dataDir(home))) {
42201
+ if (existsSync15(dataDir(home))) {
41530
42202
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
41531
42203
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
41532
42204
  }
@@ -41534,8 +42206,11 @@ function runCheckUpdates(argv) {
41534
42206
 
41535
42207
  // src/commands/completion.ts
41536
42208
  var topNames = COMMAND_SPECS.map((s) => s.name);
42209
+ var flagSpecs = COMMAND_SPECS.filter((s) => (s.flags ?? []).length > 0);
41537
42210
  function zshScript() {
41538
- const describe5 = COMMAND_SPECS.map((s) => ` '${s.name}:${s.summary}'`).join("\n");
42211
+ const zq = (text) => text.replace(/'/g, "'\\''");
42212
+ const describe5 = COMMAND_SPECS.map((s) => ` '${zq(s.name)}:${zq(s.summary)}'`).join("\n");
42213
+ const flagArms = flagSpecs.map((s) => ` ${s.name}) _aka_flags+=(${(s.flags ?? []).map((f) => f.name).join(" ")}) ;;`).join("\n");
41539
42214
  const argArms = COMMAND_SPECS.flatMap(
41540
42215
  (s) => s.args ? [` ${s.name}) _values '${s.name}' ${s.args.join(" ")} ;;`] : []
41541
42216
  );
@@ -41562,9 +42237,15 @@ ${describe5}
41562
42237
 
41563
42238
  # Flags may follow any command; complete them whenever the current word starts
41564
42239
  # with '-'. Checked BEFORE the positional switch so a command with no arg case
41565
- # (e.g. \`aka stats --\`) still offers them \u2014 matching the bash script.
42240
+ # (e.g. \`aka stats --\`) still offers them \u2014 matching the bash script. A
42241
+ # command that owns flags of its own adds them here and nowhere else.
41566
42242
  if [[ $words[CURRENT] == -* ]]; then
41567
- compadd -- ${GLOBAL_FLAGS.join(" ")}
42243
+ local -a _aka_flags
42244
+ _aka_flags=(${GLOBAL_FLAGS.join(" ")})
42245
+ case $words[2] in
42246
+ ${flagArms}
42247
+ esac
42248
+ compadd -- $_aka_flags
41568
42249
  return
41569
42250
  fi
41570
42251
 
@@ -41590,6 +42271,9 @@ compdef _aka aka
41590
42271
  `;
41591
42272
  }
41592
42273
  function bashScript() {
42274
+ const flagArms = flagSpecs.map(
42275
+ (s) => ` ${s.name}) flags="$flags ${(s.flags ?? []).map((f) => f.name).join(" ")}" ;;`
42276
+ ).join("\n");
41593
42277
  const argArms = COMMAND_SPECS.flatMap(
41594
42278
  (s) => s.args ? [` ${s.name}) COMPREPLY=( $(compgen -W "${s.args.join(" ")}" -- "$cur") ); return ;;`] : []
41595
42279
  ).join("\n");
@@ -41608,9 +42292,14 @@ _aka() {
41608
42292
  fi
41609
42293
 
41610
42294
  # Flags may follow any command \u2014 checked before the positional switch so every
41611
- # command offers them (matches the zsh script).
42295
+ # command offers them (matches the zsh script). A command that owns flags of
42296
+ # its own adds them here and nowhere else.
41612
42297
  if [[ "$cur" == -* ]]; then
41613
- COMPREPLY=( $(compgen -W "${GLOBAL_FLAGS.join(" ")}" -- "$cur") )
42298
+ local flags="${GLOBAL_FLAGS.join(" ")}"
42299
+ case "$cmd" in
42300
+ ${flagArms}
42301
+ esac
42302
+ COMPREPLY=( $(compgen -W "$flags" -- "$cur") )
41614
42303
  return
41615
42304
  fi
41616
42305
 
@@ -41680,17 +42369,17 @@ function runCompletion(argv) {
41680
42369
 
41681
42370
  // src/commands/dashboard.ts
41682
42371
  import { spawn as spawn5 } from "child_process";
41683
- import { existsSync as existsSync15 } from "fs";
42372
+ import { existsSync as existsSync16 } from "fs";
41684
42373
  import { createRequire as createRequire2 } from "module";
41685
42374
  import { createServer } from "net";
41686
- import { dirname as dirname9, join as join31 } from "path";
42375
+ import { dirname as dirname9, join as join33 } from "path";
41687
42376
  import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
41688
42377
  import { parseArgs as parseArgs2 } from "util";
41689
42378
  var require3 = createRequire2(import.meta.url);
41690
42379
  var here = dirname9(fileURLToPath5(import.meta.url));
41691
42380
  var BIND_HOST = "127.0.0.1";
41692
42381
  function pkgBase() {
41693
- return isSea() ? dirname9(process.execPath) : join31(here, "..");
42382
+ return isSea() ? dirname9(process.execPath) : join33(here, "..");
41694
42383
  }
41695
42384
  function isPortFree(port) {
41696
42385
  return new Promise((resolve7) => {
@@ -41744,9 +42433,9 @@ async function runDashboard(argv) {
41744
42433
  refreshUpdateMirror(dataDir());
41745
42434
  const base = pkgBase();
41746
42435
  const bundledServer = [
41747
- join31(base, "web-ui", "server.js"),
41748
- join31(base, "web-ui", "web-ui", "server.js")
41749
- ].find((p) => existsSync15(p));
42436
+ join33(base, "web-ui", "server.js"),
42437
+ join33(base, "web-ui", "web-ui", "server.js")
42438
+ ].find((p) => existsSync16(p));
41750
42439
  if (bundledServer) {
41751
42440
  launchStandalone(bundledServer, port, shouldOpen, url2);
41752
42441
  return;
@@ -41765,7 +42454,7 @@ async function runDashboard(argv) {
41765
42454
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
41766
42455
  `);
41767
42456
  };
41768
- if (existsSync15(join31(webUiDir, ".next", "BUILD_ID"))) {
42457
+ if (existsSync16(join33(webUiDir, ".next", "BUILD_ID"))) {
41769
42458
  startNext();
41770
42459
  return;
41771
42460
  }
@@ -42810,15 +43499,15 @@ async function runException(argv, io = terminalPrompter()) {
42810
43499
 
42811
43500
  // src/commands/extension.ts
42812
43501
  import { execFileSync as execFileSync3 } from "child_process";
42813
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync22, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
43502
+ import { existsSync as existsSync17, mkdirSync as mkdirSync6, readFileSync as readFileSync24, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "fs";
42814
43503
  import { createRequire as createRequire3 } from "module";
42815
- import { homedir as homedir5 } from "os";
42816
- import { dirname as dirname10, join as join32 } from "path";
43504
+ import { homedir as homedir7 } from "os";
43505
+ import { dirname as dirname10, join as join34 } from "path";
42817
43506
  import { fileURLToPath as fileURLToPath6 } from "url";
42818
43507
  var require4 = createRequire3(import.meta.url);
42819
43508
  var here2 = dirname10(fileURLToPath6(import.meta.url));
42820
43509
  function cliRoot() {
42821
- return isSea() ? dirname10(process.execPath) : join32(here2, "..");
43510
+ return isSea() ? dirname10(process.execPath) : join34(here2, "..");
42822
43511
  }
42823
43512
  var EXTENSION_IDS = [
42824
43513
  // Derived from the "key" in plugins/browser-extension/manifest.json.
@@ -42835,31 +43524,31 @@ function nativeHostManifest(hostPath) {
42835
43524
  };
42836
43525
  }
42837
43526
  function resolveHostScript(root = cliRoot()) {
42838
- const bundled = join32(root, "native-host", "host.js");
42839
- if (existsSync16(bundled)) return bundled;
43527
+ const bundled = join34(root, "native-host", "host.js");
43528
+ if (existsSync17(bundled)) return bundled;
42840
43529
  try {
42841
43530
  const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
42842
- const devBuilt = join32(dirname10(pkgJson), "native-host", "host.js");
42843
- if (existsSync16(devBuilt)) return devBuilt;
43531
+ const devBuilt = join34(dirname10(pkgJson), "native-host", "host.js");
43532
+ if (existsSync17(devBuilt)) return devBuilt;
42844
43533
  } catch {
42845
43534
  }
42846
43535
  return null;
42847
43536
  }
42848
43537
  function resolveExtensionDist(root = cliRoot()) {
42849
- const bundled = join32(root, "extension");
42850
- if (existsSync16(bundled)) return bundled;
43538
+ const bundled = join34(root, "extension");
43539
+ if (existsSync17(bundled)) return bundled;
42851
43540
  try {
42852
43541
  const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
42853
- const devDist = join32(dirname10(pkgJson), "dist");
42854
- if (existsSync16(devDist)) return devDist;
43542
+ const devDist = join34(dirname10(pkgJson), "dist");
43543
+ if (existsSync17(devDist)) return devDist;
42855
43544
  } catch {
42856
43545
  }
42857
43546
  return null;
42858
43547
  }
42859
43548
  function chromeManifestDir(platform3 = process.platform) {
42860
43549
  if (platform3 === "darwin") {
42861
- return join32(
42862
- homedir5(),
43550
+ return join34(
43551
+ homedir7(),
42863
43552
  "Library",
42864
43553
  "Application Support",
42865
43554
  "Google",
@@ -42868,9 +43557,9 @@ function chromeManifestDir(platform3 = process.platform) {
42868
43557
  );
42869
43558
  }
42870
43559
  if (platform3 === "win32") {
42871
- return join32(homedir5(), ".aka", "native-messaging");
43560
+ return join34(homedir7(), ".aka", "native-messaging");
42872
43561
  }
42873
- return join32(homedir5(), ".config", "google-chrome", "NativeMessagingHosts");
43562
+ return join34(homedir7(), ".config", "google-chrome", "NativeMessagingHosts");
42874
43563
  }
42875
43564
  function resolveNodeForLauncher() {
42876
43565
  if (!isSea()) return process.execPath;
@@ -42884,7 +43573,7 @@ function resolveNodeForLauncher() {
42884
43573
  }
42885
43574
  }
42886
43575
  function launcherPath(manifestDir, platform3) {
42887
- return join32(
43576
+ return join34(
42888
43577
  manifestDir,
42889
43578
  platform3 === "win32" ? `${NATIVE_HOST_NAME}.cmd` : `${NATIVE_HOST_NAME}.sh`
42890
43579
  );
@@ -42899,11 +43588,11 @@ function writeLauncher(manifestDir, hostScript, nodePath, platform3) {
42899
43588
  const path = launcherPath(manifestDir, platform3);
42900
43589
  const tmp = `${path}.tmp`;
42901
43590
  if (platform3 === "win32") {
42902
- writeFileSync8(tmp, `@echo off\r
43591
+ writeFileSync9(tmp, `@echo off\r
42903
43592
  ${cmdQuote(nodePath)} ${cmdQuote(hostScript)} %*\r
42904
43593
  `);
42905
43594
  } else {
42906
- writeFileSync8(tmp, `#!/bin/sh
43595
+ writeFileSync9(tmp, `#!/bin/sh
42907
43596
  exec ${posixQuote(nodePath)} ${posixQuote(hostScript)} "$@"
42908
43597
  `, {
42909
43598
  mode: 493
@@ -42959,11 +43648,11 @@ function runInstall(manifestDir, hostScript = resolveHostScript(), extensionDist
42959
43648
  process.exitCode = 1;
42960
43649
  return;
42961
43650
  }
42962
- mkdirSync5(manifestDir, { recursive: true });
43651
+ mkdirSync6(manifestDir, { recursive: true });
42963
43652
  const launcher = writeLauncher(manifestDir, hostScript, nodePath, process.platform);
42964
- const manifestPath = join32(manifestDir, `${NATIVE_HOST_NAME}.json`);
43653
+ const manifestPath = join34(manifestDir, `${NATIVE_HOST_NAME}.json`);
42965
43654
  const tmp = `${manifestPath}.tmp`;
42966
- writeFileSync8(tmp, `${JSON.stringify(nativeHostManifest(launcher), null, 2)}
43655
+ writeFileSync9(tmp, `${JSON.stringify(nativeHostManifest(launcher), null, 2)}
42967
43656
  `, {
42968
43657
  mode: DATA_FILE_MODE
42969
43658
  });
@@ -42998,7 +43687,7 @@ Next: load the extension in Chrome \u2014
42998
43687
  function manifestFault(manifestPath) {
42999
43688
  let parsed2;
43000
43689
  try {
43001
- parsed2 = JSON.parse(readFileSync22(manifestPath, "utf8"));
43690
+ parsed2 = JSON.parse(readFileSync24(manifestPath, "utf8"));
43002
43691
  } catch (err) {
43003
43692
  return `could not be read (${err instanceof Error ? err.message : String(err)})`;
43004
43693
  }
@@ -43009,8 +43698,8 @@ function manifestFault(manifestPath) {
43009
43698
  return `does not grant ${missing.length === 1 ? "the extension id" : "the extension ids"} ${missing.join(", ")}`;
43010
43699
  }
43011
43700
  function runStatus2(manifestDir) {
43012
- const manifestPath = join32(manifestDir, `${NATIVE_HOST_NAME}.json`);
43013
- if (!existsSync16(manifestPath)) {
43701
+ const manifestPath = join34(manifestDir, `${NATIVE_HOST_NAME}.json`);
43702
+ if (!existsSync17(manifestPath)) {
43014
43703
  process.stdout.write(
43015
43704
  `native-messaging host: not installed
43016
43705
  manifest: ${manifestPath}
@@ -43032,21 +43721,18 @@ function runStatus2(manifestDir) {
43032
43721
  }
43033
43722
 
43034
43723
  // src/commands/init.ts
43035
- import { existsSync as existsSync18, lstatSync as lstatSync4, readdirSync as readdirSync7, statSync as statSync13 } from "fs";
43036
- import { join as join33 } from "path";
43724
+ import { existsSync as existsSync19, lstatSync as lstatSync4, readdirSync as readdirSync7, statSync as statSync13 } from "fs";
43725
+ import { join as join35 } from "path";
43037
43726
  import * as readline from "readline/promises";
43038
43727
  import { parseArgs as parseArgs6 } from "util";
43039
43728
 
43040
43729
  // src/commands/plugins.ts
43041
- import { existsSync as existsSync17 } from "fs";
43730
+ import { existsSync as existsSync18 } from "fs";
43042
43731
  import { parseArgs as parseArgs5 } from "util";
43043
- function runPlugins(argv) {
43732
+ function runPlugins(argv, deps = {}) {
43044
43733
  const [sub, ...rest] = argv;
43045
43734
  if (!sub || sub === "list") return listPlugins(rest);
43046
- if (sub === "install") {
43047
- installPlugin(rest);
43048
- return;
43049
- }
43735
+ if (sub === "install") return installPlugin(rest, deps);
43050
43736
  process.stderr.write(`aka plugins: unknown subcommand '${sub}' (try: list, install <agent>)
43051
43737
  `);
43052
43738
  process.exitCode = 1;
@@ -43055,7 +43741,7 @@ async function listPlugins(argv) {
43055
43741
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
43056
43742
  const home = homeBase(values.home);
43057
43743
  const active = /* @__PURE__ */ new Set();
43058
- if (existsSync17(dbPath(home))) {
43744
+ if (existsSync18(dbPath(home))) {
43059
43745
  const db = openLocalDatabase(dataDir(home));
43060
43746
  try {
43061
43747
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -43079,7 +43765,7 @@ async function listPlugins(argv) {
43079
43765
  out.write("\nInstall: aka plugins install <agent>\n");
43080
43766
  out.write("Update: aka update (or: aka check-updates)\n");
43081
43767
  }
43082
- function installPlugin(argv) {
43768
+ async function installPlugin(argv, deps) {
43083
43769
  const { positionals } = parseArgs5({ args: argv, options: HOME_OPTION, allowPositionals: true });
43084
43770
  const id = positionals[0];
43085
43771
  if (!id) {
@@ -43119,6 +43805,27 @@ Then run \`aka init\` to set up the local store.
43119
43805
  );
43120
43806
  return;
43121
43807
  }
43808
+ if (cliBin === "claude") {
43809
+ const version2 = (deps.hostVersion ?? hostCliVersion)(cliBin);
43810
+ const gaps = hostFloorGaps(version2);
43811
+ const required2 = requiredHostVersion(gaps);
43812
+ if (version2 !== void 0 && required2 !== void 0) {
43813
+ const io = deps.prompter ?? terminalPrompter();
43814
+ io.out(
43815
+ `This ${cliBin} is ${version2}, which is older than AKA needs for ${gaps.map((g) => g.label).join(", ")}.
43816
+ Those protections will be inactive until you update Claude Code to ${required2} or newer. Everything else AKA does works normally.
43817
+ `
43818
+ );
43819
+ if (io.isInteractive && deps.assumeYes !== true) {
43820
+ const answer = (await io.ask("Install anyway? [y/N] ")).trim().toLowerCase();
43821
+ if (answer !== "y" && answer !== "yes") {
43822
+ io.out("Not installed. Update Claude Code, then run this again.\n");
43823
+ if (deps.declineIsFailure !== false) process.exitCode = 1;
43824
+ return;
43825
+ }
43826
+ }
43827
+ }
43828
+ }
43122
43829
  const plan = manager3.installSpawnPlan(ref, agent.marketplaceSource, agent.marketplace);
43123
43830
  process.stdout.write(
43124
43831
  `Installing ${agent.name} via ${cliBin}, running:
@@ -43152,7 +43859,7 @@ function storeModeTargets(home) {
43152
43859
  try {
43153
43860
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
43154
43861
  if (entry.name.endsWith(".partial") && !entry.isDirectory()) continue;
43155
- targets.add(join33(dir, entry.name));
43862
+ targets.add(join35(dir, entry.name));
43156
43863
  }
43157
43864
  } catch {
43158
43865
  }
@@ -43197,7 +43904,7 @@ function assertStoreLinksResolve(home) {
43197
43904
  }
43198
43905
  function isBrokenLink(path) {
43199
43906
  try {
43200
- return lstatSync4(path).isSymbolicLink() && !existsSync18(path);
43907
+ return lstatSync4(path).isSymbolicLink() && !existsSync19(path);
43201
43908
  } catch {
43202
43909
  return false;
43203
43910
  }
@@ -43234,12 +43941,12 @@ async function runInit(argv) {
43234
43941
  ensureLayoutDirSync(home);
43235
43942
  const settings = settingsDir(home);
43236
43943
  ensureLayoutDirSync(settings);
43237
- const settingsFile = join33(settings, "settings.json");
43944
+ const settingsFile = join35(settings, "settings.json");
43238
43945
  let settingsCreated = false;
43239
- if (!existsSync18(settingsFile)) {
43946
+ if (!existsSync19(settingsFile)) {
43240
43947
  try {
43241
43948
  settingsCreated = withFileLock(settingsFile, () => {
43242
- if (existsSync18(settingsFile)) return false;
43949
+ if (existsSync19(settingsFile)) return false;
43243
43950
  writeOwnerOnlyFileSync(
43244
43951
  settingsFile,
43245
43952
  `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
@@ -43268,7 +43975,7 @@ async function runInit(argv) {
43268
43975
  const symlinked = symlinkedStorePaths(home);
43269
43976
  process.stdout.write(
43270
43977
  `\u2713 Initialized AKA at ${home}
43271
- settings: ${settingsFile}${settingsCreated ? "" : existsSync18(settingsFile) ? " (kept existing)" : " (not written \u2014 another process was writing it)"}
43978
+ settings: ${settingsFile}${settingsCreated ? "" : existsSync19(settingsFile) ? " (kept existing)" : " (not written \u2014 another process was writing it)"}
43272
43979
  database: ${dbPath(home)}
43273
43980
  seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
43274
43981
  ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
@@ -43305,7 +44012,10 @@ Install the ${PRODUCT_NAME} plugin for Claude Code now (via the marketplace)? [y
43305
44012
  return;
43306
44013
  }
43307
44014
  }
43308
- await runPlugins(["install", "claude-code"]);
44015
+ await runPlugins(["install", "claude-code"], {
44016
+ assumeYes: autoYes,
44017
+ declineIsFailure: false
44018
+ });
43309
44019
  }
43310
44020
  async function confirm(question) {
43311
44021
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -43332,13 +44042,29 @@ function renderEgressLine(egress) {
43332
44042
  const cap = egress.truncated ? ` (capped at ${String(MAX_EGRESS_CALL_SITES_PER_PROJECT)} call sites)` : "";
43333
44043
  return `Data shares: ${String(egress.destinations)} destination(s) \xB7 ${String(egress.endpoints)} endpoint(s) \xB7 ${String(egress.callSites)} call site(s)${cap}`;
43334
44044
  }
43335
- async function runScan(argv) {
44045
+ function reportedForward(outcome) {
44046
+ return outcome === null || outcome.status === "not-attached" ? null : outcome;
44047
+ }
44048
+ function renderForwardLine(outcome) {
44049
+ switch (outcome.status) {
44050
+ case "disabled":
44051
+ return outcome.reason === "opt-out" ? "Data shares: not forwarded (--no-forward)" : "Data shares: not forwarded (Data Shares is off in Settings)";
44052
+ case "no-credential":
44053
+ return `Data shares: not forwarded to ${outcome.endpoint} \u2014 no usable credential; re-attach with \`aka attach\``;
44054
+ case "forwarded":
44055
+ return `Data shares: forwarded to ${outcome.endpoint} \xB7 ${String(outcome.callSites)} call site(s)`;
44056
+ case "failed":
44057
+ return `Data shares: not forwarded to ${outcome.endpoint} \u2014 ${FORWARD_FAILURE_LINES[outcome.kind]}`;
44058
+ }
44059
+ }
44060
+ async function runScan(argv, deps = {}) {
43336
44061
  const { values, positionals } = parseArgs7({
43337
44062
  args: argv,
43338
44063
  options: {
43339
44064
  ...HOME_OPTION,
43340
44065
  format: { type: "string" },
43341
- "fail-on": { type: "string" }
44066
+ "fail-on": { type: "string" },
44067
+ "no-forward": { type: "boolean" }
43342
44068
  },
43343
44069
  allowPositionals: true
43344
44070
  });
@@ -43383,14 +44109,39 @@ async function runScan(argv) {
43383
44109
  result = await scanPathIntoStore(db, target, {
43384
44110
  ruleActions,
43385
44111
  sourceTool: SOURCE_TOOL.Cli,
43386
- dataDir: storeDir
44112
+ dataDir: storeDir,
44113
+ // The AKA home this invocation is actually using, so the scanner's
44114
+ // protected-path exclusion covers a `--home` store rather than the
44115
+ // literal `~/.aka` the default would name. `home` is the same base
44116
+ // `recordProjectEgress` takes below, for the same reason.
44117
+ akaHome: home
43387
44118
  });
43388
44119
  inventory = recordProjectInventory(db, target);
43389
44120
  egress = recordProjectEgress(db, target, result.egress, home);
44121
+ } catch (err) {
44122
+ if (!isProtectedTarget(err)) throw err;
44123
+ process.stderr.write(`aka scan: ${err.message}
44124
+ `);
44125
+ process.exitCode = 1;
44126
+ return;
43390
44127
  } finally {
43391
44128
  db.close();
43392
44129
  }
44130
+ const recorded = egress;
44131
+ const runForward = async () => {
44132
+ if (recorded === null) return null;
44133
+ try {
44134
+ return await forwardProjectEgress(home, recorded.input, {
44135
+ send: deps.send ?? createSharesSender(),
44136
+ enabled: values["no-forward"] !== true
44137
+ });
44138
+ } catch {
44139
+ process.stderr.write("aka scan: forward failed unexpectedly\n");
44140
+ return null;
44141
+ }
44142
+ };
43393
44143
  if (format === "json") {
44144
+ const forward = await runForward();
43394
44145
  const findings = result.files.flatMap(
43395
44146
  (f) => f.findings.map((d) => ({
43396
44147
  file: f.path,
@@ -43416,9 +44167,17 @@ async function runScan(argv) {
43416
44167
  callSites: egress.callSites,
43417
44168
  truncated: egress.truncated
43418
44169
  } : null;
44170
+ const forwardJson = reportedForward(forward);
43419
44171
  process.stdout.write(
43420
44172
  `${JSON.stringify(
43421
- { target, scanned: result.scanned, findings, inventory: inventoryJson, egress: egressJson },
44173
+ {
44174
+ target,
44175
+ scanned: result.scanned,
44176
+ findings,
44177
+ inventory: inventoryJson,
44178
+ egress: egressJson,
44179
+ forward: forwardJson
44180
+ },
43422
44181
  null,
43423
44182
  2
43424
44183
  )}
@@ -43432,6 +44191,9 @@ async function runScan(argv) {
43432
44191
  if (inventory) process.stdout.write(`${renderInventoryLine(inventory)}
43433
44192
  `);
43434
44193
  if (egress) process.stdout.write(`${renderEgressLine(egress)}
44194
+ `);
44195
+ const reported = reportedForward(await runForward());
44196
+ if (reported !== null) process.stdout.write(`${renderForwardLine(reported)}
43435
44197
  `);
43436
44198
  }
43437
44199
  if (failOn !== void 0) {
@@ -43635,8 +44397,9 @@ function grant(io, exit, base, settings) {
43635
44397
  exit(1);
43636
44398
  return false;
43637
44399
  }
44400
+ const grantedAt = Date.now();
43638
44401
  const consent = {
43639
- acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString(),
44402
+ acknowledgedAt: new Date(grantedAt).toISOString(),
43640
44403
  payloadVersion: HISTORY_SYNC_PAYLOAD_VERSION,
43641
44404
  endpoint: settings.controlPlane.endpoint
43642
44405
  };
@@ -43647,14 +44410,15 @@ function grant(io, exit, base, settings) {
43647
44410
  exit(1);
43648
44411
  return false;
43649
44412
  }
44413
+ seedCaptureBacklogOwed(dataDir(base), grantedAt);
43650
44414
  io.out(
43651
44415
  `Sending this machine's unsent activity to ${controlPlaneName(settings.controlPlane)}.
43652
- That is the activity recorded before it attached, and anything a live send could not
43653
- deliver \u2014 which for a captured prompt, reply or tool result includes its text. A value
43654
- in that text is masked only where the policy assigned to the detection that flagged it
43655
- is redact or block; under monitor or warn it goes as it was seen, and every detection
43656
- ships on monitor. It goes in the background, a little at a time, starting with your
43657
- next session. Anything already sent cannot be recalled.
44416
+ That is the activity recorded before it attached and anything a live send could not
44417
+ deliver, alike: for a captured prompt, reply or tool result, either one includes its
44418
+ text. A value in that text is masked only where the policy assigned to the detection
44419
+ that flagged it is redact or block; under monitor or warn it goes as it was seen, and
44420
+ every detection ships on monitor. It goes in the background, a little at a time,
44421
+ starting with your next session. Anything already sent cannot be recalled.
43658
44422
  `
43659
44423
  );
43660
44424
  return true;
@@ -43685,9 +44449,10 @@ function describe4(settings) {
43685
44449
  Run \`aka sync-history --on\` to send it.`;
43686
44450
  }
43687
44451
  return isHistorySyncConsentStale(settings.historySyncConsent, settings.controlPlane.endpoint) ? `Not sending this machine's unsent activity to ${where}: your grant predates a change.
43688
- It now also covers the text of captured prompts, replies and tool results that no
43689
- live send delivered, in which a value is masked only where the detection that
43690
- flagged it is set to redact or block \u2014 and every detection ships on monitor.
44452
+ It now also covers the text of the activity recorded before this machine attached \u2014
44453
+ not just what a live send failed to deliver afterward \u2014 in which a value is masked
44454
+ only where the detection that flagged it is set to redact or block, and every
44455
+ detection ships on monitor.
43691
44456
  Run \`aka sync-history --on\` to grant it again.` : `Not sending this machine's unsent activity to ${where}: the earlier grant no longer
43692
44457
  applies. Run \`aka sync-history --on\` to grant it again.`;
43693
44458
  }
@@ -44150,71 +44915,6 @@ function Dashboard({
44150
44915
  ] });
44151
44916
  }
44152
44917
 
44153
- // ../packages/dashboard-ui/src/security/recommendations.ts
44154
- var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
44155
- var ADVICE = {
44156
- secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
44157
- pii: "Remove or mask personal data before it reaches the model.",
44158
- financial: "Strip card and account numbers; share only non-sensitive references.",
44159
- phi: "Remove protected health information \u2014 it should never reach an external model.",
44160
- code_context: "Confirm this proprietary code context is safe to share.",
44161
- custom: "Review against your organization\u2019s custom policy."
44162
- };
44163
- var REC_TEMPLATE = {
44164
- secret: { title: "Exposed secret detected", action: "Rotate" },
44165
- pii: { title: "Personal data in a prompt", action: "Remove" },
44166
- financial: { title: "Financial data detected", action: "Strip" },
44167
- phi: { title: "Health information detected", action: "Remove" },
44168
- code_context: { title: "Proprietary code shared", action: "Review" },
44169
- custom: { title: "Custom policy match", action: "Review" }
44170
- };
44171
- var MAX_RECOMMENDATIONS = 10;
44172
- function healthScore(summary) {
44173
- const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
44174
- const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
44175
- return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
44176
- }
44177
- function findingStatus(summary) {
44178
- return {
44179
- score: healthScore(summary),
44180
- unreviewed: { ...summary.bySeverity },
44181
- openFindings: summary.findings
44182
- };
44183
- }
44184
- function bucketize(findings) {
44185
- const buckets = /* @__PURE__ */ new Map();
44186
- for (const f of findings) {
44187
- const b = buckets.get(f.category) ?? {
44188
- category: f.category,
44189
- count: 0,
44190
- severity: f.severity,
44191
- weight: 0,
44192
- ruleId: f.ruleId
44193
- };
44194
- b.count++;
44195
- const w = SEVERITY_WEIGHT[f.severity] ?? 0;
44196
- if (w > b.weight) {
44197
- b.weight = w;
44198
- b.severity = f.severity;
44199
- b.ruleId = f.ruleId;
44200
- }
44201
- buckets.set(f.category, b);
44202
- }
44203
- return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.count - a.count).slice(0, MAX_RECOMMENDATIONS);
44204
- }
44205
- function buildRecommendations(findings) {
44206
- return bucketize(findings).map((b) => {
44207
- const t = REC_TEMPLATE[b.category] ?? { title: `${b.category} finding`, action: "Review" };
44208
- return {
44209
- severity: b.severity,
44210
- title: t.title,
44211
- description: ADVICE[b.category] ?? "Review this finding against your policy.",
44212
- context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
44213
- action: t.action
44214
- };
44215
- });
44216
- }
44217
-
44218
44918
  // src/tui/report.ts
44219
44919
  var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
44220
44920
  function weekday(isoDay2) {
@@ -44470,30 +45170,30 @@ import { parseArgs as parseArgs12 } from "util";
44470
45170
  import {
44471
45171
  lstatSync as lstatSync5,
44472
45172
  readdirSync as readdirSync8,
44473
- readFileSync as readFileSync23,
44474
- realpathSync as realpathSync5,
45173
+ readFileSync as readFileSync25,
45174
+ realpathSync as realpathSync6,
44475
45175
  renameSync as renameSync6,
44476
- rmSync as rmSync8,
45176
+ rmSync as rmSync9,
44477
45177
  statSync as statSync15,
44478
- writeFileSync as writeFileSync9
45178
+ writeFileSync as writeFileSync10
44479
45179
  } from "fs";
44480
- import { homedir as homedir6 } from "os";
44481
- import { basename as basename7, dirname as dirname12, isAbsolute as isAbsolute2, join as join34, relative as relative2, resolve as resolve6 } from "path";
45180
+ import { homedir as homedir8 } from "os";
45181
+ import { basename as basename7, dirname as dirname12, isAbsolute as isAbsolute2, join as join36, relative as relative2, resolve as resolve6 } from "path";
44482
45182
  var DEFAULT_MAX_REWRITE_BYTES = 32 * 1024 * 1024;
44483
45183
  function transcriptRoots(userHome) {
44484
- return [join34(userHome ?? homedir6(), ".claude", "projects")];
45184
+ return [join36(userHome ?? homedir8(), ".claude", "projects")];
44485
45185
  }
44486
45186
  function readContainedFile(realPath, maxBytes = DEFAULT_MAX_REWRITE_BYTES) {
44487
45187
  try {
44488
45188
  if (statSync15(realPath).size > maxBytes) return null;
44489
- return readFileSync23(realPath, "utf8");
45189
+ return readFileSync25(realPath, "utf8");
44490
45190
  } catch {
44491
45191
  return null;
44492
45192
  }
44493
45193
  }
44494
45194
  function realPathOrNull(path) {
44495
45195
  try {
44496
- return realpathSync5(path);
45196
+ return realpathSync6(path);
44497
45197
  } catch {
44498
45198
  return null;
44499
45199
  }
@@ -44537,9 +45237,9 @@ function sweepStrandedTemps(realPath) {
44537
45237
  const pid = Number(pidPart);
44538
45238
  if (pid !== process.pid && isProcessAlive(pid)) continue;
44539
45239
  try {
44540
- const stranded = join34(dir, name);
45240
+ const stranded = join36(dir, name);
44541
45241
  if (!lstatSync5(stranded).isFile()) continue;
44542
- rmSync8(stranded, { force: true });
45242
+ rmSync9(stranded, { force: true });
44543
45243
  } catch {
44544
45244
  }
44545
45245
  }
@@ -44551,7 +45251,7 @@ async function rewriteContainedFile(realPath, transform2, maxBytes = DEFAULT_MAX
44551
45251
  before = statSync15(realPath);
44552
45252
  if (before.size > maxBytes)
44553
45253
  return { status: "aborted", reason: "file is larger than the read cap" };
44554
- content = readFileSync23(realPath, "utf8");
45254
+ content = readFileSync25(realPath, "utf8");
44555
45255
  } catch {
44556
45256
  return { status: "aborted", reason: "unreadable" };
44557
45257
  }
@@ -44566,16 +45266,16 @@ async function rewriteContainedFile(realPath, transform2, maxBytes = DEFAULT_MAX
44566
45266
  if (result.text === content) return { status: "unchanged" };
44567
45267
  const tmpPath = tempPathFor(realPath);
44568
45268
  try {
44569
- writeFileSync9(tmpPath, result.text, { mode: before.mode & 511, flag: "wx" });
45269
+ writeFileSync10(tmpPath, result.text, { mode: before.mode & 511, flag: "wx" });
44570
45270
  const now = statSync15(realPath);
44571
45271
  if (now.size !== before.size || now.mtimeMs !== before.mtimeMs) {
44572
- rmSync8(tmpPath, { force: true, recursive: true });
45272
+ rmSync9(tmpPath, { force: true, recursive: true });
44573
45273
  return { status: "aborted", reason: "the file changed while it was being read" };
44574
45274
  }
44575
45275
  renameSync6(tmpPath, realPath);
44576
45276
  } catch {
44577
45277
  try {
44578
- rmSync8(tmpPath, { force: true, recursive: true });
45278
+ rmSync9(tmpPath, { force: true, recursive: true });
44579
45279
  } catch {
44580
45280
  }
44581
45281
  return { status: "aborted", reason: "the rewrite could not be written" };