@akasecurity/cli 0.9.4 → 0.9.5

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 (161) hide show
  1. package/README.md +18 -0
  2. package/dist/cli.js +2220 -410
  3. package/extension/background.js +1 -0
  4. package/extension/content.js +1 -0
  5. package/extension/icons/icon128.png +0 -0
  6. package/extension/icons/icon16.png +0 -0
  7. package/extension/icons/icon48.png +0 -0
  8. package/extension/manifest.json +39 -0
  9. package/extension/popup.html +69 -0
  10. package/extension/popup.js +1 -0
  11. package/native-host/host.js +31084 -0
  12. package/native-host/scan-worker.js +18006 -0
  13. package/package.json +12 -8
  14. package/web-ui/package.json +11 -7
  15. package/web-ui/packages/dashboard-ui/package.json +2 -0
  16. package/web-ui/packages/plugin-sdk/package.json +3 -1
  17. package/web-ui/packages/ui-kit/package.json +1 -0
  18. package/web-ui/rules/secrets/gcp-service-account.json +1 -1
  19. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  20. package/web-ui/web-ui/.next/app-build-manifest.json +194 -56
  21. package/web-ui/web-ui/.next/app-path-routes-manifest.json +8 -8
  22. package/web-ui/web-ui/.next/build-manifest.json +8 -8
  23. package/web-ui/web-ui/.next/prerender-manifest.json +14 -14
  24. package/web-ui/web-ui/.next/required-server-files.json +5 -0
  25. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  26. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  28. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  29. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  31. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  32. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  34. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  35. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  36. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  37. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  38. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  39. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  40. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
  41. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  42. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  43. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  44. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  45. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  46. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  47. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  48. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  49. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  50. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  51. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  52. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  53. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  54. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  55. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  56. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  57. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  58. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  59. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  60. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  61. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  62. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  63. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  64. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -2
  65. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -1
  66. package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -1
  67. package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
  68. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  69. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  70. package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
  71. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  72. package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
  73. package/web-ui/web-ui/.next/server/app/page.js +2 -2
  74. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  75. package/web-ui/web-ui/.next/server/app-paths-manifest.json +8 -8
  76. package/web-ui/web-ui/.next/server/chunks/114.js +792 -0
  77. package/web-ui/web-ui/.next/server/chunks/160.js +785 -0
  78. package/web-ui/web-ui/.next/server/chunks/243.js +1 -1
  79. package/web-ui/web-ui/.next/server/chunks/330.js +1 -1
  80. package/web-ui/web-ui/.next/server/chunks/333.js +1 -0
  81. package/web-ui/web-ui/.next/server/chunks/363.js +12 -0
  82. package/web-ui/web-ui/.next/server/chunks/528.js +73 -0
  83. package/web-ui/web-ui/.next/server/chunks/557.js +5 -0
  84. package/web-ui/web-ui/.next/server/chunks/561.js +123 -0
  85. package/web-ui/web-ui/.next/server/functions-config-manifest.json +6 -6
  86. package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
  87. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  88. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  89. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  90. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  91. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  92. package/web-ui/web-ui/.next/static/chunks/133-14f0f297836736b9.js +1 -0
  93. package/web-ui/web-ui/.next/static/chunks/539-b35890d398017f99.js +62 -0
  94. package/web-ui/web-ui/.next/static/chunks/992-ba273ebe2ac236a5.js +1 -0
  95. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/loading-0e999b82265bac84.js +1 -0
  96. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-90ea4cb1f1956a03.js +1 -0
  97. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/loading-0e999b82265bac84.js +1 -0
  98. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-d96aa1158bb1c630.js +1 -0
  99. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/loading-0e999b82265bac84.js +1 -0
  100. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-269fa2d4f47e6125.js +1 -0
  101. package/web-ui/web-ui/.next/static/chunks/app/(app)/error-092d6780c93209d5.js +1 -0
  102. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/loading-0e999b82265bac84.js +1 -0
  103. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-4ad5b60f476d7c6e.js +1 -0
  104. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/loading-0e999b82265bac84.js +1 -0
  105. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/loading-0e999b82265bac84.js +1 -0
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-2bf76696fbe6e76d.js +1 -0
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-5d2511a7c06dd645.js +1 -0
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/loading-0e999b82265bac84.js +1 -0
  109. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-1891ec610b461455.js +1 -0
  110. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/loading-0e999b82265bac84.js +1 -0
  111. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-bc090490b3416d48.js +1 -0
  112. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-d7a290e9b0dd8a15.js +1 -0
  113. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/loading-0e999b82265bac84.js +1 -0
  114. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-1d1a93a1e561ca82.js +1 -0
  115. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/loading-0e999b82265bac84.js +1 -0
  116. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-7a1a29a230626f96.js +1 -0
  117. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/loading-0e999b82265bac84.js +1 -0
  118. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-a7fa0e028623b7cb.js +1 -0
  119. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/loading-0e999b82265bac84.js +1 -0
  120. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-9729632fc8128534.js +1 -0
  121. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/loading-0e999b82265bac84.js +1 -0
  122. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-bda9e826a1ac1a00.js +1 -0
  123. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/loading-0e999b82265bac84.js +1 -0
  124. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-28116d3ce827e152.js +1 -0
  125. package/web-ui/web-ui/.next/static/chunks/app/page-315a9068831a3ad5.js +1 -0
  126. package/web-ui/web-ui/.next/static/chunks/{framework-3444de8758288822.js → framework-a566532c222ba944.js} +1 -1
  127. package/web-ui/web-ui/.next/static/chunks/{main-b13545f01f111855.js → main-f710385bb68f9de5.js} +1 -1
  128. package/web-ui/web-ui/.next/static/chunks/pages/{_app-7f0d920cc5af1c9f.js → _app-63f369037199ea68.js} +1 -1
  129. package/web-ui/web-ui/.next/static/chunks/pages/{_error-3d8b61361c4b2c6b.js → _error-41a11264bde7b852.js} +1 -1
  130. package/web-ui/web-ui/.next/static/css/0af7782f7a750368.css +2 -0
  131. package/web-ui/web-ui/.next/static/{cVKs25XbQsC1083sU5GOD → x4ixG3_q4UeyC_WNmSFdI}/_buildManifest.js +1 -1
  132. package/web-ui/web-ui/dist/scan-worker.js +18006 -0
  133. package/web-ui/web-ui/package.json +4 -3
  134. package/web-ui/web-ui/server.js +1 -1
  135. package/web-ui/web-ui/.next/server/chunks/28.js +0 -1
  136. package/web-ui/web-ui/.next/server/chunks/334.js +0 -2
  137. package/web-ui/web-ui/.next/server/chunks/514.js +0 -755
  138. package/web-ui/web-ui/.next/server/chunks/628.js +0 -84
  139. package/web-ui/web-ui/.next/server/chunks/817.js +0 -1
  140. package/web-ui/web-ui/.next/server/chunks/830.js +0 -750
  141. package/web-ui/web-ui/.next/server/chunks/949.js +0 -123
  142. package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +0 -1
  143. package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +0 -62
  144. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +0 -1
  145. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +0 -1
  146. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +0 -1
  147. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +0 -1
  148. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-1d468a1a0cf12aea.js +0 -1
  149. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +0 -1
  150. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +0 -1
  151. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +0 -1
  152. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +0 -1
  153. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +0 -1
  154. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +0 -1
  155. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +0 -1
  156. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +0 -1
  157. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +0 -1
  158. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +0 -1
  159. package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +0 -1
  160. package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +0 -2
  161. /package/web-ui/web-ui/.next/static/{cVKs25XbQsC1083sU5GOD → x4ixG3_q4UeyC_WNmSFdI}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -539,12 +539,16 @@ function runInherit(command, args) {
539
539
  }
540
540
  }
541
541
 
542
- // ../packages/local-ops/src/claude-plugin.ts
543
- function claudeAvailable() {
544
- return binExists("claude");
545
- }
546
- function ensureMarketplace(source) {
547
- runCapture("claude", ["plugin", "marketplace", "add", source], 6e4);
542
+ // ../packages/local-ops/src/cli-plugin-manager.ts
543
+ function createCliPluginManager(bin) {
544
+ return {
545
+ available: () => binExists(bin),
546
+ ensureMarketplace: (source) => {
547
+ runCapture(bin, ["plugin", "marketplace", "add", source], 6e4);
548
+ },
549
+ install: (ref) => runInherit(bin, ["plugin", "install", ref]),
550
+ update: (ref) => runInherit(bin, ["plugin", "update", ref])
551
+ };
548
552
  }
549
553
 
550
554
  // ../packages/local-ops/src/registry.ts
@@ -557,7 +561,43 @@ var AGENT_PLUGINS = [
557
561
  npmPackage: "@akasecurity/ai-tc-claude-code",
558
562
  pluginName: "ai-tc",
559
563
  marketplace: "akasecurity",
560
- marketplaceSource: "akasecurity/marketplace"
564
+ marketplaceSource: "akasecurity/marketplace",
565
+ cliBin: "claude"
566
+ },
567
+ {
568
+ id: "codex",
569
+ name: "Codex CLI",
570
+ sourceTool: "codex",
571
+ description: "Hooks Codex CLI sessions to detect + redact sensitive data in prompts, responses, and Bash tool calls.",
572
+ npmPackage: "@akasecurity/ai-tc-codex",
573
+ // Distinct from Claude Code's `pluginName: 'ai-tc'` — ids from the two
574
+ // registry entries flow into ONE ref-keyed `installed` lookup map
575
+ // (see updates.ts/installedPluginVersions and apply.ts/resolveRef); an
576
+ // identical ref for both would make either plugin's install ledger
577
+ // entry satisfy the other's "is it installed" check.
578
+ pluginName: "aka-codex",
579
+ // Codex CLI installs from this repo's own marketplace file; the canonical
580
+ // akasecurity/marketplace repo currently carries Claude Code only.
581
+ marketplace: "ai-tc",
582
+ marketplaceSource: "akasecurity/ai-tc",
583
+ cliBin: "codex"
584
+ },
585
+ {
586
+ id: "antigravity",
587
+ name: "Antigravity",
588
+ sourceTool: "antigravity",
589
+ description: "Hooks Antigravity CLI sessions to detect sensitive data in shell commands and file writes.",
590
+ npmPackage: "@akasecurity/ai-tc-antigravity",
591
+ // NO pluginName/marketplace/cliBin, deliberately. Antigravity has no plugin
592
+ // marketplace: `agy plugin install` takes a LOCAL DIRECTORY PATH, not a
593
+ // `<plugin>@<marketplace>` ref, so the generic cli-plugin-manager binding
594
+ // the other two entries use would emit a command `agy` does not accept.
595
+ // Leaving the coordinates absent is the registry's documented "installed by
596
+ // other means" case — buildUpdateReport skips an entry with no ref, so this
597
+ // agent is listed and joined to its rows by `sourceTool` without claiming an
598
+ // update path that does not exist. Install is documented in
599
+ // plugins/antigravity/skills/setup/SKILL.md.
600
+ installHint: "Antigravity installs plugins from a local directory rather than a marketplace:\n npm pack @akasecurity/ai-tc-antigravity && tar -xzf akasecurity-ai-tc-antigravity-*.tgz\n agy plugin install ./package\nThen run `aka init` to set up the local store."
561
601
  }
562
602
  ];
563
603
  function findAgent(id) {
@@ -569,7 +609,7 @@ function pluginRef(agent) {
569
609
  }
570
610
 
571
611
  // ../packages/local-ops/src/updates.ts
572
- import { existsSync, readFileSync } from "fs";
612
+ import { existsSync, readdirSync, readFileSync } from "fs";
573
613
  import { homedir as homedir2 } from "os";
574
614
  import { dirname, join } from "path";
575
615
  import { fileURLToPath } from "url";
@@ -583,6 +623,9 @@ function parse(version2) {
583
623
  pre: match[4] ? match[4].split(".") : []
584
624
  };
585
625
  }
626
+ function isSemver(version2) {
627
+ return parse(version2) !== null;
628
+ }
586
629
  function comparePre(a, b) {
587
630
  if (a.length === 0 && b.length === 0) return 0;
588
631
  if (a.length === 0) return 1;
@@ -665,6 +708,43 @@ function installedPluginVersions(claudeHome = join(homedir2(), ".claude")) {
665
708
  }
666
709
  return out;
667
710
  }
711
+ function codexPluginCacheRoot(codexHome) {
712
+ return join(codexHome, "plugins", "cache");
713
+ }
714
+ function activeCodexPluginVersion(dir) {
715
+ let entries;
716
+ try {
717
+ entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
718
+ } catch {
719
+ return null;
720
+ }
721
+ if (entries.length === 0) return null;
722
+ if (entries.includes("local")) return "local";
723
+ const versions = entries.filter(isSemver);
724
+ if (versions.length === 0) return null;
725
+ return versions.reduce(
726
+ (best, candidate) => compareSemver(candidate, best) > 0 ? candidate : best
727
+ );
728
+ }
729
+ function installedCodexPluginVersions(codexHome = join(homedir2(), ".codex")) {
730
+ const out = /* @__PURE__ */ new Map();
731
+ const cacheRoot = codexPluginCacheRoot(codexHome);
732
+ for (const agent of AGENT_PLUGINS) {
733
+ if (agent.cliBin !== "codex") continue;
734
+ const ref = pluginRef(agent);
735
+ if (!ref || !agent.pluginName || !agent.marketplace) continue;
736
+ const pluginDir = join(cacheRoot, agent.marketplace, agent.pluginName);
737
+ const version2 = activeCodexPluginVersion(pluginDir);
738
+ if (version2 !== null) out.set(ref, version2);
739
+ }
740
+ return out;
741
+ }
742
+ function installedAgentPluginVersions(claudeHome, codexHome) {
743
+ return new Map([
744
+ ...installedPluginVersions(claudeHome),
745
+ ...installedCodexPluginVersions(codexHome)
746
+ ]);
747
+ }
668
748
  function npmViewVersion(pkg) {
669
749
  const res = runCapture("npm", ["view", pkg, "version"], 15e3);
670
750
  if (!res.ok || !res.stdout) return null;
@@ -707,7 +787,7 @@ function gatherReport(deps) {
707
787
  function gatherReportLive() {
708
788
  return gatherReport({
709
789
  viewVersion: npmViewVersion,
710
- installed: installedPluginVersions(),
790
+ installed: installedAgentPluginVersions(),
711
791
  cliInstalled: cliVersion()
712
792
  });
713
793
  }
@@ -728,31 +808,53 @@ function resolveRef(agentId) {
728
808
  const agent = findAgent(agentId);
729
809
  if (!agent) return { ok: false, output: `unknown agent '${agentId}'` };
730
810
  const ref = pluginRef(agent);
731
- if (!ref) return { ok: false, output: `${agent.name} has no automated install path yet.` };
732
- if (!claudeAvailable()) {
811
+ const cliBin = agent.cliBin;
812
+ if (!ref || !cliBin) {
813
+ return { ok: false, output: `${agent.name} has no automated install path yet.` };
814
+ }
815
+ const manager3 = createCliPluginManager(cliBin);
816
+ if (!manager3.available()) {
733
817
  return {
734
818
  ok: false,
735
- output: `the \`claude\` CLI isn't on your PATH \u2014 install Claude Code, then run \`claude plugin install ${ref}\` (or update with \`claude plugin update ${ref}\`).`
819
+ output: `the \`${cliBin}\` CLI isn't on your PATH \u2014 install ${agent.name}, then run \`${cliBin} plugin install ${ref}\` (or update with \`${cliBin} plugin update ${ref}\`).`
736
820
  };
737
821
  }
738
- return { ref, marketplaceSource: agent.marketplaceSource };
822
+ return { ref, cliBin, marketplaceSource: agent.marketplaceSource };
739
823
  }
740
824
  function applyPluginUpdate(agentId, mode = "capture") {
741
825
  const resolved = resolveRef(agentId);
742
826
  if ("ok" in resolved) return resolved;
743
- if (resolved.marketplaceSource) ensureMarketplace(resolved.marketplaceSource);
744
- return run("claude", ["plugin", "update", resolved.ref], mode);
827
+ if (resolved.marketplaceSource) {
828
+ createCliPluginManager(resolved.cliBin).ensureMarketplace(resolved.marketplaceSource);
829
+ }
830
+ return run(resolved.cliBin, ["plugin", "update", resolved.ref], mode);
745
831
  }
746
832
  function installAgentPlugin(agentId, mode = "capture") {
747
833
  const resolved = resolveRef(agentId);
748
834
  if ("ok" in resolved) return resolved;
749
- if (resolved.marketplaceSource) ensureMarketplace(resolved.marketplaceSource);
750
- return run("claude", ["plugin", "install", resolved.ref], mode);
835
+ if (resolved.marketplaceSource) {
836
+ createCliPluginManager(resolved.cliBin).ensureMarketplace(resolved.marketplaceSource);
837
+ }
838
+ return run(resolved.cliBin, ["plugin", "install", resolved.ref], mode);
751
839
  }
752
840
 
841
+ // ../packages/local-ops/src/claude-plugin.ts
842
+ var manager = createCliPluginManager("claude");
843
+ var claudeAvailable = manager.available;
844
+ var ensureMarketplace = manager.ensureMarketplace;
845
+ var installClaudePlugin = manager.install;
846
+ var updateClaudePlugin = manager.update;
847
+
848
+ // ../packages/local-ops/src/codex-plugin.ts
849
+ var manager2 = createCliPluginManager("codex");
850
+ var codexAvailable = manager2.available;
851
+ var ensureCodexMarketplace = manager2.ensureMarketplace;
852
+ var installCodexPlugin = manager2.install;
853
+ var updateCodexPlugin = manager2.update;
854
+
753
855
  // ../packages/local-ops/src/egress-record.ts
754
- import { statSync as statSync5 } from "fs";
755
- import { dirname as dirname4, relative as relative2, resolve } from "path";
856
+ import { statSync as statSync7 } from "fs";
857
+ import { dirname as dirname5, relative as relative2, resolve } from "path";
756
858
 
757
859
  // ../packages/detections/src/egress/registry.ts
758
860
  var EXTRACTOR_VERSION = "1";
@@ -1742,11 +1844,11 @@ var EXACT_KIND_BY_BASENAME = {
1742
1844
  "composer.json": "composer.json",
1743
1845
  "packages.config": "packages.config"
1744
1846
  };
1745
- function manifestKindOf(basename6) {
1746
- if (LOCKFILE_BASENAMES.has(basename6)) return null;
1747
- const exact = Object.hasOwn(EXACT_KIND_BY_BASENAME, basename6) ? EXACT_KIND_BY_BASENAME[basename6] : void 0;
1847
+ function manifestKindOf(basename7) {
1848
+ if (LOCKFILE_BASENAMES.has(basename7)) return null;
1849
+ const exact = Object.hasOwn(EXACT_KIND_BY_BASENAME, basename7) ? EXACT_KIND_BY_BASENAME[basename7] : void 0;
1748
1850
  if (exact !== void 0) return exact;
1749
- if (basename6.endsWith(".csproj")) return "csproj";
1851
+ if (basename7.endsWith(".csproj")) return "csproj";
1750
1852
  return null;
1751
1853
  }
1752
1854
  function extractManifestSdks(text, kind) {
@@ -2486,6 +2588,14 @@ var SQLITE_MIGRATIONS = [
2486
2588
  {
2487
2589
  tag: "0018_serious_tana_nile",
2488
2590
  sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
2591
+ },
2592
+ {
2593
+ tag: "0019_audit_started_at_index",
2594
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
2595
+ },
2596
+ {
2597
+ tag: "0020_secret_vault_pagination_indexes",
2598
+ sql: "CREATE INDEX `idx_secret_vault_last_seen` ON `secret_vault` (`last_seen`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_reuse` ON `secret_vault` (`occurrence_count`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_at` ON `secret_vault_deref` (`at`,`id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_signal` ON `secret_vault_deref` (`at`,`id`) WHERE `reason` NOT IN ('display', 'view-render');"
2489
2599
  }
2490
2600
  ];
2491
2601
 
@@ -17346,7 +17456,17 @@ var Finding = external_exports.object({
17346
17456
  }).meta({ id: "Finding" });
17347
17457
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
17348
17458
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
17349
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
17459
+ var FindingProvider = external_exports.enum([
17460
+ "claudecode",
17461
+ "claudedesktop",
17462
+ "cursor",
17463
+ "copilot",
17464
+ "chatgpt",
17465
+ "claudeai",
17466
+ "codex",
17467
+ "antigravity",
17468
+ "api"
17469
+ ]).meta({ id: "FindingProvider" });
17350
17470
  var FindingCategory = external_exports.enum([
17351
17471
  "secret",
17352
17472
  "pii",
@@ -17400,7 +17520,16 @@ var FindingInstance = external_exports.object({
17400
17520
  confidence: external_exports.number().min(0).max(1),
17401
17521
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
17402
17522
  // that predate the resolution feature stay valid.
17403
- status: FindingStatus.optional()
17523
+ status: FindingStatus.optional(),
17524
+ // The audit event this finding was captured from. Optional so callers that
17525
+ // do not project it stay valid. An at-rest finding is content-addressed by
17526
+ // finding_key and its row is upserted on re-detection, so this names the
17527
+ // MOST RECENT detection event, not the first.
17528
+ eventId: external_exports.string().optional(),
17529
+ // The session that event belongs to, when it has one — the seam a
17530
+ // per-instance "view session" link needs. Absent for events captured
17531
+ // outside a session.
17532
+ sessionId: external_exports.string().optional()
17404
17533
  }).meta({ id: "FindingInstance" });
17405
17534
  var FindingGroup = external_exports.object({
17406
17535
  id: external_exports.string(),
@@ -17444,7 +17573,11 @@ var FindingFacets = external_exports.object({
17444
17573
  // for every instance, so every group lands in a bucket; a status-less
17445
17574
  // group (possible only for callers whose rows carry no statuses) is
17446
17575
  // counted under no value.
17447
- status: external_exports.array(FindingFacetItem)
17576
+ status: external_exports.array(FindingFacetItem),
17577
+ // Host tool (attributes.tool_name). Present only on the instance-level
17578
+ // reads, which can filter by it; the grouped read omits the dimension
17579
+ // because a group spans tools.
17580
+ tool: external_exports.array(FindingFacetItem).optional()
17448
17581
  }).meta({ id: "FindingFacets" });
17449
17582
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
17450
17583
  var ListGroupedFindingsQuery = external_exports.object({
@@ -17462,6 +17595,16 @@ var ListGroupedFindingsQuery = external_exports.object({
17462
17595
  // Scope to findings whose event carries this session id (the Activity page's
17463
17596
  // session → findings drilldown). Findings without a session never match.
17464
17597
  sessionId: external_exports.string().optional(),
17598
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
17599
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
17600
+ // means all time — this list has no default window.
17601
+ from: external_exports.iso.datetime().optional(),
17602
+ // A group or instance id that must appear in the page even when the cursor
17603
+ // has already advanced past its sort position. This is what keeps the
17604
+ // Findings page's one-shot ?finding= deep link resolving once the list
17605
+ // paginates: the target group is appended out of sort order rather than
17606
+ // scanning forward for it. Never affects totals, facets or the cursor.
17607
+ includeId: external_exports.string().optional(),
17465
17608
  groupBy: external_exports.literal("type").optional(),
17466
17609
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
17467
17610
  cursor: external_exports.string().optional()
@@ -17506,15 +17649,110 @@ var FindingInstanceDetail = FindingInstance.extend({
17506
17649
  detection: FindingDetectionRef,
17507
17650
  policy: FindingPolicyRef
17508
17651
  }).meta({ id: "FindingInstanceDetail" });
17652
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
17653
+ var ListFindingInstancesQuery = external_exports.object({
17654
+ severity: external_exports.array(Severity).optional(),
17655
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
17656
+ subtype: external_exports.array(external_exports.string()).optional(),
17657
+ provider: external_exports.array(FindingProvider).optional(),
17658
+ action: external_exports.array(FindingAction).optional(),
17659
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
17660
+ // the grouped query's group-level fold.
17661
+ status: external_exports.array(FindingStatus).optional(),
17662
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
17663
+ // where the free-text `q` can only match the rendered "via Bash" label.
17664
+ tool: external_exports.array(external_exports.string()).optional(),
17665
+ // Exact repository / file-path matches, for the drill-down out of the
17666
+ // locations view. A row whose event carries no repo/file matches neither.
17667
+ repo: external_exports.string().optional(),
17668
+ file: external_exports.string().optional(),
17669
+ q: external_exports.string().optional(),
17670
+ sessionId: external_exports.string().optional(),
17671
+ from: external_exports.iso.datetime().optional(),
17672
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
17673
+ cursor: external_exports.string().optional()
17674
+ });
17675
+ var ListFindingInstancesResponse = external_exports.object({
17676
+ // Instances matching the filters across the whole scope, not just this
17677
+ // page — cursor-independent, like the grouped list's totals.
17678
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
17679
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
17680
+ // dimension still excludes its own filter.
17681
+ facets: FindingFacets,
17682
+ items: external_exports.array(FindingInstanceDetail),
17683
+ nextCursor: external_exports.string().nullable()
17684
+ }).meta({ id: "ListFindingInstancesResponse" });
17685
+ var FindingLocationFile = external_exports.object({
17686
+ // Empty when the instances carried no file path (a prompt or a tool call
17687
+ // with no file attribution).
17688
+ file: external_exports.string(),
17689
+ instanceCount: external_exports.number().int().nonnegative(),
17690
+ maxSeverity: Severity,
17691
+ latestDetectedAt: external_exports.iso.datetime(),
17692
+ // Folded from the instances' derived statuses with the same
17693
+ // open-dominates precedence a group uses.
17694
+ status: FindingStatus.optional(),
17695
+ // Distinct rules seen at this location, capped — the row shows them as
17696
+ // chips, and the count is what conveys scale.
17697
+ ruleIds: external_exports.array(external_exports.string())
17698
+ }).meta({ id: "FindingLocationFile" });
17699
+ var FindingLocationRepo = external_exports.object({
17700
+ /** Empty when the instances carried no repo attribute. */
17701
+ repo: external_exports.string(),
17702
+ instanceCount: external_exports.number().int().nonnegative(),
17703
+ maxSeverity: Severity,
17704
+ latestDetectedAt: external_exports.iso.datetime(),
17705
+ status: FindingStatus.optional(),
17706
+ files: external_exports.array(FindingLocationFile)
17707
+ }).meta({ id: "FindingLocationRepo" });
17708
+ var ListFindingLocationsQuery = external_exports.object({
17709
+ severity: external_exports.array(Severity).optional(),
17710
+ subtype: external_exports.array(external_exports.string()).optional(),
17711
+ provider: external_exports.array(FindingProvider).optional(),
17712
+ action: external_exports.array(FindingAction).optional(),
17713
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
17714
+ // instances that match, and folds its status from those.
17715
+ status: external_exports.array(FindingStatus).optional(),
17716
+ tool: external_exports.array(external_exports.string()).optional(),
17717
+ q: external_exports.string().optional(),
17718
+ sessionId: external_exports.string().optional(),
17719
+ from: external_exports.iso.datetime().optional(),
17720
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
17721
+ });
17722
+ var ListFindingLocationsResponse = external_exports.object({
17723
+ totals: external_exports.object({
17724
+ findings: external_exports.number().int().nonnegative(),
17725
+ repos: external_exports.number().int().nonnegative(),
17726
+ files: external_exports.number().int().nonnegative()
17727
+ }),
17728
+ /** Sorted by max severity, then most recent. */
17729
+ items: external_exports.array(FindingLocationRepo),
17730
+ /** Whether `limit` truncated the repo list. */
17731
+ hasMore: external_exports.boolean()
17732
+ }).meta({ id: "ListFindingLocationsResponse" });
17509
17733
 
17510
17734
  // ../packages/schema/src/zod/harness-map.ts
17511
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
17735
+ var Harness = external_exports.enum([
17736
+ "claudecode",
17737
+ "cursor",
17738
+ "copilot",
17739
+ "codex",
17740
+ "antigravity",
17741
+ "windsurf",
17742
+ "claudedesktop",
17743
+ "chatgpt",
17744
+ "claudeai",
17745
+ "api"
17746
+ ]).meta({ id: "Harness" });
17512
17747
  var TOOL_TO_HARNESS = {
17513
17748
  "claude-code": "claudecode",
17514
17749
  "claude-desktop": "claudedesktop",
17515
17750
  "github-copilot": "copilot",
17516
17751
  cursor: "cursor",
17517
- chatgpt: "chatgpt"
17752
+ chatgpt: "chatgpt",
17753
+ codex: "codex",
17754
+ antigravity: "antigravity",
17755
+ "claude-ai": "claudeai"
17518
17756
  };
17519
17757
 
17520
17758
  // ../packages/schema/src/zod/meta.ts
@@ -17972,7 +18210,18 @@ var ActivityOverviewResponse = external_exports.object({
17972
18210
  // ../packages/schema/src/zod/event.ts
17973
18211
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
17974
18212
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
17975
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
18213
+ var SourceTool = external_exports.enum([
18214
+ "claude-code",
18215
+ "claude-desktop",
18216
+ "cursor",
18217
+ "chatgpt",
18218
+ "claude-ai",
18219
+ "github-copilot",
18220
+ "codex",
18221
+ "antigravity",
18222
+ "cli",
18223
+ "unknown"
18224
+ ]).meta({ id: "SourceTool" });
17976
18225
  var EventMetadata = external_exports.object({
17977
18226
  sessionId: external_exports.string().optional(),
17978
18227
  repo: external_exports.string().optional(),
@@ -18043,7 +18292,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
18043
18292
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
18044
18293
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
18045
18294
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
18046
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
18295
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
18047
18296
  var AccessCounts = external_exports.object({
18048
18297
  open: external_exports.number().int().nonnegative(),
18049
18298
  approved: external_exports.number().int().nonnegative(),
@@ -18312,9 +18561,19 @@ var ExceptionBundleEntry = DetectionException.pick({
18312
18561
  useCount: true,
18313
18562
  conditions: true
18314
18563
  });
18564
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
18315
18565
  function isMatchableUnder(keyVersion, key) {
18316
18566
  return key.status === "present" && key.version === keyVersion;
18317
18567
  }
18568
+ var LEDGER_WINDOW_HOURS = 24;
18569
+ function rotationBlockedLedgerNote(stillApprovable) {
18570
+ if (stillApprovable <= 0) {
18571
+ return "Recently blocked detections are invalidated too: the ledger outlives a rotation, so its rows stay listed as a record of what was blocked but stop being approvable. Trigger the detection again to approve it under the new key.";
18572
+ }
18573
+ const window = `from the last ${String(LEDGER_WINDOW_HOURS)} hours`;
18574
+ const subject = stillApprovable === 1 ? `1 recently blocked detection ${window} is still approvable; after rotating, none are` : `${String(stillApprovable)} recently blocked detections ${window} are still approvable; after rotating, none are`;
18575
+ return `${subject}. They stay listed as a record of what was blocked \u2014 trigger the detection again to approve under the new key.`;
18576
+ }
18318
18577
 
18319
18578
  // ../packages/schema/src/zod/rule.ts
18320
18579
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -19125,6 +19384,35 @@ var EgressWriteSummary = external_exports.object({
19125
19384
  droppedFiles: external_exports.array(external_exports.string()).default([])
19126
19385
  }).meta({ id: "EgressWriteSummary" });
19127
19386
 
19387
+ // ../packages/schema/src/zod/exception-action.ts
19388
+ var confirmation = external_exports.string().optional();
19389
+ var ApproveBlockedInput = external_exports.object({
19390
+ reference: external_exports.string(),
19391
+ scope: external_exports.string(),
19392
+ reason: external_exports.string(),
19393
+ confirmation
19394
+ });
19395
+ var AddExceptionInput = external_exports.object({
19396
+ ruleId: external_exports.string(),
19397
+ value: external_exports.string(),
19398
+ scope: external_exports.string(),
19399
+ reason: external_exports.string(),
19400
+ confirmation
19401
+ });
19402
+ var GrantRevealInput = external_exports.object({
19403
+ pointer: external_exports.string(),
19404
+ scope: external_exports.string(),
19405
+ justification: external_exports.string(),
19406
+ confirmation
19407
+ });
19408
+ var RevokeExceptionInput = external_exports.object({
19409
+ id: external_exports.string(),
19410
+ reason: external_exports.string()
19411
+ });
19412
+ var RotateKeyInput = external_exports.object({
19413
+ confirmation: external_exports.string()
19414
+ });
19415
+
19128
19416
  // ../packages/schema/src/zod/findings-group-build.ts
19129
19417
  function toApiAction(dbVal) {
19130
19418
  const map2 = {
@@ -19180,6 +19468,8 @@ function buildFindingGroups(rows, opts = {}) {
19180
19468
  repo: r.repo,
19181
19469
  file: r.file,
19182
19470
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
19471
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
19472
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
19183
19473
  action: toApiAction(effectiveDbAction),
19184
19474
  detectedAt: r.occurredAt,
19185
19475
  confidence: r.confidence,
@@ -19311,14 +19601,17 @@ function applyFindingFilters(groups, opts) {
19311
19601
  }
19312
19602
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
19313
19603
  var SEVERITY_RANK2 = SEVERITY_ORDER;
19604
+ function compareFindingGroupOrder(a, b) {
19605
+ const rankA = SEVERITY_RANK2[a.severity] ?? -1;
19606
+ const rankB = SEVERITY_RANK2[b.severity] ?? -1;
19607
+ const severityDiff = rankA - rankB;
19608
+ if (severityDiff !== 0) return severityDiff;
19609
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
19610
+ if (recencyDiff !== 0) return recencyDiff;
19611
+ return a.id.localeCompare(b.id);
19612
+ }
19314
19613
  function sortFindingGroups(groups) {
19315
- return [...groups].sort((a, b) => {
19316
- const rankA = SEVERITY_RANK2[a.severity] ?? -1;
19317
- const rankB = SEVERITY_RANK2[b.severity] ?? -1;
19318
- const severityDiff = rankA - rankB;
19319
- if (severityDiff !== 0) return severityDiff;
19320
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
19321
- });
19614
+ return [...groups].sort(compareFindingGroupOrder);
19322
19615
  }
19323
19616
  function computeFindingFacets(allGroups, opts) {
19324
19617
  const forSeverity = applyFindingFilters(allGroups, {
@@ -19374,15 +19667,158 @@ function computeFindingFacets(allGroups, opts) {
19374
19667
  for (const g of forStatus) {
19375
19668
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
19376
19669
  }
19377
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
19670
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
19671
+ return {
19672
+ severity: toItems2(severityMap),
19673
+ provider: toItems2(providerMap),
19674
+ action: toItems2(actionMap),
19675
+ subtype: toItems2(subtypeMap),
19676
+ status: toItems2(statusMap)
19677
+ };
19678
+ }
19679
+
19680
+ // ../packages/schema/src/zod/findings-flat-build.ts
19681
+ function rowHaystack(row) {
19682
+ return [
19683
+ row.ruleId,
19684
+ row.category,
19685
+ row.maskedMatch,
19686
+ row.repo,
19687
+ row.file,
19688
+ row.toolName ? `via ${row.toolName}` : "",
19689
+ row.id
19690
+ ].join(" ").toLowerCase();
19691
+ }
19692
+ function matchesDimension(row, opts, dimension) {
19693
+ switch (dimension) {
19694
+ case "severity":
19695
+ return !opts.severity?.length || opts.severity.includes(row.severity);
19696
+ case "subtype":
19697
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
19698
+ case "providers":
19699
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
19700
+ case "actions":
19701
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
19702
+ case "statuses":
19703
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
19704
+ case "tools":
19705
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
19706
+ case "repo":
19707
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
19708
+ case "file":
19709
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
19710
+ case "q":
19711
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
19712
+ }
19713
+ }
19714
+ var DIMENSIONS = [
19715
+ "severity",
19716
+ "subtype",
19717
+ "providers",
19718
+ "actions",
19719
+ "statuses",
19720
+ "tools",
19721
+ "repo",
19722
+ "file",
19723
+ "q"
19724
+ ];
19725
+ function matchesInstanceFilters(row, opts, except) {
19726
+ for (const dimension of DIMENSIONS) {
19727
+ if (dimension === except) continue;
19728
+ if (!matchesDimension(row, opts, dimension)) return false;
19729
+ }
19730
+ return true;
19731
+ }
19732
+ function toItems(counts) {
19733
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
19734
+ }
19735
+ function bump(counts, value) {
19736
+ counts.set(value, (counts.get(value) ?? 0) + 1);
19737
+ }
19738
+ function createInstanceFacetAccumulator(opts) {
19739
+ const severity = /* @__PURE__ */ new Map();
19740
+ const subtype = /* @__PURE__ */ new Map();
19741
+ const provider = /* @__PURE__ */ new Map();
19742
+ const action = /* @__PURE__ */ new Map();
19743
+ const status = /* @__PURE__ */ new Map();
19744
+ const tool = /* @__PURE__ */ new Map();
19745
+ return {
19746
+ add(row) {
19747
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
19748
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
19749
+ if (matchesInstanceFilters(row, opts, "providers")) {
19750
+ bump(provider, toApiProvider(row.sourceTool));
19751
+ }
19752
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
19753
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
19754
+ bump(status, row.status);
19755
+ }
19756
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
19757
+ bump(tool, row.toolName);
19758
+ }
19759
+ },
19760
+ facets: () => ({
19761
+ severity: toItems(severity),
19762
+ subtype: toItems(subtype),
19763
+ provider: toItems(provider),
19764
+ action: toItems(action),
19765
+ status: toItems(status),
19766
+ tool: toItems(tool)
19767
+ })
19768
+ };
19769
+ }
19770
+ function toInstanceDetail(row) {
19771
+ const category = toApiCategory(row.category);
19772
+ return {
19773
+ id: row.id,
19774
+ provider: toApiProvider(row.sourceTool),
19775
+ repo: row.repo,
19776
+ file: row.file,
19777
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
19778
+ eventId: row.eventId,
19779
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
19780
+ action: toApiAction(row.actionTaken),
19781
+ detectedAt: row.occurredAt,
19782
+ confidence: row.confidence,
19783
+ ...row.status === void 0 ? {} : { status: row.status },
19784
+ groupId: row.ruleId,
19785
+ category,
19786
+ subtype: row.ruleId,
19787
+ severity: row.severity,
19788
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
19789
+ detection: { id: row.ruleId, name: null },
19790
+ policy: { id: `category:${category}`, name: category }
19791
+ };
19792
+ }
19793
+ var SEVERITY_ORDER2 = {
19794
+ critical: 0,
19795
+ high: 1,
19796
+ medium: 2,
19797
+ low: 3
19798
+ };
19799
+ function newLocationAccumulator() {
19378
19800
  return {
19379
- severity: toItems(severityMap),
19380
- provider: toItems(providerMap),
19381
- action: toItems(actionMap),
19382
- subtype: toItems(subtypeMap),
19383
- status: toItems(statusMap)
19801
+ instanceCount: 0,
19802
+ // Sorts after every known severity, so the first row always wins the
19803
+ // comparison below rather than an unknown value pinning the location.
19804
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
19805
+ maxSeverity: "low",
19806
+ latestDetectedAt: "",
19807
+ statuses: [],
19808
+ ruleIds: /* @__PURE__ */ new Set()
19384
19809
  };
19385
19810
  }
19811
+ function addToLocation(acc, row) {
19812
+ acc.instanceCount += 1;
19813
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
19814
+ if (rank < acc.maxSeverityRank) {
19815
+ acc.maxSeverityRank = rank;
19816
+ acc.maxSeverity = row.severity;
19817
+ }
19818
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
19819
+ acc.statuses.push(row.status);
19820
+ acc.ruleIds.add(row.ruleId);
19821
+ }
19386
19822
 
19387
19823
  // ../packages/schema/src/zod/installed-pack.ts
19388
19824
  var InstalledPack = external_exports.object({
@@ -19523,6 +19959,50 @@ var VaultInventoryEntry = external_exports.object({
19523
19959
  revealGrantId: external_exports.string().nullable(),
19524
19960
  sightings: external_exports.array(VaultSighting)
19525
19961
  });
19962
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
19963
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
19964
+ var MAX_VAULT_PAGE_LIMIT = 200;
19965
+ var ListVaultInventoryQuery = external_exports.object({
19966
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
19967
+ // Opaque; names the last row of the page just served.
19968
+ cursor: external_exports.string().optional()
19969
+ });
19970
+ var ListVaultInventoryResponse = external_exports.object({
19971
+ // Vaulted values across the whole store, not just this page — cursor-
19972
+ // independent, so paging never changes what the count claims.
19973
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
19974
+ items: external_exports.array(VaultInventoryEntry),
19975
+ // `null` once the last page is reached.
19976
+ nextCursor: external_exports.string().nullable()
19977
+ });
19978
+ var ListVaultReuseQuery = external_exports.object({
19979
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
19980
+ cursor: external_exports.string().optional()
19981
+ });
19982
+ var ListVaultReuseResponse = external_exports.object({
19983
+ // Reused values across the whole store — the number the section's claim
19984
+ // ("values detected in more than one place") is about.
19985
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
19986
+ items: external_exports.array(VaultInventoryEntry),
19987
+ nextCursor: external_exports.string().nullable()
19988
+ });
19989
+ var ListVaultDerefsQuery = external_exports.object({
19990
+ // Include the batched, high-volume reasons (display, view-render). Omitted
19991
+ // hides them and counts them into `hiddenBatched` instead, so the model
19992
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
19993
+ // over a Server Action, which preserves the type, never as a URL param.
19994
+ includeBatched: external_exports.boolean().optional(),
19995
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
19996
+ cursor: external_exports.string().optional()
19997
+ });
19998
+ var ListVaultDerefsResponse = external_exports.object({
19999
+ items: external_exports.array(VaultDeref),
20000
+ nextCursor: external_exports.string().nullable(),
20001
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
20002
+ // this page — it is the count the "N hidden" line and its toggle speak for.
20003
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
20004
+ hiddenBatched: external_exports.number().int().nonnegative()
20005
+ });
19526
20006
  var VaultKeyCustody = external_exports.string();
19527
20007
  var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
19528
20008
  var VAULT_CONSENT_VERSION = 1;
@@ -19902,7 +20382,7 @@ var TopSourcesQuery = external_exports.object({
19902
20382
  // Omit for both kinds.
19903
20383
  kind: external_exports.enum(SOURCE_KINDS).optional()
19904
20384
  });
19905
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
20385
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
19906
20386
  var ScanCoverageProvider = external_exports.object({
19907
20387
  provider: Provider,
19908
20388
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -20214,9 +20694,8 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
20214
20694
  );
20215
20695
 
20216
20696
  // ../packages/persistence/src/database.ts
20217
- import { randomUUID as randomUUID9 } from "crypto";
20218
- import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
20219
- import { join as join2, sep } from "path";
20697
+ import { randomUUID as randomUUID10 } from "crypto";
20698
+ import { join as join3, sep } from "path";
20220
20699
  import { DatabaseSync } from "node:sqlite";
20221
20700
 
20222
20701
  // ../packages/persistence/src/ids.ts
@@ -20250,6 +20729,212 @@ function captureId(sessionId, contentHash, filePath = null) {
20250
20729
  );
20251
20730
  }
20252
20731
 
20732
+ // ../packages/persistence/src/internal/snapshot.ts
20733
+ import { randomUUID } from "crypto";
20734
+ import { existsSync as existsSync2, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
20735
+ import { basename, dirname as dirname2, join as join2 } from "path";
20736
+
20737
+ // ../packages/persistence/src/paths.ts
20738
+ import {
20739
+ chmodSync,
20740
+ linkSync,
20741
+ lstatSync,
20742
+ mkdirSync,
20743
+ renameSync,
20744
+ rmSync,
20745
+ writeFileSync
20746
+ } from "fs";
20747
+ import { threadId } from "worker_threads";
20748
+ var DATA_DIR_MODE = 448;
20749
+ var DATA_FILE_MODE = 384;
20750
+ var DB_FILENAME = "aka.db";
20751
+ function isSymlink(path) {
20752
+ try {
20753
+ return lstatSync(path).isSymbolicLink();
20754
+ } catch {
20755
+ return false;
20756
+ }
20757
+ }
20758
+ function chmodBestEffort(path, mode) {
20759
+ if (isSymlink(path)) return;
20760
+ try {
20761
+ chmodSync(path, mode);
20762
+ } catch {
20763
+ }
20764
+ }
20765
+ function tightenDir(dir) {
20766
+ chmodBestEffort(dir, DATA_DIR_MODE);
20767
+ }
20768
+ function ensureDataDirSync(dir) {
20769
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
20770
+ tightenDir(dir);
20771
+ }
20772
+ function dbSidecars(file2) {
20773
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
20774
+ }
20775
+ function tightenFile(file2) {
20776
+ chmodBestEffort(file2, DATA_FILE_MODE);
20777
+ }
20778
+ function tightenPerms(file2) {
20779
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
20780
+ }
20781
+ function writeOwnerOnlyFileSync(file2, data) {
20782
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
20783
+ try {
20784
+ rmSync(tmp, { force: true });
20785
+ } catch {
20786
+ }
20787
+ try {
20788
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20789
+ renameSync(tmp, file2);
20790
+ } finally {
20791
+ try {
20792
+ rmSync(tmp, { force: true });
20793
+ } catch {
20794
+ }
20795
+ }
20796
+ tightenFile(file2);
20797
+ }
20798
+ function classifyOccupant(file2) {
20799
+ try {
20800
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
20801
+ return { kind: "gone" };
20802
+ } catch (err) {
20803
+ if (err.code === "ENOENT") return { kind: "gone" };
20804
+ return { kind: "unknown", cause: err };
20805
+ }
20806
+ }
20807
+ var KeyUnclaimableError = class extends Error {
20808
+ code = "key-unclaimable";
20809
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
20810
+ // defines the property anyway, so an error carrying nothing would still answer
20811
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
20812
+ // captured and then lost, which is worse than its plain absence.
20813
+ constructor(message, cause) {
20814
+ super(message, cause === void 0 ? void 0 : { cause });
20815
+ this.name = "KeyUnclaimableError";
20816
+ }
20817
+ };
20818
+ function createOwnerOnlyFileSync(file2, data) {
20819
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
20820
+ try {
20821
+ rmSync(tmp, { force: true });
20822
+ } catch {
20823
+ }
20824
+ let created;
20825
+ try {
20826
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20827
+ created = publishByLink(tmp, file2, data);
20828
+ } finally {
20829
+ try {
20830
+ rmSync(tmp, { force: true });
20831
+ } catch {
20832
+ }
20833
+ }
20834
+ if (created) tightenFile(file2);
20835
+ return created;
20836
+ }
20837
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
20838
+ function publishByLink(tmp, file2, data) {
20839
+ try {
20840
+ linkSync(tmp, file2);
20841
+ return true;
20842
+ } catch (err) {
20843
+ const code = err.code;
20844
+ if (code === "EEXIST") return false;
20845
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
20846
+ }
20847
+ try {
20848
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
20849
+ return true;
20850
+ } catch (err) {
20851
+ if (err.code === "EEXIST") return false;
20852
+ throw err;
20853
+ }
20854
+ }
20855
+
20856
+ // ../packages/persistence/src/internal/snapshot.ts
20857
+ function backupPath(file2, tag) {
20858
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
20859
+ }
20860
+ var STALE_PARTIAL_MS = 5 * 6e4;
20861
+ function reapStalePartials(file2) {
20862
+ const dir = dirname2(file2);
20863
+ const prefix = `${basename(file2)}.`;
20864
+ let entries;
20865
+ try {
20866
+ entries = readdirSync2(dir);
20867
+ } catch {
20868
+ return;
20869
+ }
20870
+ for (const name of entries) {
20871
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
20872
+ const partial2 = join2(dir, name);
20873
+ try {
20874
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
20875
+ rmSync2(partial2, { force: true });
20876
+ }
20877
+ } catch {
20878
+ }
20879
+ }
20880
+ }
20881
+ function snapshotStore(db, backup) {
20882
+ const partial2 = `${backup}.partial`;
20883
+ try {
20884
+ rmSync2(partial2, { force: true });
20885
+ db.prepare("VACUUM INTO ?").run(partial2);
20886
+ tightenFile(partial2);
20887
+ renameSync2(partial2, backup);
20888
+ } catch (error51) {
20889
+ try {
20890
+ rmSync2(partial2, { force: true });
20891
+ } catch {
20892
+ }
20893
+ throw error51;
20894
+ }
20895
+ }
20896
+ function moveStoreAside(file2, backup) {
20897
+ const undo = [];
20898
+ renameSync2(file2, backup);
20899
+ undo.push([backup, file2]);
20900
+ try {
20901
+ for (const sidecar of dbSidecars(file2)) {
20902
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
20903
+ try {
20904
+ renameSync2(sidecar, moved);
20905
+ undo.push([moved, sidecar]);
20906
+ } catch {
20907
+ rmSync2(sidecar, { force: true });
20908
+ }
20909
+ }
20910
+ } catch (error51) {
20911
+ for (const [from, to] of undo.reverse()) {
20912
+ try {
20913
+ renameSync2(from, to);
20914
+ } catch {
20915
+ }
20916
+ }
20917
+ throw error51;
20918
+ }
20919
+ tightenPerms(backup);
20920
+ }
20921
+ function discardStore(file2, backup) {
20922
+ try {
20923
+ rmSync2(file2, { force: true });
20924
+ for (const sidecar of dbSidecars(file2)) {
20925
+ rmSync2(sidecar, { force: true });
20926
+ }
20927
+ } catch (error51) {
20928
+ if (existsSync2(file2)) {
20929
+ try {
20930
+ rmSync2(backup, { force: true });
20931
+ } catch {
20932
+ }
20933
+ }
20934
+ throw error51;
20935
+ }
20936
+ }
20937
+
20253
20938
  // ../packages/persistence/src/internal/sql-text.ts
20254
20939
  function escapeLikePattern(s) {
20255
20940
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -20391,55 +21076,6 @@ function mapRowsTolerant(rows, map2) {
20391
21076
  return out;
20392
21077
  }
20393
21078
 
20394
- // ../packages/persistence/src/paths.ts
20395
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
20396
- var DATA_DIR_MODE = 448;
20397
- var DATA_FILE_MODE = 384;
20398
- var DB_FILENAME = "aka.db";
20399
- function chmodBestEffort(path, mode) {
20400
- try {
20401
- chmodSync(path, mode);
20402
- } catch {
20403
- }
20404
- }
20405
- function tightenDir(dir) {
20406
- chmodBestEffort(dir, DATA_DIR_MODE);
20407
- }
20408
- function ensureDataDirSync(dir) {
20409
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
20410
- tightenDir(dir);
20411
- }
20412
- function dbSidecars(file2) {
20413
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
20414
- }
20415
- function tightenFile(file2) {
20416
- try {
20417
- if (lstatSync(file2).isSymbolicLink()) return;
20418
- } catch {
20419
- }
20420
- chmodBestEffort(file2, DATA_FILE_MODE);
20421
- }
20422
- function tightenPerms(file2) {
20423
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
20424
- }
20425
- function writeOwnerOnlyFileSync(file2, data) {
20426
- const tmp = `${file2}.${String(process.pid)}.tmp`;
20427
- try {
20428
- rmSync(tmp, { force: true });
20429
- } catch {
20430
- }
20431
- try {
20432
- writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20433
- renameSync(tmp, file2);
20434
- } finally {
20435
- try {
20436
- rmSync(tmp, { force: true });
20437
- } catch {
20438
- }
20439
- }
20440
- tightenFile(file2);
20441
- }
20442
-
20443
21079
  // ../packages/persistence/src/migrations.ts
20444
21080
  function describeObject(object2) {
20445
21081
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -20555,9 +21191,9 @@ function applyLegacyDropMigration(db, file2) {
20555
21191
  }
20556
21192
  }
20557
21193
  function backupBeforeLegacyDrop(db, file2) {
20558
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
20559
- db.prepare("VACUUM INTO ?").run(backup);
20560
- tightenFile(backup);
21194
+ reapStalePartials(file2);
21195
+ const backup = backupPath(file2, "pre-drop");
21196
+ snapshotStore(db, backup);
20561
21197
  return backup;
20562
21198
  }
20563
21199
  var TOKEN_USAGE_COLUMNS = [
@@ -20901,6 +21537,25 @@ function parseJsonObject(s) {
20901
21537
  return void 0;
20902
21538
  }
20903
21539
 
21540
+ // ../packages/persistence/src/internal/keyset-cursor.ts
21541
+ function encodeKeysetCursor(payload) {
21542
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
21543
+ }
21544
+ function decodeKeysetCursor(cursor) {
21545
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
21546
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
21547
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
21548
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
21549
+ // a null cursor, which a caller reads as "end of list". That is the one
21550
+ // outcome a cursor that does not decode must never produce, since the
21551
+ // documented behaviour above is to restart from the top. (`1e999` is valid
21552
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
21553
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
21554
+ return parsed;
21555
+ }
21556
+ return null;
21557
+ }
21558
+
20904
21559
  // ../packages/persistence/src/repositories/activity.ts
20905
21560
  var DAY_MS = 864e5;
20906
21561
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -20946,16 +21601,6 @@ function utcWindow(nowMs) {
20946
21601
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
20947
21602
  return { startMs, endMs: startMs + DAY_MS };
20948
21603
  }
20949
- function encodeCursor(payload) {
20950
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
20951
- }
20952
- function decodeCursor(cursor) {
20953
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20954
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
20955
- return parsed;
20956
- }
20957
- return null;
20958
- }
20959
21604
  var DB_EVENT_TYPE_TO_KIND = {
20960
21605
  session: "session",
20961
21606
  prompt: "prompt",
@@ -21100,7 +21745,7 @@ var SqliteActivityRepository = class {
21100
21745
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
21101
21746
  }
21102
21747
  listSessions(query) {
21103
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
21748
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
21104
21749
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
21105
21750
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
21106
21751
  const conditions = [SESSION_ROOT];
@@ -21174,7 +21819,7 @@ var SqliteActivityRepository = class {
21174
21819
  )
21175
21820
  );
21176
21821
  const last = page[page.length - 1];
21177
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
21822
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
21178
21823
  return Promise.resolve({ items, nextCursor, emptyCount });
21179
21824
  }
21180
21825
  getSession(sessionId) {
@@ -22047,7 +22692,7 @@ var SqliteEventsRepository = class {
22047
22692
  };
22048
22693
 
22049
22694
  // ../packages/persistence/src/repositories/exceptions.ts
22050
- import { randomUUID } from "crypto";
22695
+ import { randomUUID as randomUUID2 } from "crypto";
22051
22696
 
22052
22697
  // ../packages/persistence/src/internal/sqlite-errors.ts
22053
22698
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -22083,8 +22728,9 @@ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
22083
22728
  AND conditions IS NULL
22084
22729
  AND ${ACTIVE_PREDICATE}`;
22085
22730
  var SqliteExceptionsRepository = class {
22086
- constructor(db) {
22731
+ constructor(db, now = () => Date.now()) {
22087
22732
  this.db = db;
22733
+ this.now = now;
22088
22734
  this.consumeStmt = db.prepare(
22089
22735
  `UPDATE exceptions
22090
22736
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -22102,6 +22748,7 @@ var SqliteExceptionsRepository = class {
22102
22748
  );
22103
22749
  }
22104
22750
  db;
22751
+ now;
22105
22752
  consumeStmt;
22106
22753
  insertBlockedStmt;
22107
22754
  sweepBlockedStmt;
@@ -22128,8 +22775,8 @@ var SqliteExceptionsRepository = class {
22128
22775
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
22129
22776
  );
22130
22777
  }
22131
- const id = randomUUID();
22132
- const now = Date.now();
22778
+ const id = randomUUID2();
22779
+ const now = this.now();
22133
22780
  try {
22134
22781
  this.insertExceptionRow(id, input, now);
22135
22782
  } catch (err) {
@@ -22207,7 +22854,7 @@ var SqliteExceptionsRepository = class {
22207
22854
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
22208
22855
  const rows = allRows(
22209
22856
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
22210
- opts?.includeTerminal ? {} : { now: Date.now() }
22857
+ opts?.includeTerminal ? {} : { now: this.now() }
22211
22858
  );
22212
22859
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
22213
22860
  return Promise.resolve(exceptions);
@@ -22242,7 +22889,7 @@ var SqliteExceptionsRepository = class {
22242
22889
  * already revoked.
22243
22890
  */
22244
22891
  revoke(id, revokedBy, reason) {
22245
- const now = Date.now();
22892
+ const now = this.now();
22246
22893
  const result = this.db.prepare(
22247
22894
  `UPDATE exceptions
22248
22895
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -22256,7 +22903,7 @@ var SqliteExceptionsRepository = class {
22256
22903
  * callers must treat identically — means it does not and the detection is
22257
22904
  * enforced as usual. Deliberately NOT wrapped in try/catch.
22258
22905
  */
22259
- consume(id, now = Date.now()) {
22906
+ consume(id, now = this.now()) {
22260
22907
  const result = this.consumeStmt.run({ id, now });
22261
22908
  return Promise.resolve(Number(result.changes) === 1);
22262
22909
  }
@@ -22265,7 +22912,7 @@ var SqliteExceptionsRepository = class {
22265
22912
  * version — what rides the policy bundle to the hook. Grants written under
22266
22913
  * a different (rotated-away) key never match, so they are excluded at read.
22267
22914
  */
22268
- activeBundleEntries(keyVersion, now = Date.now()) {
22915
+ activeBundleEntries(keyVersion, now = this.now()) {
22269
22916
  const rows = allRows(
22270
22917
  this.db.prepare(
22271
22918
  `SELECT * FROM exceptions
@@ -22297,7 +22944,7 @@ var SqliteExceptionsRepository = class {
22297
22944
  * than the retention window on every write, so the ledger self-limits.
22298
22945
  */
22299
22946
  recordBlocked(entry) {
22300
- const now = Date.now();
22947
+ const now = this.now();
22301
22948
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
22302
22949
  this.insertBlockedStmt.run({
22303
22950
  reference: entry.reference,
@@ -22320,7 +22967,7 @@ var SqliteExceptionsRepository = class {
22320
22967
  WHERE blocked_at > :cutoff
22321
22968
  ORDER BY blocked_at DESC, rowid DESC`
22322
22969
  ),
22323
- { cutoff: Date.now() - windowMs }
22970
+ { cutoff: this.now() - windowMs }
22324
22971
  );
22325
22972
  return Promise.resolve(
22326
22973
  rows.map((row) => ({
@@ -22348,8 +22995,9 @@ var SqliteExceptionsRepository = class {
22348
22995
  * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
22349
22996
  * grant instead. Fail closed until reveal-side condition evaluation exists.
22350
22997
  */
22351
- activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
22998
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
22352
22999
  try {
23000
+ const at = now ?? this.now();
22353
23001
  const row = getRow(
22354
23002
  this.db.prepare(
22355
23003
  `SELECT id FROM exceptions
@@ -22358,7 +23006,7 @@ var SqliteExceptionsRepository = class {
22358
23006
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22359
23007
  LIMIT 1`
22360
23008
  ),
22361
- { ruleId, valueFingerprint, keyVersion, now }
23009
+ { ruleId, valueFingerprint, keyVersion, now: at }
22362
23010
  );
22363
23011
  return Promise.resolve(row ?? null);
22364
23012
  } catch (err) {
@@ -22372,7 +23020,7 @@ var SqliteExceptionsRepository = class {
22372
23020
  * predicate, so correctness never depends on this sweep; it only bounds how
22373
23021
  * long the audit evidence is kept locally. Returns the deleted count.
22374
23022
  */
22375
- sweepTerminal(retentionMs, now = Date.now()) {
23023
+ sweepTerminal(retentionMs, now = this.now()) {
22376
23024
  const result = this.db.prepare(
22377
23025
  `DELETE FROM exceptions
22378
23026
  WHERE updated_at < :cutoff
@@ -22435,6 +23083,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
22435
23083
 
22436
23084
  // ../packages/persistence/src/repositories/findings.ts
22437
23085
  var PREVIEW_INSTANCES_PER_GROUP = 200;
23086
+ var SCAN_BATCH_ROWS = 1e3;
23087
+ var DEFAULT_LOCATIONS_LIMIT = 100;
23088
+ var LOCATION_RULE_IDS_CAP = 20;
23089
+ function compareLocationOrder(a, b) {
23090
+ return compareFindingGroupOrder(
23091
+ {
23092
+ severity: a.maxSeverity,
23093
+ latestDetectedAt: a.latestDetectedAt,
23094
+ id: ""
23095
+ },
23096
+ {
23097
+ severity: b.maxSeverity,
23098
+ latestDetectedAt: b.latestDetectedAt,
23099
+ id: ""
23100
+ }
23101
+ );
23102
+ }
22438
23103
  var CONCAT_SEP = ",";
22439
23104
  var TUPLE_SEP = "|";
22440
23105
  function splitConcat(value) {
@@ -22447,6 +23112,33 @@ function deriveInstanceStatus(row) {
22447
23112
  latestResolutionStatus: row.latest_status
22448
23113
  });
22449
23114
  }
23115
+ function encodeGroupCursor(group) {
23116
+ const payload = {
23117
+ sev: group.severity,
23118
+ t: group.latestDetectedAt,
23119
+ id: group.id
23120
+ };
23121
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23122
+ }
23123
+ function decodeGroupCursor(cursor) {
23124
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23125
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
23126
+ return {
23127
+ severity: parsed.sev,
23128
+ latestDetectedAt: parsed.t,
23129
+ id: parsed.id
23130
+ };
23131
+ }
23132
+ return null;
23133
+ }
23134
+ function firstAfter(sorted, cursor) {
23135
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
23136
+ return index === -1 ? sorted.length : index;
23137
+ }
23138
+ function findDeepLinked(sorted, page, id) {
23139
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
23140
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
23141
+ }
22450
23142
  var DAY_MS3 = 864e5;
22451
23143
  var SqliteFindingsRepository = class {
22452
23144
  constructor(db) {
@@ -22556,8 +23248,13 @@ var SqliteFindingsRepository = class {
22556
23248
  */
22557
23249
  listGroupedFindings(query) {
22558
23250
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
22559
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
22560
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
23251
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
23252
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
23253
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
23254
+ const sessionParams = {
23255
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
23256
+ ...fromMs === void 0 ? {} : { fromMs }
23257
+ };
22561
23258
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
22562
23259
  predicate,
22563
23260
  params: sessionParams
@@ -22565,7 +23262,8 @@ var SqliteFindingsRepository = class {
22565
23262
  const rows = allRows(
22566
23263
  this.db.prepare(
22567
23264
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
22568
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
23265
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
23266
+ kind, finding_key, latest_status
22569
23267
  FROM (
22570
23268
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
22571
23269
  d.severity AS severity, f.masked_match AS masked_match,
@@ -22575,6 +23273,7 @@ var SqliteFindingsRepository = class {
22575
23273
  json_extract(e.attributes, '$.repo') AS repo,
22576
23274
  json_extract(e.attributes, '$.file_path') AS file,
22577
23275
  json_extract(e.attributes, '$.tool_name') AS tool_name,
23276
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
22578
23277
  e.event_type AS kind, f.finding_key AS finding_key,
22579
23278
  latest.status AS latest_status,
22580
23279
  ROW_NUMBER() OVER (
@@ -22606,6 +23305,8 @@ var SqliteFindingsRepository = class {
22606
23305
  repo: r.repo ?? "",
22607
23306
  file: r.file ?? "",
22608
23307
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
23308
+ eventId: r.event_id,
23309
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
22609
23310
  status: deriveInstanceStatus(r)
22610
23311
  }));
22611
23312
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -22629,18 +23330,23 @@ var SqliteFindingsRepository = class {
22629
23330
  groups: sorted.length
22630
23331
  };
22631
23332
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
23333
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
23334
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
23335
+ const page = sorted.slice(start, start + limit);
23336
+ const lastOnPage = page.at(-1);
23337
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
23338
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
22632
23339
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
22633
- const items = sorted.slice(0, limit).map(
22634
- (g) => statusSet ? {
22635
- ...g,
22636
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
22637
- } : g
22638
- );
23340
+ const narrow = (g) => statusSet ? {
23341
+ ...g,
23342
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
23343
+ } : g;
23344
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
22639
23345
  return Promise.resolve({
22640
23346
  totals,
22641
23347
  facets,
22642
23348
  items,
22643
- nextCursor: null,
23349
+ nextCursor,
22644
23350
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
22645
23351
  });
22646
23352
  }
@@ -22672,6 +23378,266 @@ var SqliteFindingsRepository = class {
22672
23378
  * request actually carries a `q`. (Substring matching is unaffected by a
22673
23379
  * path repeating across tuples.)
22674
23380
  */
23381
+ /**
23382
+ * The instance-level (flat) findings list: one row per finding, newest first,
23383
+ * paged by keyset.
23384
+ *
23385
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
23386
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
23387
+ * them changes no reported number. Severity, subtype, provider, action,
23388
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
23389
+ * facet excludes its own filter, so a row the filter rejects still has to be
23390
+ * counted. Pushing any of them into SQL would silently empty its own facet.
23391
+ * Several could not be expressed there anyway: status comes from the one
23392
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
23393
+ * none of the mappers names", which no IN-list can say.
23394
+ *
23395
+ * The scan runs from the top of the scope on every request, not from the
23396
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
23397
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
23398
+ * while the counting runs, and only the page itself is retained.
23399
+ */
23400
+ listFindingInstances(query) {
23401
+ const opts = {
23402
+ severity: query.severity,
23403
+ subtype: query.subtype,
23404
+ providers: query.provider,
23405
+ actions: query.action,
23406
+ statuses: query.status,
23407
+ tools: query.tool,
23408
+ repo: query.repo,
23409
+ file: query.file,
23410
+ q: query.q
23411
+ };
23412
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
23413
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23414
+ const accumulator = createInstanceFacetAccumulator(opts);
23415
+ const items = [];
23416
+ let total = 0;
23417
+ let last;
23418
+ let hasMore = false;
23419
+ for (const row of this.scanFindingRows({
23420
+ sessionId: query.sessionId,
23421
+ from: query.from
23422
+ })) {
23423
+ accumulator.add(row);
23424
+ if (!matchesInstanceFilters(row, opts)) continue;
23425
+ total += 1;
23426
+ if (items.length < limit) {
23427
+ items.push(toInstanceDetail(row));
23428
+ last = row;
23429
+ } else {
23430
+ hasMore = true;
23431
+ }
23432
+ }
23433
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
23434
+ if (cursor !== null) {
23435
+ const resumed = this.pageAfter(cursor, opts, limit, query);
23436
+ return Promise.resolve({
23437
+ totals: { findings: total },
23438
+ facets: accumulator.facets(),
23439
+ items: resumed.items,
23440
+ nextCursor: resumed.nextCursor
23441
+ });
23442
+ }
23443
+ return Promise.resolve({
23444
+ totals: { findings: total },
23445
+ facets: accumulator.facets(),
23446
+ items,
23447
+ nextCursor
23448
+ });
23449
+ }
23450
+ /**
23451
+ * The page of matching rows strictly after `cursor`. Separate from the
23452
+ * counting pass because that one starts at the top of the scope by design;
23453
+ * this one narrows the scan with the same keyset predicate the activity list
23454
+ * uses, so a later page costs less than the first rather than more.
23455
+ */
23456
+ pageAfter(cursor, opts, limit, query) {
23457
+ const items = [];
23458
+ let last;
23459
+ let hasMore = false;
23460
+ for (const row of this.scanFindingRows({
23461
+ sessionId: query.sessionId,
23462
+ from: query.from,
23463
+ after: cursor
23464
+ })) {
23465
+ if (!matchesInstanceFilters(row, opts)) continue;
23466
+ if (items.length < limit) {
23467
+ items.push(toInstanceDetail(row));
23468
+ last = row;
23469
+ } else {
23470
+ hasMore = true;
23471
+ break;
23472
+ }
23473
+ }
23474
+ return {
23475
+ items,
23476
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
23477
+ };
23478
+ }
23479
+ /**
23480
+ * The same findings folded by location: repository, then file within it.
23481
+ *
23482
+ * The grouping keys come from the capturing event's attributes, which is what
23483
+ * the local store relates a finding to — there is no finding↔asset row to
23484
+ * group by instead. A repo or file the event did not record folds into the
23485
+ * empty-string bucket, which the view renders but does not link, since no
23486
+ * filter can name it.
23487
+ */
23488
+ listFindingLocations(query) {
23489
+ const opts = {
23490
+ severity: query.severity,
23491
+ subtype: query.subtype,
23492
+ providers: query.provider,
23493
+ actions: query.action,
23494
+ statuses: query.status,
23495
+ tools: query.tool,
23496
+ q: query.q
23497
+ };
23498
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
23499
+ const byRepo = /* @__PURE__ */ new Map();
23500
+ let total = 0;
23501
+ for (const row of this.scanFindingRows({
23502
+ sessionId: query.sessionId,
23503
+ from: query.from
23504
+ })) {
23505
+ if (!matchesInstanceFilters(row, opts)) continue;
23506
+ total += 1;
23507
+ let files = byRepo.get(row.repo);
23508
+ if (files === void 0) {
23509
+ files = /* @__PURE__ */ new Map();
23510
+ byRepo.set(row.repo, files);
23511
+ }
23512
+ let acc = files.get(row.file);
23513
+ if (acc === void 0) {
23514
+ acc = newLocationAccumulator();
23515
+ files.set(row.file, acc);
23516
+ }
23517
+ addToLocation(acc, row);
23518
+ }
23519
+ let fileCount = 0;
23520
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
23521
+ fileCount += files.size;
23522
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
23523
+ file: file2,
23524
+ instanceCount: acc.instanceCount,
23525
+ maxSeverity: acc.maxSeverity,
23526
+ latestDetectedAt: acc.latestDetectedAt,
23527
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
23528
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
23529
+ })).sort(compareLocationOrder);
23530
+ const rollup = fileRows.reduce(
23531
+ (a, f) => ({
23532
+ instanceCount: a.instanceCount + f.instanceCount,
23533
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
23534
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
23535
+ }),
23536
+ {
23537
+ instanceCount: 0,
23538
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
23539
+ latestDetectedAt: ""
23540
+ }
23541
+ );
23542
+ const statuses = fileRows.map((f) => f.status);
23543
+ const folded = foldGroupStatus(statuses);
23544
+ return {
23545
+ repo,
23546
+ instanceCount: rollup.instanceCount,
23547
+ maxSeverity: rollup.maxSeverity,
23548
+ latestDetectedAt: rollup.latestDetectedAt,
23549
+ ...folded === void 0 ? {} : { status: folded },
23550
+ files: fileRows
23551
+ };
23552
+ });
23553
+ repos.sort(compareLocationOrder);
23554
+ return Promise.resolve({
23555
+ totals: { findings: total, repos: repos.length, files: fileCount },
23556
+ items: repos.slice(0, limit),
23557
+ hasMore: repos.length > limit
23558
+ });
23559
+ }
23560
+ /**
23561
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
23562
+ *
23563
+ * A generator so a caller streams the scope without it ever being an array:
23564
+ * the flat list counts and facets the whole filtered scope, which on a large
23565
+ * store is far more rows than any page. Each batch advances the same keyset
23566
+ * predicate the page read uses, so the scan is a sequence of bounded reads
23567
+ * rather than one unbounded result set.
23568
+ *
23569
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
23570
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
23571
+ * makes it a point lookup per row, and the derived table would re-materialize
23572
+ * a window over the whole resolution table once per batch.
23573
+ *
23574
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
23575
+ * would be missing from its own facet, which is computed by excluding that
23576
+ * dimension — see listFindingInstances.
23577
+ */
23578
+ *scanFindingRows(scope) {
23579
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
23580
+ const params = [];
23581
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
23582
+ conditions.push("e.root_session_id = ?");
23583
+ params.push(scope.sessionId);
23584
+ }
23585
+ if (scope.from !== void 0) {
23586
+ conditions.push("e.started_at >= ?");
23587
+ params.push(isoToEpochMillis(scope.from));
23588
+ }
23589
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
23590
+ d.severity AS severity, f.masked_match AS masked_match,
23591
+ f.action_taken AS action_taken, f.confidence AS confidence,
23592
+ e.started_at AS occurred_at,
23593
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
23594
+ json_extract(e.attributes, '$.repo') AS repo,
23595
+ json_extract(e.attributes, '$.file_path') AS file,
23596
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
23597
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
23598
+ e.event_type AS kind, f.finding_key AS finding_key,
23599
+ ${latestResolutionStatusSql("f")} AS latest_status
23600
+ FROM inspection_findings f
23601
+ JOIN audit_events e ON e.id = f.audit_event_id
23602
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
23603
+ WHERE ${conditions.join(" AND ")}
23604
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
23605
+ ORDER BY e.started_at DESC, f.id DESC
23606
+ LIMIT ?`;
23607
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
23608
+ for (; ; ) {
23609
+ const rows = allRows(this.db.prepare(sql), [
23610
+ ...params,
23611
+ after.startedAtMs,
23612
+ after.startedAtMs,
23613
+ after.id,
23614
+ SCAN_BATCH_ROWS
23615
+ ]);
23616
+ for (const r of rows) {
23617
+ yield {
23618
+ id: r.id,
23619
+ ruleId: r.rule_id,
23620
+ category: r.category,
23621
+ severity: r.severity,
23622
+ maskedMatch: r.masked_match,
23623
+ actionTaken: r.action_taken,
23624
+ confidence: r.confidence,
23625
+ occurredAt: epochMillisToIso(r.occurred_at),
23626
+ sourceTool: r.source_tool,
23627
+ repo: r.repo ?? "",
23628
+ file: r.file ?? "",
23629
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
23630
+ eventId: r.event_id,
23631
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
23632
+ status: deriveInstanceStatus(r)
23633
+ };
23634
+ }
23635
+ if (rows.length < SCAN_BATCH_ROWS) return;
23636
+ const lastRow = rows[rows.length - 1];
23637
+ if (lastRow === void 0) return;
23638
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
23639
+ }
23640
+ }
22675
23641
  groupAggregates(withSearchText, scope) {
22676
23642
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
22677
23643
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -22932,7 +23898,7 @@ var SqliteInspectionFindingsRepository = class {
22932
23898
  };
22933
23899
 
22934
23900
  // ../packages/persistence/src/repositories/installed-packs.ts
22935
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
23901
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
22936
23902
 
22937
23903
  // ../packages/persistence/src/semver.ts
22938
23904
  function parse4(version2) {
@@ -23083,7 +24049,7 @@ var SqliteInstalledPacksRepository = class {
23083
24049
  let behind = false;
23084
24050
  for (const row of rows) {
23085
24051
  const params = {
23086
- id: randomUUID2(),
24052
+ id: randomUUID3(),
23087
24053
  namespace: row.namespace,
23088
24054
  packId: row.packId,
23089
24055
  version: row.version,
@@ -23095,7 +24061,7 @@ var SqliteInstalledPacksRepository = class {
23095
24061
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
23096
24062
  this.upsertAvailableStmt.run({
23097
24063
  ...params,
23098
- id: randomUUID2(),
24064
+ id: randomUUID3(),
23099
24065
  recordedBy: meta3?.recordedBy ?? null
23100
24066
  });
23101
24067
  } else {
@@ -23418,14 +24384,15 @@ var SqliteInventoryRepository = class {
23418
24384
  };
23419
24385
 
23420
24386
  // ../packages/persistence/src/repositories/inventory-assets.ts
23421
- import { randomUUID as randomUUID3 } from "crypto";
24387
+ import { randomUUID as randomUUID4 } from "crypto";
23422
24388
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
23423
24389
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
23424
24390
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
23425
24391
  var HARNESS_LABELS = {
23426
24392
  claudecode: "Claude Code",
23427
24393
  cursor: "Cursor",
23428
- codex: "Codex"
24394
+ codex: "Codex",
24395
+ antigravity: "Antigravity"
23429
24396
  };
23430
24397
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
23431
24398
  var EMPTY_PROJECT_AGG = {
@@ -23440,6 +24407,7 @@ function resolveHarnessId(attrs, row) {
23440
24407
  if (t.includes("claudecode") || t === "claude") return "claudecode";
23441
24408
  if (t.includes("cursor")) return "cursor";
23442
24409
  if (t.includes("codex")) return "codex";
24410
+ if (t.includes("antigravity")) return "antigravity";
23443
24411
  return null;
23444
24412
  }
23445
24413
  function isLiveRealClaudeCode(rows) {
@@ -23898,7 +24866,7 @@ var SqliteInventoryAssetsRepository = class {
23898
24866
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
23899
24867
  VALUES (:id, :projectId, :path, :access, :now, :now)
23900
24868
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
23901
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
24869
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
23902
24870
  }
23903
24871
  return true;
23904
24872
  }
@@ -23919,7 +24887,7 @@ var SqliteInventoryAssetsRepository = class {
23919
24887
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
23920
24888
  VALUES (:id, :assetId, :trust, :now, :now)
23921
24889
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
23922
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
24890
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
23923
24891
  }
23924
24892
  this.configRowsCache = void 0;
23925
24893
  return "ok";
@@ -24216,7 +25184,7 @@ var SqliteInventoryAssetsRepository = class {
24216
25184
  };
24217
25185
 
24218
25186
  // ../packages/persistence/src/repositories/policies.ts
24219
- import { randomUUID as randomUUID4 } from "crypto";
25187
+ import { randomUUID as randomUUID5 } from "crypto";
24220
25188
  var SqlitePoliciesRepository = class {
24221
25189
  constructor(db) {
24222
25190
  this.db = db;
@@ -24251,7 +25219,7 @@ var SqlitePoliciesRepository = class {
24251
25219
  failOpenTransaction(this.db, () => {
24252
25220
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
24253
25221
  stmt.run({
24254
- id: randomUUID4(),
25222
+ id: randomUUID5(),
24255
25223
  target: JSON.stringify({ category }),
24256
25224
  action,
24257
25225
  now: Date.now()
@@ -24271,7 +25239,7 @@ var SqlitePoliciesRepository = class {
24271
25239
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
24272
25240
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
24273
25241
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
24274
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
25242
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
24275
25243
  }
24276
25244
  // Caps every global per-category policy currently set to block/redact down
24277
25245
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -24339,7 +25307,7 @@ var SqlitePolicyCatalogRepository = class {
24339
25307
  };
24340
25308
 
24341
25309
  // ../packages/persistence/src/repositories/project-files.ts
24342
- import { randomUUID as randomUUID5 } from "crypto";
25310
+ import { randomUUID as randomUUID6 } from "crypto";
24343
25311
  var SqliteProjectFilesRepository = class {
24344
25312
  constructor(db) {
24345
25313
  this.db = db;
@@ -24371,7 +25339,7 @@ var SqliteProjectFilesRepository = class {
24371
25339
  const stamp = Math.max(now, maxStamp + 1);
24372
25340
  for (const file2 of scan2.files) {
24373
25341
  this.upsertStmt.run({
24374
- id: randomUUID5(),
25342
+ id: randomUUID6(),
24375
25343
  projectId,
24376
25344
  path: file2.path,
24377
25345
  name: file2.name,
@@ -24385,7 +25353,7 @@ var SqliteProjectFilesRepository = class {
24385
25353
  };
24386
25354
 
24387
25355
  // ../packages/persistence/src/repositories/resolutions.ts
24388
- import { randomUUID as randomUUID6 } from "crypto";
25356
+ import { randomUUID as randomUUID7 } from "crypto";
24389
25357
  var SqliteResolutionsRepository = class {
24390
25358
  constructor(db, now = () => Date.now()) {
24391
25359
  this.db = db;
@@ -24439,7 +25407,7 @@ var SqliteResolutionsRepository = class {
24439
25407
  */
24440
25408
  insertResolution(r) {
24441
25409
  this.insertStmt.run({
24442
- id: randomUUID6(),
25410
+ id: randomUUID7(),
24443
25411
  findingKey: r.findingKey,
24444
25412
  status: FindingStatus.parse(r.status),
24445
25413
  method: ResolutionMethod.parse(r.method),
@@ -24498,13 +25466,51 @@ var SqliteRuleProbeCacheRepository = class {
24498
25466
  this.readStmt = db.prepare(
24499
25467
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
24500
25468
  );
25469
+ this.countQuarantinedStmt = db.prepare(
25470
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
25471
+ );
25472
+ this.clearQuarantinedStmt = db.prepare(
25473
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
25474
+ );
24501
25475
  }
24502
25476
  db;
24503
25477
  upsertStmt;
24504
25478
  readStmt;
25479
+ countQuarantinedStmt;
25480
+ clearQuarantinedStmt;
24505
25481
  getVerdict(ruleKey) {
24506
25482
  return getRow(this.readStmt, { ruleKey });
24507
25483
  }
25484
+ /** How many rules are currently excluded by a cached quarantine verdict. */
25485
+ countQuarantined() {
25486
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
25487
+ }
25488
+ /**
25489
+ * Forgets every quarantine verdict, so the rules behind them are measured
25490
+ * again on the next load. This is the undo for a verdict the machine reached
25491
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
25492
+ * every later scan, and a timing verdict is a wall-clock judgement that a
25493
+ * loaded or slow machine can reach about a rule that is in fact fine.
25494
+ *
25495
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
25496
+ * keeping, and dropping it would make every rule pay the battery again.
25497
+ *
25498
+ * Reports `refused` from the write's own result rather than inferring it from
25499
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
25500
+ * swallows a contended DELETE (another writer holding the lock past
25501
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
25502
+ * leaves the count unchanged, which is indistinguishable from "there was
25503
+ * nothing to clear". An undo that reports success while the quarantines are
25504
+ * still in place is worse than one that fails, because the rules it claimed
25505
+ * to restore are silently still disabled.
25506
+ */
25507
+ clearQuarantined() {
25508
+ const before = this.countQuarantined();
25509
+ const committed = failOpenTransaction(this.db, () => {
25510
+ this.clearQuarantinedStmt.run();
25511
+ });
25512
+ return { refused: !committed, cleared: before - this.countQuarantined() };
25513
+ }
24508
25514
  setVerdict(ruleKey, verdict, worstProbeMs) {
24509
25515
  failOpenTransaction(this.db, () => {
24510
25516
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -24559,7 +25565,39 @@ var SqliteScanLedgerRepository = class {
24559
25565
  };
24560
25566
 
24561
25567
  // ../packages/persistence/src/repositories/secret-vault.ts
24562
- import { randomUUID as randomUUID7 } from "crypto";
25568
+ import { randomUUID as randomUUID8 } from "crypto";
25569
+ function pageLimit(requested, fallback) {
25570
+ if (requested === void 0) return fallback;
25571
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
25572
+ }
25573
+ function encodeReuseCursor(payload) {
25574
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
25575
+ }
25576
+ function decodeReuseCursor(cursor) {
25577
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
25578
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
25579
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
25580
+ // null cursor, which the caller reads as "end of list" — the one outcome a
25581
+ // malformed cursor must never produce, since restarting from the top is the
25582
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
25583
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
25584
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
25585
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
25586
+ }
25587
+ return null;
25588
+ }
25589
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
25590
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
25591
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
25592
+ v.occurrence_count, v.first_seen, v.last_seen`;
25593
+ function toSighting(row) {
25594
+ return {
25595
+ location: row.location,
25596
+ kind: row.kind,
25597
+ firstSeen: new Date(row.first_seen).toISOString(),
25598
+ lastSeen: new Date(row.last_seen).toISOString()
25599
+ };
25600
+ }
24563
25601
  var SELECT_COLUMNS = `
24564
25602
  pointer_id AS pointerId,
24565
25603
  value_fingerprint AS valueFingerprint,
@@ -24743,39 +25781,67 @@ var SqliteSecretVaultRepository = class {
24743
25781
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
24744
25782
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
24745
25783
  ).run({
24746
- id: randomUUID7(),
25784
+ id: randomUUID8(),
24747
25785
  pointerId: entry.pointerId,
24748
25786
  location: entry.location,
24749
25787
  kind: entry.kind,
24750
25788
  now
24751
25789
  });
24752
25790
  }
24753
- listSightings(pointerId) {
25791
+ /**
25792
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
25793
+ * than one query per row. A pointer with no sightings still gets an entry, so
25794
+ * the caller never has to distinguish "none" from "missing".
25795
+ *
25796
+ * The `IN` list is sized to the page, so this statement cannot be cached on
25797
+ * the instance the way the fixed-shape ones in the constructor are.
25798
+ */
25799
+ sightingsFor(pointerIds) {
25800
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
25801
+ if (pointerIds.length === 0) return byPointer;
24754
25802
  const rows = allRows(
24755
25803
  this.db.prepare(
24756
- `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
24757
- WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
25804
+ `SELECT pointer_id, location, kind, first_seen, last_seen
25805
+ FROM secret_vault_sighting
25806
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
25807
+ ORDER BY last_seen DESC`
24758
25808
  ),
24759
- { pointerId }
25809
+ pointerIds
24760
25810
  );
25811
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
25812
+ return byPointer;
25813
+ }
25814
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
25815
+ toInventoryEntries(rows) {
25816
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
24761
25817
  return rows.map((r) => ({
24762
- location: r.location,
24763
- kind: r.kind,
25818
+ pointerId: r.pointer_id,
25819
+ category: r.category,
25820
+ ...r.provider === null ? {} : { provider: r.provider },
25821
+ maskedMatch: r.masked_match,
25822
+ occurrences: r.occurrence_count,
24764
25823
  firstSeen: new Date(r.first_seen).toISOString(),
24765
- lastSeen: new Date(r.last_seen).toISOString()
25824
+ lastSeen: new Date(r.last_seen).toISOString(),
25825
+ revealGrantId: r.grant_id,
25826
+ sightings: sightings.get(r.pointer_id) ?? []
24766
25827
  }));
24767
25828
  }
24768
25829
  /**
24769
- * The dashboard inventory: every vaulted value's descriptor data joined with
24770
- * its sightings and the active reveal-to-model grant when one exists.
24771
- * Raw-free by construction — neither the fingerprint nor the ciphertext
24772
- * columns are selected.
25830
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
25831
+ * value's descriptor data joined with its sightings and the active
25832
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
25833
+ * the fingerprint nor the ciphertext columns are selected.
25834
+ *
25835
+ * `totals.values` counts the whole store, not the page, so the count a reader
25836
+ * sees never depends on how far they have paged.
24773
25837
  */
24774
- listInventory(now = Date.now()) {
25838
+ listInventory(query = {}, now = Date.now()) {
25839
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
25840
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
25841
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
24775
25842
  const rows = allRows(
24776
25843
  this.db.prepare(
24777
- `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
24778
- v.occurrence_count, v.first_seen, v.last_seen,
25844
+ `SELECT ${INVENTORY_COLUMNS},
24779
25845
  (SELECT e.id FROM exceptions e
24780
25846
  WHERE e.rule_id = v.rule_id
24781
25847
  AND e.value_fingerprint = v.value_fingerprint
@@ -24783,45 +25849,109 @@ var SqliteSecretVaultRepository = class {
24783
25849
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
24784
25850
  LIMIT 1) AS grant_id
24785
25851
  FROM secret_vault v
24786
- ORDER BY v.last_seen DESC`
25852
+ ${where}
25853
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
25854
+ LIMIT :limit`
24787
25855
  ),
24788
- { now }
25856
+ bindParams({
25857
+ now,
25858
+ limit: limit + 1,
25859
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
25860
+ })
24789
25861
  );
24790
- return rows.map((r) => ({
24791
- pointerId: r.pointer_id,
24792
- category: r.category,
24793
- ...r.provider === null ? {} : { provider: r.provider },
24794
- maskedMatch: r.masked_match,
24795
- occurrences: r.occurrence_count,
24796
- firstSeen: new Date(r.first_seen).toISOString(),
24797
- lastSeen: new Date(r.last_seen).toISOString(),
24798
- revealGrantId: r.grant_id,
24799
- sightings: this.listSightings(r.pointer_id)
24800
- }));
25862
+ const hasMore = rows.length > limit;
25863
+ const page = hasMore ? rows.slice(0, limit) : rows;
25864
+ const last = page[page.length - 1];
25865
+ return {
25866
+ totals: { values: this.countEntries() },
25867
+ items: this.toInventoryEntries(page),
25868
+ // Minted from the last row of the PAGE, never the extra probe row.
25869
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
25870
+ };
25871
+ }
25872
+ /**
25873
+ * Values reused on this machine — detected more than once, or written to more
25874
+ * than one location — most-reused first, one page at a time.
25875
+ *
25876
+ * Its own read rather than a filter over an inventory page: reuse is a
25877
+ * property of the whole store, and deriving it from 50 newest rows would
25878
+ * under-report exactly the values a reader most needs to see.
25879
+ */
25880
+ listReuse(query = {}, now = Date.now()) {
25881
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
25882
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
25883
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
25884
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
25885
+ const rows = allRows(
25886
+ this.db.prepare(
25887
+ `SELECT ${INVENTORY_COLUMNS},
25888
+ (SELECT e.id FROM exceptions e
25889
+ WHERE e.rule_id = v.rule_id
25890
+ AND e.value_fingerprint = v.value_fingerprint
25891
+ AND e.key_version = v.fingerprint_key_version
25892
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
25893
+ LIMIT 1) AS grant_id
25894
+ FROM secret_vault v
25895
+ WHERE ${REUSED_PREDICATE} ${after}
25896
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
25897
+ LIMIT :limit`
25898
+ ),
25899
+ bindParams({
25900
+ now,
25901
+ limit: limit + 1,
25902
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
25903
+ })
25904
+ );
25905
+ const hasMore = rows.length > limit;
25906
+ const page = hasMore ? rows.slice(0, limit) : rows;
25907
+ const last = page[page.length - 1];
25908
+ return {
25909
+ totals: { reused: this.countReused() },
25910
+ items: this.toInventoryEntries(page),
25911
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
25912
+ };
24801
25913
  }
24802
25914
  /**
24803
- * The de-reference trail, newest first. By default the batched, high-volume
24804
- * reasons (display, view-render) are hidden and counted instead — the rows
24805
- * that matter as a signal are the model crossings, and burying them under
24806
- * render noise would defeat the audit's purpose.
25915
+ * The de-reference trail, newest first, one page at a time. By default the
25916
+ * batched, high-volume reasons (display, view-render) are hidden and counted
25917
+ * instead — the rows that matter as a signal are the model crossings, and
25918
+ * burying them under render noise would defeat the audit's purpose.
25919
+ *
25920
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
25921
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
25922
+ * the reader pages.
24807
25923
  */
24808
- listDerefs(opts) {
24809
- const limit = opts?.limit ?? 200;
24810
- const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
25924
+ listDerefs(query = {}) {
25925
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
25926
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
25927
+ const conditions = [];
25928
+ if (query.includeBatched !== true) {
25929
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
25930
+ }
25931
+ if (cursor !== null) {
25932
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
25933
+ }
25934
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
24811
25935
  const rows = allRows(
24812
25936
  this.db.prepare(
24813
25937
  `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
24814
25938
  FROM secret_vault_deref ${where}
24815
- ORDER BY at DESC, rowid DESC LIMIT :limit`
25939
+ ORDER BY at DESC, id DESC LIMIT :limit`
24816
25940
  ),
24817
- { limit }
25941
+ bindParams({
25942
+ limit: limit + 1,
25943
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
25944
+ })
24818
25945
  );
24819
- const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
25946
+ const hasMore = rows.length > limit;
25947
+ const page = hasMore ? rows.slice(0, limit) : rows;
25948
+ const last = page[page.length - 1];
25949
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
24820
25950
  this.db,
24821
25951
  `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
24822
25952
  );
24823
25953
  return {
24824
- rows: rows.map((r) => ({
25954
+ items: page.map((r) => ({
24825
25955
  id: r.id,
24826
25956
  pointerId: r.pointer_id,
24827
25957
  at: new Date(r.at).toISOString(),
@@ -24831,12 +25961,20 @@ var SqliteSecretVaultRepository = class {
24831
25961
  ...r.grant_id === null ? {} : { grantId: r.grant_id },
24832
25962
  pointerCount: r.pointer_count
24833
25963
  })),
25964
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
24834
25965
  hiddenBatched
24835
25966
  };
24836
25967
  }
24837
25968
  countEntries() {
24838
25969
  return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
24839
25970
  }
25971
+ /** Values reused on this machine — the reuse list's page-independent total. */
25972
+ countReused() {
25973
+ return countScalar(
25974
+ this.db,
25975
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
25976
+ );
25977
+ }
24840
25978
  };
24841
25979
 
24842
25980
  // ../packages/persistence/src/repositories/security.ts
@@ -24851,7 +25989,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
24851
25989
  var SCAN_COVERAGE = [
24852
25990
  { provider: "claudecode", coverage: 100, supported: true },
24853
25991
  { provider: "cursor", coverage: 0, supported: false },
24854
- { provider: "codex", coverage: 0, supported: false },
25992
+ { provider: "codex", coverage: 80, supported: true },
25993
+ { provider: "antigravity", coverage: 60, supported: true },
25994
+ { provider: "claudeai", coverage: 0, supported: false },
24855
25995
  { provider: "chatgpt", coverage: 0, supported: false },
24856
25996
  { provider: "copilot", coverage: 0, supported: false },
24857
25997
  { provider: "api", coverage: 0, supported: false }
@@ -25184,7 +26324,7 @@ var SqliteSecurityRepository = class {
25184
26324
  };
25185
26325
 
25186
26326
  // ../packages/persistence/src/repositories/shares.ts
25187
- import { randomUUID as randomUUID8 } from "crypto";
26327
+ import { randomUUID as randomUUID9 } from "crypto";
25188
26328
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
25189
26329
  var IN_CHUNK = 500;
25190
26330
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -25440,7 +26580,7 @@ var SqliteSharesRepository = class {
25440
26580
  (id, destination_id, host, decision, created_at, updated_at)
25441
26581
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
25442
26582
  ).run({
25443
- id: randomUUID8(),
26583
+ id: randomUUID9(),
25444
26584
  destinationId,
25445
26585
  host: dest.host,
25446
26586
  decision,
@@ -25589,7 +26729,7 @@ var SqliteSharesRepository = class {
25589
26729
  let destinationId = destIds.get(hit.host);
25590
26730
  if (destinationId === void 0) {
25591
26731
  destStmt.run({
25592
- id: randomUUID8(),
26732
+ id: randomUUID9(),
25593
26733
  kind: hit.kind,
25594
26734
  name: hit.name,
25595
26735
  host: hit.host,
@@ -25605,7 +26745,7 @@ var SqliteSharesRepository = class {
25605
26745
  let endpointId = endpointIds.get(endpointKey);
25606
26746
  if (endpointId === void 0) {
25607
26747
  endpointStmt.run({
25608
- id: randomUUID8(),
26748
+ id: randomUUID9(),
25609
26749
  destinationId,
25610
26750
  method: hit.method,
25611
26751
  transport: hit.transport,
@@ -25618,7 +26758,7 @@ var SqliteSharesRepository = class {
25618
26758
  endpointIds.set(endpointKey, endpointId);
25619
26759
  }
25620
26760
  siteStmt.run({
25621
- id: randomUUID8(),
26761
+ id: randomUUID9(),
25622
26762
  endpointId,
25623
26763
  project: input.project,
25624
26764
  projectKey: input.projectKey,
@@ -25983,6 +27123,9 @@ function purgeSampleData(db) {
25983
27123
  }
25984
27124
 
25985
27125
  // ../packages/persistence/src/database.ts
27126
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
27127
+ "aka.persistence.unsafeTestOnlyRawHandle"
27128
+ );
25986
27129
  function linkHost(input, hostId) {
25987
27130
  return hostId ? { ...input, hostId } : input;
25988
27131
  }
@@ -26004,21 +27147,34 @@ function openWithPragmas(file2) {
26004
27147
  }
26005
27148
  return db;
26006
27149
  }
26007
- function backupLegacyStore(file2) {
26008
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
26009
- renameSync2(file2, backup);
26010
- tightenFile(backup);
26011
- for (const sidecar of dbSidecars(file2)) {
26012
- if (existsSync2(sidecar)) rmSync2(sidecar);
27150
+ function backupLegacyStore(db, file2) {
27151
+ reapStalePartials(file2);
27152
+ const backup = backupPath(file2, "legacy");
27153
+ let snapshotted = false;
27154
+ let snapshotError;
27155
+ try {
27156
+ snapshotStore(db, backup);
27157
+ snapshotted = true;
27158
+ } catch (error51) {
27159
+ snapshotError = error51;
27160
+ } finally {
27161
+ db.close();
27162
+ }
27163
+ if (!snapshotted) {
27164
+ akaWarn(
27165
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
27166
+ );
27167
+ moveStoreAside(file2, backup);
27168
+ return backup;
26013
27169
  }
27170
+ discardStore(file2, backup);
26014
27171
  return backup;
26015
27172
  }
26016
27173
  function openAndInitialize(file2) {
26017
27174
  let db = openWithPragmas(file2);
26018
27175
  try {
26019
27176
  if (isForeignSqliteLineage(db)) {
26020
- db.close();
26021
- const backup = backupLegacyStore(file2);
27177
+ const backup = backupLegacyStore(db, file2);
26022
27178
  db = openWithPragmas(file2);
26023
27179
  akaWarn(
26024
27180
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -26062,7 +27218,7 @@ function openAndInitialize(file2) {
26062
27218
  }
26063
27219
  function openLocalDatabase(dir) {
26064
27220
  ensureDataDirSync(dir);
26065
- const file2 = join2(dir, DB_FILENAME);
27221
+ const file2 = join3(dir, DB_FILENAME);
26066
27222
  const {
26067
27223
  db,
26068
27224
  events,
@@ -26179,7 +27335,7 @@ function openLocalDatabase(dir) {
26179
27335
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
26180
27336
  if (!definitionId) continue;
26181
27337
  inspectionFindings.insertFinding({
26182
- id: randomUUID9(),
27338
+ id: randomUUID10(),
26183
27339
  auditEventId: record2.scanEvent.id,
26184
27340
  inspectionDefinitionId: definitionId,
26185
27341
  span: finding.span,
@@ -26285,10 +27441,226 @@ function openLocalDatabase(dir) {
26285
27441
  transaction,
26286
27442
  close: () => {
26287
27443
  db.close();
26288
- }
27444
+ },
27445
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
27446
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
26289
27447
  };
26290
27448
  }
26291
27449
 
27450
+ // ../packages/persistence/src/file-lock.ts
27451
+ import { randomUUID as randomUUID11 } from "crypto";
27452
+ import {
27453
+ closeSync,
27454
+ existsSync as existsSync3,
27455
+ openSync,
27456
+ readFileSync as readFileSync2,
27457
+ rmSync as rmSync3,
27458
+ statSync as statSync2,
27459
+ writeFileSync as writeFileSync2
27460
+ } from "fs";
27461
+ import { hostname as hostname3 } from "os";
27462
+ var LOCK_SUFFIX = ".lock";
27463
+ var DEFAULT_TIMEOUT_MS = 5e3;
27464
+ var DEFAULT_STALE_MS = 2e3;
27465
+ var RETRY_INTERVAL_MS = 5;
27466
+ var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
27467
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
27468
+ function sleepSync(ms) {
27469
+ Atomics.wait(PARK, 0, 0, ms);
27470
+ }
27471
+ var FileLockError = class extends Error {
27472
+ reason;
27473
+ file;
27474
+ holderPid;
27475
+ constructor(reason, file2, detail, holderPid, options) {
27476
+ super(
27477
+ `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
27478
+ options
27479
+ );
27480
+ this.name = "FileLockError";
27481
+ this.reason = reason;
27482
+ this.file = file2;
27483
+ this.holderPid = holderPid;
27484
+ }
27485
+ };
27486
+ function lockPathFor(file2) {
27487
+ return `${file2}${LOCK_SUFFIX}`;
27488
+ }
27489
+ function readLockBody(lock) {
27490
+ let raw;
27491
+ try {
27492
+ raw = readFileSync2(lock, "utf8");
27493
+ } catch {
27494
+ return null;
27495
+ }
27496
+ try {
27497
+ const parsed = JSON.parse(raw);
27498
+ const { pid, token, at, host } = parsed;
27499
+ if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
27500
+ return { pid, token, at, host: typeof host === "string" ? host : "" };
27501
+ } catch {
27502
+ return null;
27503
+ }
27504
+ }
27505
+ function holderIsAlive(pid) {
27506
+ if (!Number.isInteger(pid) || pid <= 0) return false;
27507
+ try {
27508
+ process.kill(pid, 0);
27509
+ return true;
27510
+ } catch (err) {
27511
+ return err.code !== "ESRCH";
27512
+ }
27513
+ }
27514
+ function directoryAcceptsCreates(lock) {
27515
+ const probe = `${lock}.probe-${randomUUID11()}`;
27516
+ try {
27517
+ closeSync(openSync(probe, "wx", DATA_FILE_MODE));
27518
+ return true;
27519
+ } catch {
27520
+ return false;
27521
+ } finally {
27522
+ try {
27523
+ rmSync3(probe, { force: true });
27524
+ } catch {
27525
+ }
27526
+ }
27527
+ }
27528
+ function tryAcquire(lock, file2) {
27529
+ const token = randomUUID11();
27530
+ let fd;
27531
+ try {
27532
+ fd = openSync(lock, "wx", DATA_FILE_MODE);
27533
+ } catch (err) {
27534
+ const code = err.code ?? "";
27535
+ if (code === "EEXIST") return null;
27536
+ if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync3(lock) || directoryAcceptsCreates(lock))) {
27537
+ return null;
27538
+ }
27539
+ throw new FileLockError(
27540
+ "unavailable",
27541
+ file2,
27542
+ err instanceof Error ? err.message : String(err),
27543
+ void 0,
27544
+ { cause: err }
27545
+ );
27546
+ }
27547
+ const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
27548
+ try {
27549
+ writeFileSync2(fd, `${JSON.stringify(body)}
27550
+ `);
27551
+ } catch {
27552
+ try {
27553
+ closeSync(fd);
27554
+ } catch {
27555
+ }
27556
+ rmSync3(lock, { force: true });
27557
+ return null;
27558
+ }
27559
+ try {
27560
+ closeSync(fd);
27561
+ } catch {
27562
+ }
27563
+ return token;
27564
+ }
27565
+ function isAbandoned(body, lock, staleMs) {
27566
+ if (!body) {
27567
+ try {
27568
+ return Date.now() - statSync2(lock).mtimeMs >= staleMs;
27569
+ } catch {
27570
+ return false;
27571
+ }
27572
+ }
27573
+ if (Date.now() - body.at < staleMs) return false;
27574
+ if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
27575
+ return Date.now() - body.at >= abandonWindow(staleMs);
27576
+ }
27577
+ function breakIfStale(lock, staleMs) {
27578
+ const breaker = `${lock}.break`;
27579
+ let fd;
27580
+ try {
27581
+ fd = openSync(breaker, "wx", DATA_FILE_MODE);
27582
+ } catch {
27583
+ reapAbandonedBreaker(breaker);
27584
+ return false;
27585
+ }
27586
+ try {
27587
+ closeSync(fd);
27588
+ } catch {
27589
+ }
27590
+ try {
27591
+ if (!existsSync3(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
27592
+ rmSync3(lock, { force: true });
27593
+ return true;
27594
+ } catch {
27595
+ return false;
27596
+ } finally {
27597
+ try {
27598
+ rmSync3(breaker, { force: true });
27599
+ } catch {
27600
+ }
27601
+ }
27602
+ }
27603
+ var BREAKER_ABANDONED_MS = 1e4;
27604
+ function reapAbandonedBreaker(breaker) {
27605
+ try {
27606
+ if (Date.now() - statSync2(breaker).mtimeMs >= BREAKER_ABANDONED_MS) {
27607
+ rmSync3(breaker, { force: true });
27608
+ }
27609
+ } catch {
27610
+ }
27611
+ }
27612
+ function abandonWindow(staleMs) {
27613
+ return Math.max(staleMs * 30, 6e4);
27614
+ }
27615
+ function release(lock, token) {
27616
+ try {
27617
+ if (readLockBody(lock)?.token !== token) return;
27618
+ rmSync3(lock, { force: true });
27619
+ } catch {
27620
+ }
27621
+ }
27622
+ function withFileLock(file2, fn, options = {}) {
27623
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27624
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
27625
+ const lock = lockPathFor(file2);
27626
+ const deadline = Date.now() + timeoutMs;
27627
+ let token = tryAcquire(lock, file2);
27628
+ while (token === null) {
27629
+ if (breakIfStale(lock, staleMs)) {
27630
+ token = tryAcquire(lock, file2);
27631
+ continue;
27632
+ }
27633
+ if (Date.now() >= deadline) {
27634
+ throw new FileLockError(
27635
+ "timeout",
27636
+ file2,
27637
+ `still held after ${String(timeoutMs)}ms`,
27638
+ readLockBody(lock)?.pid
27639
+ );
27640
+ }
27641
+ sleepSync(RETRY_INTERVAL_MS);
27642
+ token = tryAcquire(lock, file2);
27643
+ }
27644
+ try {
27645
+ const result = fn();
27646
+ if (isThenable(result)) {
27647
+ void result.then(
27648
+ () => void 0,
27649
+ () => void 0
27650
+ );
27651
+ throw new TypeError(
27652
+ `withFileLock(${file2}) was given an async body; the lock is released as soon as it returns, so the awaited work would run unguarded. Pass a synchronous function.`
27653
+ );
27654
+ }
27655
+ return result;
27656
+ } finally {
27657
+ release(lock, token);
27658
+ }
27659
+ }
27660
+ function isThenable(value) {
27661
+ return typeof value === "object" && value !== null && typeof value.then === "function";
27662
+ }
27663
+
26292
27664
  // ../packages/persistence/src/finding-key.ts
26293
27665
  import { createHash as createHash3 } from "crypto";
26294
27666
  function normalizeFilePath(filePath) {
@@ -26301,13 +27673,13 @@ function computeFindingKey(input) {
26301
27673
 
26302
27674
  // ../packages/persistence/src/fingerprint.ts
26303
27675
  import { createHmac, randomBytes } from "crypto";
26304
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
26305
- import { join as join3 } from "path";
27676
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
27677
+ import { join as join4 } from "path";
26306
27678
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
26307
- var KEY_FILENAME = "exception.key";
27679
+ var EXCEPTION_KEY_FILENAME = "exception.key";
26308
27680
  var KEY_MATERIAL_BYTES = 32;
26309
27681
  function keyFilePath(dataDir2) {
26310
- return join3(dataDir2, KEY_FILENAME);
27682
+ return join4(dataDir2, EXCEPTION_KEY_FILENAME);
26311
27683
  }
26312
27684
  function parseKeyFile(raw) {
26313
27685
  const parsed = JSON.parse(raw);
@@ -26345,8 +27717,8 @@ var FloorUnreadableError = class extends Error {
26345
27717
  }
26346
27718
  };
26347
27719
  function storedKeyVersionFloor(dataDir2) {
26348
- const file2 = join3(dataDir2, DB_FILENAME);
26349
- if (!existsSync3(file2)) return 0;
27720
+ const file2 = join4(dataDir2, DB_FILENAME);
27721
+ if (!existsSync4(file2)) return 0;
26350
27722
  let db;
26351
27723
  try {
26352
27724
  db = new DatabaseSync2(file2, { readOnly: true });
@@ -26371,18 +27743,42 @@ function storedKeyVersionFloor(dataDir2) {
26371
27743
  db?.close();
26372
27744
  }
26373
27745
  }
27746
+ function serializeKey(key) {
27747
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
27748
+ }
26374
27749
  function writeKeyFile(dataDir2, key) {
26375
27750
  ensureDataDirSync(dataDir2);
26376
- const file2 = keyFilePath(dataDir2);
26377
- const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
26378
- writeOwnerOnlyFileSync(file2, `${body}
27751
+ writeOwnerOnlyFileSync(keyFilePath(dataDir2), `${serializeKey(key)}
26379
27752
  `);
26380
27753
  return key;
26381
27754
  }
27755
+ function createKeyFile(dataDir2, key) {
27756
+ ensureDataDirSync(dataDir2);
27757
+ const file2 = keyFilePath(dataDir2);
27758
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
27759
+ `)) return key;
27760
+ const winner = readFingerprintKey(dataDir2);
27761
+ if (winner) {
27762
+ tightenFile(file2);
27763
+ return winner;
27764
+ }
27765
+ const occupant = classifyOccupant(file2);
27766
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
27767
+ }
27768
+ function occupantMessage(file2, kind) {
27769
+ switch (kind) {
27770
+ case "symlink":
27771
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
27772
+ case "gone":
27773
+ return "exception key file was removed while it was being created";
27774
+ case "unknown":
27775
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
27776
+ }
27777
+ }
26382
27778
  function readFingerprintKey(dataDir2) {
26383
27779
  let raw;
26384
27780
  try {
26385
- raw = readFileSync2(keyFilePath(dataDir2), "utf8");
27781
+ raw = readFileSync3(keyFilePath(dataDir2), "utf8");
26386
27782
  } catch (err) {
26387
27783
  if (err.code === "ENOENT") return null;
26388
27784
  throw err instanceof Error ? err : new Error(String(err));
@@ -26395,7 +27791,7 @@ function loadOrCreateFingerprintKey(dataDir2) {
26395
27791
  tightenFile(keyFilePath(dataDir2));
26396
27792
  return existing;
26397
27793
  }
26398
- return writeKeyFile(dataDir2, {
27794
+ return createKeyFile(dataDir2, {
26399
27795
  version: storedKeyVersionFloor(dataDir2) + 1,
26400
27796
  material: randomBytes(KEY_MATERIAL_BYTES)
26401
27797
  });
@@ -26421,31 +27817,32 @@ function keyStateOf(key) {
26421
27817
  import { renameSync as renameSync3 } from "fs";
26422
27818
  import { mkdir } from "fs/promises";
26423
27819
  import { homedir as homedir3 } from "os";
26424
- import { join as join4 } from "path";
27820
+ import { join as join5 } from "path";
26425
27821
  function defaultDataDir() {
26426
- return join4(homedir3(), ".aka");
27822
+ return join5(homedir3(), ".aka");
26427
27823
  }
26428
27824
  function settingsDir(base = defaultDataDir()) {
26429
- return join4(base, "settings");
27825
+ return join5(base, "settings");
26430
27826
  }
26431
27827
  function dataDir(base = defaultDataDir()) {
26432
- return join4(base, "data");
27828
+ return join5(base, "data");
26433
27829
  }
26434
27830
  function dbPath(base = defaultDataDir()) {
26435
- return join4(dataDir(base), "aka.db");
27831
+ return join5(dataDir(base), "aka.db");
26436
27832
  }
26437
27833
  function keysDir(base = defaultDataDir()) {
26438
- return join4(base, "keys");
27834
+ return join5(base, "keys");
26439
27835
  }
26440
27836
  function ensureLayoutDirSync(dir = defaultDataDir()) {
26441
27837
  ensureDataDirSync(dir);
26442
27838
  }
26443
27839
 
26444
27840
  // ../packages/persistence/src/settings.ts
26445
- import { readFileSync as readFileSync3 } from "fs";
26446
- import { join as join5 } from "path";
27841
+ import { readFileSync as readFileSync4 } from "fs";
27842
+ import { join as join6 } from "path";
27843
+ var SETTINGS_FILENAME = "settings.json";
26447
27844
  function readWorkspaceSettings(base = defaultDataDir()) {
26448
- const record2 = readJson(join5(settingsDir(base), "settings.json"));
27845
+ const record2 = readJson(join6(settingsDir(base), SETTINGS_FILENAME));
26449
27846
  if (!record2) return defaultWorkspaceSettings();
26450
27847
  try {
26451
27848
  return WorkspaceSettings.parse(record2);
@@ -26456,7 +27853,7 @@ function readWorkspaceSettings(base = defaultDataDir()) {
26456
27853
  function readJson(file2) {
26457
27854
  let text;
26458
27855
  try {
26459
- text = readFileSync3(file2, "utf8");
27856
+ text = readFileSync4(file2, "utf8");
26460
27857
  } catch {
26461
27858
  return null;
26462
27859
  }
@@ -26577,13 +27974,18 @@ import { randomBytes as randomBytes2 } from "crypto";
26577
27974
  import {
26578
27975
  chmodSync as chmodSync2,
26579
27976
  mkdirSync as mkdirSync2,
26580
- readFileSync as readFileSync4,
27977
+ readFileSync as readFileSync5,
26581
27978
  renameSync as renameSync4,
26582
- rmSync as rmSync3,
26583
- statSync,
26584
- writeFileSync as writeFileSync2
27979
+ rmSync as rmSync4,
27980
+ statSync as statSync3,
27981
+ writeFileSync as writeFileSync3
26585
27982
  } from "fs";
26586
- import { join as join6 } from "path";
27983
+ import { join as join7 } from "path";
27984
+ var VAULT_OCCUPANT_REASON = {
27985
+ symlink: "the path is a symlink; remove it so a keyring can be created",
27986
+ gone: "the path was occupied but holds no keyring (removed while it was being created)",
27987
+ unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
27988
+ };
26587
27989
  var VaultKeyEpochMissingError = class extends Error {
26588
27990
  version;
26589
27991
  constructor(version2) {
@@ -26676,28 +28078,28 @@ function claimRotationLock(lock, owner) {
26676
28078
  throw asError(err);
26677
28079
  }
26678
28080
  try {
26679
- writeFileSync2(join6(lock, LOCK_OWNER_FILE), `${owner}
28081
+ writeFileSync3(join7(lock, LOCK_OWNER_FILE), `${owner}
26680
28082
  `, { mode: DATA_FILE_MODE });
26681
28083
  return true;
26682
28084
  } catch (err) {
26683
- rmSync3(lock, { recursive: true, force: true });
28085
+ rmSync4(lock, { recursive: true, force: true });
26684
28086
  throw asError(err);
26685
28087
  }
26686
28088
  }
26687
28089
  function acquireRotationLock(keysDir2) {
26688
- const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
28090
+ const lock = join7(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
26689
28091
  const owner = randomBytes2(16).toString("hex");
26690
28092
  if (claimRotationLock(lock, owner)) return { lock, owner };
26691
28093
  let held;
26692
28094
  try {
26693
- held = statSync(lock);
28095
+ held = statSync3(lock);
26694
28096
  } catch {
26695
28097
  throw new Error(ROTATION_IN_PROGRESS);
26696
28098
  }
26697
28099
  if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
26698
28100
  const aside = `${lock}.stale.${owner}`;
26699
28101
  try {
26700
- const now = statSync(lock);
28102
+ const now = statSync3(lock);
26701
28103
  if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
26702
28104
  throw new Error(ROTATION_IN_PROGRESS);
26703
28105
  }
@@ -26706,17 +28108,17 @@ function acquireRotationLock(keysDir2) {
26706
28108
  if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
26707
28109
  throw new Error(ROTATION_IN_PROGRESS, { cause: err });
26708
28110
  }
26709
- rmSync3(aside, { recursive: true, force: true });
28111
+ rmSync4(aside, { recursive: true, force: true });
26710
28112
  if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
26711
28113
  return { lock, owner };
26712
28114
  }
26713
28115
  function releaseRotationLock(lease) {
26714
28116
  try {
26715
- if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
28117
+ if (readFileSync5(join7(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
26716
28118
  } catch {
26717
28119
  return;
26718
28120
  }
26719
- rmSync3(lease.lock, { recursive: true, force: true });
28121
+ rmSync4(lease.lock, { recursive: true, force: true });
26720
28122
  }
26721
28123
  function withRotationLock(keysDir2, work) {
26722
28124
  ensureDataDirSync(keysDir2);
@@ -26733,7 +28135,7 @@ var FileKeyProvider = class {
26733
28135
  this.#keysDir = keysDir2;
26734
28136
  }
26735
28137
  get filePath() {
26736
- return join6(this.#keysDir, VAULT_KEY_FILENAME);
28138
+ return join7(this.#keysDir, VAULT_KEY_FILENAME);
26737
28139
  }
26738
28140
  loadOrCreate() {
26739
28141
  return asAsync(() => {
@@ -26763,7 +28165,7 @@ var FileKeyProvider = class {
26763
28165
  #read() {
26764
28166
  let raw;
26765
28167
  try {
26766
- raw = readFileSync4(this.filePath, "utf8");
28168
+ raw = readFileSync5(this.filePath, "utf8");
26767
28169
  } catch (err) {
26768
28170
  if (err.code === "ENOENT") return null;
26769
28171
  throw err instanceof Error ? err : new Error(String(err));
@@ -26771,34 +28173,32 @@ var FileKeyProvider = class {
26771
28173
  return parseKeyring(raw);
26772
28174
  }
26773
28175
  /**
26774
- * First mint: the keyring is created at its FINAL path with a
26775
- * creation-exclusive write, so two processes racing a fresh machine cannot
26776
- * each mint a different epoch 1 — with tmp + rename the loser's replace
26777
- * would orphan everything the winner had already sealed. On EEXIST the
26778
- * loser re-reads and adopts the winner's keyring; it minted nothing.
26779
- * Atomic replace is unnecessary here: nothing can be mid-read of a file
26780
- * that did not exist, and a torn exclusive write parses as corrupt on the
26781
- * next read and fails secure rather than being re-minted over.
28176
+ * First mint: the keyring is CREATED, never replaced, so two processes racing
28177
+ * a fresh machine cannot each mint a different epoch 1 — with tmp + rename
28178
+ * the loser's replace would orphan everything the winner had already sealed.
28179
+ * The loser re-reads and adopts the winner's keyring; it minted nothing.
28180
+ *
28181
+ * `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
28182
+ * at the final path, so the keyring never exists at zero length: a reader —
28183
+ * including the loser, re-reading in order to adopt — sees the file absent or
28184
+ * whole, and never mistakes a live keyring for a corrupt one. A corrupt file
28185
+ * still throws from the parse and is never re-minted over.
26782
28186
  */
26783
28187
  #createExclusive() {
26784
28188
  ensureDataDirSync(this.#keysDir);
26785
28189
  const keyring = mintKeyring();
26786
- try {
26787
- writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
26788
- `, {
26789
- flag: "wx",
26790
- mode: DATA_FILE_MODE
26791
- });
26792
- } catch (err) {
26793
- if (err.code !== "EEXIST") throw asError(err);
26794
- const winner = this.#read();
26795
- if (!winner) {
26796
- throw new Error("vault: key file vanished during first mint", { cause: err });
26797
- }
26798
- return winner;
28190
+ if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
28191
+ `)) return keyring;
28192
+ const winner = this.#read();
28193
+ if (!winner) {
28194
+ const occupant = classifyOccupant(this.filePath);
28195
+ throw new KeyUnclaimableError(
28196
+ `vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
28197
+ occupant.cause
28198
+ );
26799
28199
  }
26800
28200
  tightenFileMode(this.filePath);
26801
- return keyring;
28201
+ return winner;
26802
28202
  }
26803
28203
  /**
26804
28204
  * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
@@ -26809,7 +28209,7 @@ var FileKeyProvider = class {
26809
28209
  ensureDataDirSync(this.#keysDir);
26810
28210
  const file2 = this.filePath;
26811
28211
  const tmp = `${file2}.tmp`;
26812
- writeFileSync2(tmp, `${serializeKeyring(keyring)}
28212
+ writeFileSync3(tmp, `${serializeKeyring(keyring)}
26813
28213
  `, { mode: DATA_FILE_MODE });
26814
28214
  renameSync4(tmp, file2);
26815
28215
  tightenFileMode(file2);
@@ -26935,7 +28335,7 @@ function createKeyProvider(custody, keysDir2) {
26935
28335
  }
26936
28336
 
26937
28337
  // ../packages/persistence/src/vault/vault.ts
26938
- import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
28338
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
26939
28339
  var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
26940
28340
  var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
26941
28341
  var VAULT_PURGE_POINTER_ID = "*";
@@ -26962,14 +28362,12 @@ function parsePointer(token) {
26962
28362
  var SecretVault = class {
26963
28363
  #repo;
26964
28364
  #keys;
26965
- #fingerprintKey;
26966
28365
  #isConsented;
26967
28366
  #verifyGrant;
26968
28367
  #now;
26969
28368
  constructor(deps) {
26970
28369
  this.#repo = deps.repo;
26971
28370
  this.#keys = deps.keys;
26972
- this.#fingerprintKey = deps.fingerprintKey;
26973
28371
  this.#isConsented = deps.isConsented;
26974
28372
  this.#verifyGrant = deps.verifyGrant;
26975
28373
  this.#now = deps.now ?? (() => Date.now());
@@ -26978,10 +28376,23 @@ var SecretVault = class {
26978
28376
  * Store a value and return the pointer that stands for it. The same value
26979
28377
  * always yields the same pointer on this machine — one row, one pointer id,
26980
28378
  * one category — which is what makes dedup and reuse counting work.
28379
+ *
28380
+ * `fingerprintKey` is the exception-key epoch this value's fingerprint is
28381
+ * derived under — a different key from the vault's, with different rotation
28382
+ * semantics. It is a parameter of the WRITE rather than a constructor dep,
28383
+ * and a thunk rather than a value, so that the only way to reach a key is to
28384
+ * store something: a read-only caller never names it, and a caller whose
28385
+ * source mints on absence mints only once consent has actually opened the
28386
+ * write. `refreshFingerprints` takes its key the same way, for the same
28387
+ * reason.
28388
+ *
28389
+ * Resolved once per call, so the fingerprint and the version it is recorded
28390
+ * under can never come from two different epochs.
26981
28391
  */
26982
- async tokenize(raw, meta3) {
28392
+ async tokenize(raw, meta3, fingerprintKey) {
26983
28393
  if (!this.#isConsented()) return CONSENT_ABSENT;
26984
- const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
28394
+ const fpKey = fingerprintKey();
28395
+ const valueFingerprint = fingerprintValue(fpKey, raw);
26985
28396
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
26986
28397
  const now = this.#now();
26987
28398
  if (existing) {
@@ -26997,7 +28408,7 @@ var SecretVault = class {
26997
28408
  {
26998
28409
  pointerId: base32Encode(pointerId),
26999
28410
  valueFingerprint,
27000
- fingerprintKeyVersion: this.#fingerprintKey.version,
28411
+ fingerprintKeyVersion: fpKey.version,
27001
28412
  keyVersion: version2,
27002
28413
  // Recorded so the row stays OPENABLE if the wire-format constant ever
27003
28414
  // moves: it is part of this row's AEAD AAD. It is not a tag input —
@@ -27241,7 +28652,7 @@ var SecretVault = class {
27241
28652
  purgeVault() {
27242
28653
  const destroyed = this.#repo.purgeAll();
27243
28654
  this.#repo.recordDeref({
27244
- id: randomUUID10(),
28655
+ id: randomUUID12(),
27245
28656
  pointerId: VAULT_PURGE_POINTER_ID,
27246
28657
  at: this.#now(),
27247
28658
  target: "human",
@@ -27310,7 +28721,7 @@ var SecretVault = class {
27310
28721
  }
27311
28722
  #audit(pointerId, opts, outcome) {
27312
28723
  this.#repo.recordDeref({
27313
- id: randomUUID10(),
28724
+ id: randomUUID12(),
27314
28725
  pointerId,
27315
28726
  at: this.#now(),
27316
28727
  target: opts.target,
@@ -27325,12 +28736,12 @@ var SecretVault = class {
27325
28736
  };
27326
28737
 
27327
28738
  // ../packages/persistence/src/warn-era-cap.ts
27328
- import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
27329
- import { join as join7 } from "path";
28739
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
28740
+ import { join as join8 } from "path";
27330
28741
 
27331
28742
  // ../packages/plugin-sdk/src/config.ts
27332
- import { existsSync as existsSync5 } from "fs";
27333
- import { join as join8 } from "path";
28743
+ import { existsSync as existsSync6 } from "fs";
28744
+ import { join as join9 } from "path";
27334
28745
 
27335
28746
  // ../packages/plugin-sdk/src/provider-env.ts
27336
28747
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -27351,9 +28762,9 @@ var providerEnvShape = {
27351
28762
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
27352
28763
 
27353
28764
  // ../packages/plugin-sdk/src/config-inventory.ts
27354
- import { readdirSync, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
28765
+ import { readdirSync as readdirSync3, readFileSync as readFileSync7, realpathSync, statSync as statSync5 } from "fs";
27355
28766
  import { homedir as homedir4 } from "os";
27356
- import { basename as basename2, join as join10 } from "path";
28767
+ import { basename as basename3, join as join11 } from "path";
27357
28768
 
27358
28769
  // ../rules/code-flaws/auth-jwt-no-verify.json
27359
28770
  var auth_jwt_no_verify_default = {
@@ -29009,7 +30420,7 @@ var gcp_service_account_default = {
29009
30420
  severity: "critical",
29010
30421
  matcher: {
29011
30422
  type: "regex",
29012
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
30423
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
29013
30424
  flags: "g"
29014
30425
  },
29015
30426
  examples: [
@@ -29388,8 +30799,8 @@ function bundledDetections() {
29388
30799
  }
29389
30800
 
29390
30801
  // ../packages/plugin-sdk/src/repo.ts
29391
- import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
29392
- import { basename, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
30802
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
30803
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join10, sep as sep2 } from "path";
29393
30804
  function resolveRepoIdentity(cwd) {
29394
30805
  try {
29395
30806
  const root = findGitRoot(cwd);
@@ -29402,7 +30813,7 @@ function resolveRepoIdentity(cwd) {
29402
30813
  // win32) so the persistence layer's `/`-separated checkout-path patterns
29403
30814
  // (the ghost sweep + the read-side worktree filter) match it as written.
29404
30815
  url: url2 ?? headRoot.split(sep2).join("/"),
29405
- name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
30816
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
29406
30817
  };
29407
30818
  } catch {
29408
30819
  return void 0;
@@ -29427,36 +30838,36 @@ function resolveHeadRoot(cwd) {
29427
30838
  function findGitRoot(start) {
29428
30839
  let dir = start;
29429
30840
  for (; ; ) {
29430
- if (existsSync6(join9(dir, ".git"))) return dir;
29431
- const parent = dirname2(dir);
30841
+ if (existsSync7(join10(dir, ".git"))) return dir;
30842
+ const parent = dirname3(dir);
29432
30843
  if (parent === dir) return void 0;
29433
30844
  dir = parent;
29434
30845
  }
29435
30846
  }
29436
30847
  function resolveGitContext(root) {
29437
- const dotGit = join9(root, ".git");
30848
+ const dotGit = join10(root, ".git");
29438
30849
  try {
29439
- if (statSync2(dotGit).isDirectory()) {
29440
- return { configPath: join9(dotGit, "config"), headRoot: root };
30850
+ if (statSync4(dotGit).isDirectory()) {
30851
+ return { configPath: join10(dotGit, "config"), headRoot: root };
29441
30852
  }
29442
30853
  } catch {
29443
30854
  return void 0;
29444
30855
  }
29445
30856
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
29446
30857
  if (!target) return void 0;
29447
- const gitdir = isAbsolute(target) ? target : join9(root, target);
29448
- if (existsSync6(join9(gitdir, "config"))) {
29449
- return { configPath: join9(gitdir, "config"), headRoot: root };
30858
+ const gitdir = isAbsolute(target) ? target : join10(root, target);
30859
+ if (existsSync7(join10(gitdir, "config"))) {
30860
+ return { configPath: join10(gitdir, "config"), headRoot: root };
29450
30861
  }
29451
- const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
30862
+ const commonRaw = safeRead(join10(gitdir, "commondir"))?.trim();
29452
30863
  if (!commonRaw) return void 0;
29453
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
29454
- const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
29455
- return { configPath: join9(commonGitDir, "config"), headRoot };
30864
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join10(gitdir, commonRaw);
30865
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
30866
+ return { configPath: join10(commonGitDir, "config"), headRoot };
29456
30867
  }
29457
30868
  function safeRead(path) {
29458
30869
  try {
29459
- return readFileSync5(path, "utf8");
30870
+ return readFileSync6(path, "utf8");
29460
30871
  } catch {
29461
30872
  return void 0;
29462
30873
  }
@@ -29494,18 +30905,23 @@ function slugFromUrl(url2) {
29494
30905
  }
29495
30906
 
29496
30907
  // ../packages/plugin-sdk/src/events.ts
29497
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
30908
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
30909
+
30910
+ // ../packages/plugin-sdk/src/isolated-scan.ts
30911
+ import { existsSync as existsSync8 } from "fs";
30912
+ import { fileURLToPath as fileURLToPath2 } from "url";
30913
+ import { Worker } from "worker_threads";
29498
30914
 
29499
30915
  // ../packages/plugin-sdk/src/inventory-resolver.ts
29500
- import { arch, hostname as hostname3, platform, release } from "os";
30916
+ import { arch, hostname as hostname4, platform, release as release2 } from "os";
29501
30917
 
29502
30918
  // ../packages/plugin-sdk/src/nudge.ts
29503
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
29504
- import { join as join11 } from "path";
30919
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
30920
+ import { join as join12 } from "path";
29505
30921
 
29506
30922
  // ../packages/plugin-sdk/src/paths.ts
29507
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
29508
- import { basename as basename3, dirname as dirname3, sep as sep3 } from "path";
30923
+ import { readdirSync as readdirSync4, realpathSync as realpathSync2 } from "fs";
30924
+ import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
29509
30925
  function toPosix(path) {
29510
30926
  return path.split(sep3).join("/");
29511
30927
  }
@@ -29515,7 +30931,7 @@ function findProjectRoot(startDir, recognizeMarker) {
29515
30931
  let root = null;
29516
30932
  for (let level = 0; level < MAX_PROJECT_ROOT_LEVELS; level += 1) {
29517
30933
  if (directoryHasMarker(dir, recognizeMarker)) root = dir;
29518
- const parent = dirname3(dir);
30934
+ const parent = dirname4(dir);
29519
30935
  if (parent === dir) break;
29520
30936
  dir = parent;
29521
30937
  }
@@ -29523,7 +30939,7 @@ function findProjectRoot(startDir, recognizeMarker) {
29523
30939
  }
29524
30940
  function directoryHasMarker(dir, recognizeMarker) {
29525
30941
  try {
29526
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
30942
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29527
30943
  if (entry.isFile() && recognizeMarker(entry.name) != null) return true;
29528
30944
  }
29529
30945
  } catch {
@@ -29534,13 +30950,13 @@ function directoryHasMarker(dir, recognizeMarker) {
29534
30950
  function resolveNonGitProject(startDir, recognizeMarker) {
29535
30951
  const projectRoot = findProjectRoot(startDir, recognizeMarker);
29536
30952
  const realRoot = realpathSync2(projectRoot);
29537
- return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename3(realRoot) };
30953
+ return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename4(realRoot) };
29538
30954
  }
29539
30955
 
29540
30956
  // ../packages/plugin-sdk/src/project-files.ts
29541
30957
  var import_ignore = __toESM(require_ignore(), 1);
29542
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
29543
- import { basename as basename4, join as join12, relative, sep as sep4 } from "path";
30958
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
30959
+ import { basename as basename5, join as join13, relative, sep as sep4 } from "path";
29544
30960
  var SKIP_DIRS = /* @__PURE__ */ new Set([
29545
30961
  ".git",
29546
30962
  "node_modules",
@@ -29560,7 +30976,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
29560
30976
  var MAX_FILES = 2e4;
29561
30977
  function readIgnoreLayer(dir) {
29562
30978
  try {
29563
- const content = readFileSync8(join12(dir, ".gitignore"), "utf8");
30979
+ const content = readFileSync9(join13(dir, ".gitignore"), "utf8");
29564
30980
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
29565
30981
  } catch {
29566
30982
  return void 0;
@@ -29630,7 +31046,7 @@ function resolveProjectFiles(cwd) {
29630
31046
  let visit3 = function(dir, layers) {
29631
31047
  let dirents;
29632
31048
  try {
29633
- dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
31049
+ dirents = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
29634
31050
  } catch {
29635
31051
  walk.lostSubtree = true;
29636
31052
  return false;
@@ -29638,10 +31054,10 @@ function resolveProjectFiles(cwd) {
29638
31054
  const layer = readIgnoreLayer(dir);
29639
31055
  const dirLayers = layer ? [...layers, layer] : layers;
29640
31056
  for (const entry of dirents) {
29641
- const fullPath = join12(dir, entry.name);
31057
+ const fullPath = join13(dir, entry.name);
29642
31058
  if (entry.isDirectory()) {
29643
31059
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
29644
- if (existsSync7(join12(fullPath, ".git"))) continue;
31060
+ if (existsSync9(join13(fullPath, ".git"))) continue;
29645
31061
  if (visit3(fullPath, dirLayers)) return true;
29646
31062
  continue;
29647
31063
  }
@@ -29652,7 +31068,7 @@ function resolveProjectFiles(cwd) {
29652
31068
  const relPath = relative(root, fullPath).split(sep4).join("/");
29653
31069
  files.push({
29654
31070
  path: relPath,
29655
- name: basename4(entry.name),
31071
+ name: basename5(entry.name),
29656
31072
  origin: classifyOrigin(relPath, entry.name),
29657
31073
  defaultAccess: "approved"
29658
31074
  });
@@ -29675,22 +31091,48 @@ function resolveProjectFiles(cwd) {
29675
31091
  }
29676
31092
  }
29677
31093
 
31094
+ // ../packages/plugin-sdk/src/provider-env-antigravity.ts
31095
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
31096
+ if (typeof v === "string" && v.trim() === "") return void 0;
31097
+ return v;
31098
+ }, external_exports.string().optional()).catch(void 0);
31099
+ var optionalFlag = external_exports.preprocess((v) => {
31100
+ if (typeof v !== "string") return false;
31101
+ const normalized = v.trim().toLowerCase();
31102
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
31103
+ }, external_exports.boolean()).catch(false);
31104
+ var antigravityProviderEnvShape = {
31105
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
31106
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
31107
+ };
31108
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
31109
+
31110
+ // ../packages/plugin-sdk/src/provider-env-codex.ts
31111
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
31112
+ if (typeof v === "string" && v.trim() === "") return void 0;
31113
+ return v;
31114
+ }, external_exports.string().optional()).catch(void 0);
31115
+ var codexProviderEnvShape = {
31116
+ OPENAI_BASE_URL: optionalBaseUrl3
31117
+ };
31118
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
31119
+
29678
31120
  // ../packages/plugin-sdk/src/runtime.ts
29679
- import { randomUUID as randomUUID12 } from "crypto";
31121
+ import { randomUUID as randomUUID14 } from "crypto";
29680
31122
 
29681
31123
  // ../packages/plugin-sdk/src/suppressions.ts
29682
31124
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
29683
31125
 
29684
31126
  // ../packages/plugin-sdk/src/throttle.ts
29685
- import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
29686
- import { join as join13 } from "path";
31127
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
31128
+ import { join as join14 } from "path";
29687
31129
 
29688
31130
  // ../packages/local-ops/src/egress-record.ts
29689
31131
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29690
31132
  try {
29691
31133
  if (!readWorkspaceSettings(base).dataSharesInPlace) return null;
29692
31134
  const abs = resolve(target);
29693
- const targetDir = statSync5(abs).isDirectory() ? abs : dirname4(abs);
31135
+ const targetDir = statSync7(abs).isDirectory() ? abs : dirname5(abs);
29694
31136
  const identity = resolveRepoIdentity(abs);
29695
31137
  const worktreeRoot = resolveWorktreeRoot(abs);
29696
31138
  let root;
@@ -29731,9 +31173,9 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29731
31173
  }
29732
31174
 
29733
31175
  // ../packages/local-ops/src/fs-scan.ts
29734
- import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
29735
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29736
- import { basename as basename5, extname, join as join14, relative as relative3, resolve as resolve2 } from "path";
31176
+ import { createHash as createHash5, randomUUID as randomUUID15 } from "crypto";
31177
+ import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync8 } from "fs";
31178
+ import { basename as basename6, extname, join as join15, relative as relative3, resolve as resolve2 } from "path";
29737
31179
  var import_ignore2 = __toESM(require_ignore(), 1);
29738
31180
  var AKAIGNORE_FILENAME = ".akaignore";
29739
31181
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
@@ -29749,7 +31191,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
29749
31191
  var MAX_BYTES = 1e6;
29750
31192
  function readIgnoreLayer2(dir, filename) {
29751
31193
  try {
29752
- const content = readFileSync9(join14(dir, filename), "utf8");
31194
+ const content = readFileSync10(join15(dir, filename), "utf8");
29753
31195
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
29754
31196
  } catch {
29755
31197
  return void 0;
@@ -29768,7 +31210,7 @@ function evaluate(layers, absPath, isDir) {
29768
31210
  function* collectFiles(target) {
29769
31211
  let st;
29770
31212
  try {
29771
- st = statSync6(target);
31213
+ st = statSync8(target);
29772
31214
  } catch {
29773
31215
  return;
29774
31216
  }
@@ -29784,8 +31226,8 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29784
31226
  const dirMarkLayers = markLayer ? [...markLayers, markLayer] : markLayers;
29785
31227
  const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
29786
31228
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
29787
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29788
- const path = join14(dir, entry.name);
31229
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
31230
+ const path = join15(dir, entry.name);
29789
31231
  if (entry.isDirectory()) {
29790
31232
  const skipState = evaluate(dirSkipLayers, path, true);
29791
31233
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
@@ -29797,7 +31239,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29797
31239
  if (evaluate(dirSkipLayers, path, false) === "ignored") continue;
29798
31240
  let size;
29799
31241
  try {
29800
- size = statSync6(path).size;
31242
+ size = statSync8(path).size;
29801
31243
  } catch {
29802
31244
  continue;
29803
31245
  }
@@ -29811,7 +31253,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29811
31253
  }
29812
31254
  function extractFileEgress(file2, text) {
29813
31255
  if (text.includes("\0")) return null;
29814
- const name = basename5(file2);
31256
+ const name = basename6(file2);
29815
31257
  if (LOCKFILE_BASENAMES.has(name)) return null;
29816
31258
  const kind = manifestKindOf(name);
29817
31259
  const sdkHits = kind === null ? [] : extractManifestSdks(text, kind);
@@ -29819,7 +31261,8 @@ function extractFileEgress(file2, text) {
29819
31261
  if (endpoints.length === 0 && sdkHits.length === 0) return null;
29820
31262
  return { file: file2, vendored: isVendoredPath(toPosix(file2)), endpoints, sdkHits };
29821
31263
  }
29822
- function scanPathIntoStore(db, target, opts = {}) {
31264
+ async function scanPathIntoStore(db, target, opts = {}) {
31265
+ const matchText = opts.scanText ?? ((text) => Promise.resolve(scan(text, opts.rules)));
29823
31266
  let scanned = 0;
29824
31267
  let findingCount = 0;
29825
31268
  const files = [];
@@ -29838,7 +31281,7 @@ function scanPathIntoStore(db, target, opts = {}) {
29838
31281
  for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
29839
31282
  let text;
29840
31283
  try {
29841
- text = readFileSync9(file2, "utf8");
31284
+ text = readFileSync10(file2, "utf8");
29842
31285
  } catch {
29843
31286
  continue;
29844
31287
  }
@@ -29846,9 +31289,9 @@ function scanPathIntoStore(db, target, opts = {}) {
29846
31289
  const fileEgress = extractFileEgress(file2, text);
29847
31290
  if (fileEgress) egressFiles.push(fileEgress);
29848
31291
  const shielded = shieldPointers(text);
29849
- const matches = dropShieldedFindings(scan(shielded.text, opts.rules), shielded.spans);
31292
+ const matches = dropShieldedFindings(await matchText(shielded.text), shielded.spans);
29850
31293
  if (matches.length === 0) continue;
29851
- const eventId = randomUUID13();
31294
+ const eventId = randomUUID15();
29852
31295
  const metadata = { filePath: file2 };
29853
31296
  if (gitignored) metadata.gitignored = true;
29854
31297
  const event = {
@@ -29866,7 +31309,7 @@ function scanPathIntoStore(db, target, opts = {}) {
29866
31309
  const key = resolveFingerprintKey();
29867
31310
  const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
29868
31311
  return {
29869
- id: randomUUID13(),
31312
+ id: randomUUID15(),
29870
31313
  eventId,
29871
31314
  ruleId: m.ruleId,
29872
31315
  category: m.category,
@@ -29891,12 +31334,12 @@ function scanPathIntoStore(db, target, opts = {}) {
29891
31334
  }
29892
31335
 
29893
31336
  // ../packages/local-ops/src/project-inventory.ts
29894
- import { statSync as statSync7 } from "fs";
31337
+ import { statSync as statSync9 } from "fs";
29895
31338
  import { resolve as resolve3 } from "path";
29896
31339
  function recordProjectInventory(db, target) {
29897
31340
  try {
29898
31341
  const abs = resolve3(target);
29899
- statSync7(abs);
31342
+ statSync9(abs);
29900
31343
  const identity = resolveRepoIdentity(abs);
29901
31344
  if (!identity) return null;
29902
31345
  const sourceProjectId2 = db.sourceProject.upsert({
@@ -29945,17 +31388,17 @@ function reinvokeArgv(subcommand, extraArgs = []) {
29945
31388
 
29946
31389
  // ../packages/local-ops/src/update-cache.ts
29947
31390
  import { spawn } from "child_process";
29948
- import { existsSync as existsSync8, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
29949
- import { join as join15 } from "path";
31391
+ import { existsSync as existsSync10, readFileSync as readFileSync11, rmSync as rmSync5 } from "fs";
31392
+ import { join as join16 } from "path";
29950
31393
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
29951
31394
  function cachePath(home) {
29952
- return join15(dataDir(home), "update-check.json");
31395
+ return join16(dataDir(home), "update-check.json");
29953
31396
  }
29954
31397
  function readCache(home) {
29955
31398
  const path = cachePath(home);
29956
- if (!existsSync8(path)) return null;
31399
+ if (!existsSync10(path)) return null;
29957
31400
  try {
29958
- const raw = JSON.parse(readFileSync10(path, "utf8"));
31401
+ const raw = JSON.parse(readFileSync11(path, "utf8"));
29959
31402
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
29960
31403
  return raw;
29961
31404
  } catch {
@@ -29963,9 +31406,9 @@ function readCache(home) {
29963
31406
  }
29964
31407
  }
29965
31408
  function writeCache(home, cache) {
29966
- if (!existsSync8(dataDir(home))) return;
31409
+ if (!existsSync10(dataDir(home))) return;
29967
31410
  try {
29968
- writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
31411
+ writeOwnerOnlyFileSync(cachePath(home), `${JSON.stringify(cache, null, 2)}
29969
31412
  `);
29970
31413
  } catch {
29971
31414
  }
@@ -29975,7 +31418,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
29975
31418
  }
29976
31419
  function clearCache(home) {
29977
31420
  try {
29978
- rmSync4(cachePath(home), { force: true });
31421
+ rmSync5(cachePath(home), { force: true });
29979
31422
  } catch {
29980
31423
  }
29981
31424
  }
@@ -30082,9 +31525,9 @@ var COMMAND_SPECS = [
30082
31525
  { name: "stats", summary: "Print findings / enforcement / detections from the local store" },
30083
31526
  {
30084
31527
  name: "detections",
30085
- argHint: "[update]",
31528
+ argHint: "[update|unquarantine]",
30086
31529
  summary: "List installed detection packs + available updates (updates are manual)",
30087
- args: ["update"]
31530
+ args: ["update", "unquarantine"]
30088
31531
  },
30089
31532
  {
30090
31533
  name: "plugins",
@@ -30092,6 +31535,11 @@ var COMMAND_SPECS = [
30092
31535
  args: ["list", "install"]
30093
31536
  },
30094
31537
  { name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
31538
+ {
31539
+ name: "extension",
31540
+ summary: "Install/check the native-messaging host for the browser extension (Chrome)",
31541
+ args: ["install", "status"]
31542
+ },
30095
31543
  {
30096
31544
  name: "exception",
30097
31545
  summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
@@ -30134,13 +31582,14 @@ function commandsHelp() {
30134
31582
  }
30135
31583
 
30136
31584
  // src/commands/check-updates.ts
30137
- import { existsSync as existsSync9 } from "fs";
31585
+ import { existsSync as existsSync11 } from "fs";
30138
31586
  import { parseArgs } from "util";
30139
31587
 
30140
31588
  // src/lib/args.ts
31589
+ import { resolve as resolve4 } from "path";
30141
31590
  var HOME_OPTION = { home: { type: "string" } };
30142
31591
  function homeBase(home) {
30143
- return home ?? defaultDataDir();
31592
+ return home === void 0 ? defaultDataDir() : resolve4(home);
30144
31593
  }
30145
31594
 
30146
31595
  // src/commands/check-updates.ts
@@ -30165,7 +31614,7 @@ function runCheckUpdates(argv) {
30165
31614
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
30166
31615
  `);
30167
31616
  }
30168
- if (existsSync9(dataDir(home))) {
31617
+ if (existsSync11(dataDir(home))) {
30169
31618
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
30170
31619
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
30171
31620
  }
@@ -30319,11 +31768,11 @@ function runCompletion(argv) {
30319
31768
 
30320
31769
  // src/commands/dashboard.ts
30321
31770
  import { spawn as spawn3 } from "child_process";
30322
- import { existsSync as existsSync10 } from "fs";
31771
+ import { existsSync as existsSync12 } from "fs";
30323
31772
  import { createRequire as createRequire2 } from "module";
30324
31773
  import { createServer } from "net";
30325
- import { dirname as dirname5, join as join16 } from "path";
30326
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
31774
+ import { dirname as dirname6, join as join17 } from "path";
31775
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
30327
31776
  import { parseArgs as parseArgs2 } from "util";
30328
31777
 
30329
31778
  // src/lib/open-url.ts
@@ -30340,20 +31789,20 @@ function openUrl(url2) {
30340
31789
 
30341
31790
  // src/commands/dashboard.ts
30342
31791
  var require3 = createRequire2(import.meta.url);
30343
- var here = dirname5(fileURLToPath2(import.meta.url));
31792
+ var here = dirname6(fileURLToPath3(import.meta.url));
30344
31793
  var BIND_HOST = "127.0.0.1";
30345
31794
  function pkgBase() {
30346
- return isSea() ? dirname5(process.execPath) : join16(here, "..");
31795
+ return isSea() ? dirname6(process.execPath) : join17(here, "..");
30347
31796
  }
30348
31797
  function isPortFree(port) {
30349
- return new Promise((resolve4) => {
31798
+ return new Promise((resolve6) => {
30350
31799
  const probe = createServer();
30351
31800
  probe.once("error", () => {
30352
- resolve4(false);
31801
+ resolve6(false);
30353
31802
  });
30354
31803
  probe.once("listening", () => {
30355
31804
  probe.close(() => {
30356
- resolve4(true);
31805
+ resolve6(true);
30357
31806
  });
30358
31807
  });
30359
31808
  probe.listen(port, BIND_HOST);
@@ -30397,14 +31846,14 @@ async function runDashboard(argv) {
30397
31846
  refreshUpdateMirror(dataDir());
30398
31847
  const base = pkgBase();
30399
31848
  const bundledServer = [
30400
- join16(base, "web-ui", "server.js"),
30401
- join16(base, "web-ui", "web-ui", "server.js")
30402
- ].find((p) => existsSync10(p));
31849
+ join17(base, "web-ui", "server.js"),
31850
+ join17(base, "web-ui", "web-ui", "server.js")
31851
+ ].find((p) => existsSync12(p));
30403
31852
  if (bundledServer) {
30404
31853
  launchStandalone(bundledServer, port, shouldOpen, url2);
30405
31854
  return;
30406
31855
  }
30407
- const webUiDir = dirname5(require3.resolve("@akasecurity/web-ui/package.json"));
31856
+ const webUiDir = dirname6(require3.resolve("@akasecurity/web-ui/package.json"));
30408
31857
  const nextBin = require3.resolve("next/dist/bin/next", { paths: [webUiDir] });
30409
31858
  const startNext = () => {
30410
31859
  const child = spawn3(
@@ -30418,7 +31867,7 @@ async function runDashboard(argv) {
30418
31867
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
30419
31868
  `);
30420
31869
  };
30421
- if (existsSync10(join16(webUiDir, ".next", "BUILD_ID"))) {
31870
+ if (existsSync12(join17(webUiDir, ".next", "BUILD_ID"))) {
30422
31871
  startNext();
30423
31872
  return;
30424
31873
  }
@@ -30441,7 +31890,7 @@ function launchStandalone(serverJs, port, shouldOpen, url2) {
30441
31890
  return;
30442
31891
  }
30443
31892
  const child = spawn3(reinvoke.command, reinvoke.args, {
30444
- cwd: dirname5(serverJs),
31893
+ cwd: dirname6(serverJs),
30445
31894
  stdio: ["inherit", "pipe", "inherit"],
30446
31895
  // Inherit the parent env so the child server sees PATH etc.; PORT/HOSTNAME tell
30447
31896
  // Next's standalone server where to bind (its only port input).
@@ -30484,16 +31933,18 @@ function onReadyOpen(child, shouldOpen, url2, banner) {
30484
31933
 
30485
31934
  // src/commands/detections.ts
30486
31935
  import { parseArgs as parseArgs3 } from "util";
31936
+ var SUBCOMMANDS = ["update", "unquarantine"];
30487
31937
  async function runDetections(argv) {
30488
31938
  const { values, positionals } = parseArgs3({
30489
31939
  args: argv,
30490
31940
  options: { ...HOME_OPTION, all: { type: "boolean" } },
30491
31941
  allowPositionals: true
30492
31942
  });
30493
- const sub = positionals[0] === "update" ? "update" : "list";
31943
+ const named = SUBCOMMANDS.find((s) => s === positionals[0]);
31944
+ const sub = named ?? "list";
30494
31945
  if (sub === "list" && positionals.length > 0) {
30495
31946
  process.stderr.write(
30496
- `aka detections: unknown subcommand '${positionals[0] ?? ""}' (did you mean \`aka detections update\`?)
31947
+ `aka detections: unknown subcommand '${positionals[0] ?? ""}' (expected one of: ${SUBCOMMANDS.join(", ")})
30497
31948
  `
30498
31949
  );
30499
31950
  process.exitCode = 1;
@@ -30505,6 +31956,8 @@ async function runDetections(argv) {
30505
31956
  db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
30506
31957
  if (sub === "update") {
30507
31958
  await runUpdateSub(db, positionals.slice(1), values.all === true);
31959
+ } else if (sub === "unquarantine") {
31960
+ runUnquarantineSub(db);
30508
31961
  } else {
30509
31962
  await runListSub(db);
30510
31963
  }
@@ -30527,6 +31980,15 @@ async function runListSub(db) {
30527
31980
  ${String(items.length)} pack(s) \xB7 ${String(items.reduce((n, i) => n + i.ruleCount, 0))} rule(s) \xB7 ${String(active)} enabled
30528
31981
  `
30529
31982
  );
31983
+ const quarantined = db.ruleProbeCache.countQuarantined();
31984
+ if (quarantined > 0) {
31985
+ out.write(
31986
+ `
31987
+ \u26A0 ${String(quarantined)} rule(s) quarantined for exceeding the ReDoS timing budget \u2014 they are excluded from every scan.
31988
+ aka detections unquarantine # forget the verdicts and measure them again
31989
+ `
31990
+ );
31991
+ }
30530
31992
  if (counts.updates > 0) {
30531
31993
  out.write(
30532
31994
  `
@@ -30539,6 +32001,20 @@ ${String(items.length)} pack(s) \xB7 ${String(items.reduce((n, i) => n + i.ruleC
30539
32001
  out.write("\n\u2713 All detection packs are up to date with this CLI.\n");
30540
32002
  }
30541
32003
  }
32004
+ function runUnquarantineSub(db) {
32005
+ const { refused, cleared } = db.ruleProbeCache.clearQuarantined();
32006
+ if (refused) {
32007
+ process.stderr.write(
32008
+ "aka detections unquarantine: the store refused the write (another process may be holding it). Nothing was cleared \u2014 try again.\n"
32009
+ );
32010
+ process.exitCode = 1;
32011
+ return;
32012
+ }
32013
+ process.stdout.write(
32014
+ cleared === 0 ? "\u2713 No quarantined rules \u2014 nothing to clear.\n" : `\u2713 Cleared ${String(cleared)} quarantine verdict(s). Each rule is measured again the next time it loads, and is re-quarantined if it still exceeds the budget.
32015
+ `
32016
+ );
32017
+ }
30542
32018
  async function runUpdateSub(db, ids, all) {
30543
32019
  const { items } = await db.detections.listDetections({ filter: "all" });
30544
32020
  const byId = new Map(items.map((i) => [i.id, i]));
@@ -30660,11 +32136,11 @@ function terminalPrompter() {
30660
32136
  },
30661
32137
  isInteractive: process.stdin.isTTY && process.stdout.isTTY,
30662
32138
  ask(question) {
30663
- return new Promise((resolve4) => {
32139
+ return new Promise((resolve6) => {
30664
32140
  const rl = createInterface({ input: process.stdin, output: process.stdout });
30665
32141
  rl.question(question, (answer) => {
30666
32142
  rl.close();
30667
- resolve4(answer);
32143
+ resolve6(answer);
30668
32144
  });
30669
32145
  });
30670
32146
  },
@@ -30675,7 +32151,7 @@ function terminalPrompter() {
30675
32151
  // fatal signals: a security tool must never brick the terminal it was
30676
32152
  // invoked from.
30677
32153
  askHidden(question) {
30678
- return new Promise((resolve4, reject) => {
32154
+ return new Promise((resolve6, reject) => {
30679
32155
  const stdin = process.stdin;
30680
32156
  process.stdout.write(question);
30681
32157
  stdin.setRawMode(true);
@@ -30698,7 +32174,7 @@ function terminalPrompter() {
30698
32174
  restore();
30699
32175
  process.stdout.write("\n");
30700
32176
  if (err) reject(err);
30701
- else resolve4(state.value);
32177
+ else resolve6(state.value);
30702
32178
  };
30703
32179
  const onError = (err) => {
30704
32180
  finish(err);
@@ -31052,7 +32528,6 @@ async function runApproveReveal(pointer, values, io, base) {
31052
32528
  const vault = new SecretVault({
31053
32529
  repo: db.secretVault,
31054
32530
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31055
- fingerprintKey: loadOrCreateFingerprintKey(dir),
31056
32531
  // Read live so a consent revocation applies to the very next call.
31057
32532
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31058
32533
  });
@@ -31344,11 +32819,11 @@ async function runShow(argv, io) {
31344
32819
  ensurePacks();
31345
32820
  const rule = getLoadedRules().find((r) => r.id === ex.ruleId);
31346
32821
  const ruleLine = rule ? `${ex.ruleId} (${ex.category} \xB7 ${rule.severity})` : `${ex.ruleId} (${ex.category})`;
31347
- const fp = `${ex.valueFingerprint.slice(0, 4)}\u2026${ex.valueFingerprint.slice(-4)}`;
31348
32822
  const label = (name) => ` ${name.padEnd(10)}`;
31349
32823
  const lines = [
32824
+ `${label("Id")}${shortId(ex.id)}`,
31350
32825
  `${label("Rule")}${ruleLine}`,
31351
- `${label("Value")}${ex.maskedValue} fingerprint ${fp} (key v${String(ex.keyVersion)})`,
32826
+ `${label("Value")}${ex.maskedValue} (fingerprint key v${String(ex.keyVersion)})`,
31352
32827
  `${label("Scope")}${scopeSummary(ex)}${ex.maxUses !== null ? ` \xB7 max ${String(ex.maxUses)} use${ex.maxUses === 1 ? "" : "s"}` : ""}`,
31353
32828
  `${label("Reason")}"${ex.justification}"`,
31354
32829
  `${label("Created")}${formatTimestamp(ex.createdAt)} by ${ex.createdBy} via ${viaLabel(ex.createdVia)}`,
@@ -31412,6 +32887,17 @@ async function runRevoke(argv, io) {
31412
32887
  db.close();
31413
32888
  }
31414
32889
  }
32890
+ async function countApprovableBlocked(db, dir) {
32891
+ const key = (() => {
32892
+ try {
32893
+ return keyStateOf(readFingerprintKey(dir));
32894
+ } catch {
32895
+ return { status: "unreadable" };
32896
+ }
32897
+ })();
32898
+ const rows = await db.exceptions.recentBlocked(BLOCKED_DETECTIONS_RETENTION_MS);
32899
+ return rows.filter((row) => isMatchableUnder(row.keyVersion, key)).length;
32900
+ }
31415
32901
  async function runRotateKey(argv, io) {
31416
32902
  const { values } = parseArgs4({
31417
32903
  args: argv,
@@ -31438,6 +32924,10 @@ written under the old key simply stop matching. They remain listed for audit.
31438
32924
  `);
31439
32925
  }
31440
32926
  }
32927
+ io.out(`
32928
+ ${rotationBlockedLedgerNote(await countApprovableBlocked(db, dir))}
32929
+
32930
+ `);
31441
32931
  if (values.yes !== true) {
31442
32932
  if (!io.isInteractive) {
31443
32933
  throw new Error("pass --yes to rotate non-interactively");
@@ -31466,7 +32956,6 @@ Existing grants no longer match; re-approve deliberately where still needed.
31466
32956
  const vault = new SecretVault({
31467
32957
  repo: db.secretVault,
31468
32958
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31469
- fingerprintKey: next,
31470
32959
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31471
32960
  });
31472
32961
  const refreshed = await vault.refreshFingerprints(next);
@@ -31507,14 +32996,208 @@ async function runException(argv, io = terminalPrompter()) {
31507
32996
  await handler(rest, io);
31508
32997
  }
31509
32998
 
32999
+ // src/commands/extension.ts
33000
+ import { execFileSync as execFileSync3 } from "child_process";
33001
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
33002
+ import { createRequire as createRequire3 } from "module";
33003
+ import { homedir as homedir5 } from "os";
33004
+ import { dirname as dirname7, join as join18 } from "path";
33005
+ import { fileURLToPath as fileURLToPath4 } from "url";
33006
+ var require4 = createRequire3(import.meta.url);
33007
+ var here2 = dirname7(fileURLToPath4(import.meta.url));
33008
+ function cliRoot() {
33009
+ return isSea() ? dirname7(process.execPath) : join18(here2, "..");
33010
+ }
33011
+ var EXTENSION_ID = "mdoiaiemcnjnaokmcmgbikcdhgiemdof";
33012
+ var NATIVE_HOST_NAME = "com.akasecurity.aka";
33013
+ function nativeHostManifest(hostPath) {
33014
+ return {
33015
+ name: NATIVE_HOST_NAME,
33016
+ description: "AI Traffic Control native messaging host \u2014 bridges the browser extension to the local ~/.aka SQLite store.",
33017
+ path: hostPath,
33018
+ type: "stdio",
33019
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`]
33020
+ };
33021
+ }
33022
+ function resolveHostScript(root = cliRoot()) {
33023
+ const bundled = join18(root, "native-host", "host.js");
33024
+ if (existsSync13(bundled)) return bundled;
33025
+ try {
33026
+ const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
33027
+ const devBuilt = join18(dirname7(pkgJson), "native-host", "host.js");
33028
+ if (existsSync13(devBuilt)) return devBuilt;
33029
+ } catch {
33030
+ }
33031
+ return null;
33032
+ }
33033
+ function resolveExtensionDist(root = cliRoot()) {
33034
+ const bundled = join18(root, "extension");
33035
+ if (existsSync13(bundled)) return bundled;
33036
+ try {
33037
+ const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
33038
+ const devDist = join18(dirname7(pkgJson), "dist");
33039
+ if (existsSync13(devDist)) return devDist;
33040
+ } catch {
33041
+ }
33042
+ return null;
33043
+ }
33044
+ function chromeManifestDir(platform2 = process.platform) {
33045
+ if (platform2 === "darwin") {
33046
+ return join18(
33047
+ homedir5(),
33048
+ "Library",
33049
+ "Application Support",
33050
+ "Google",
33051
+ "Chrome",
33052
+ "NativeMessagingHosts"
33053
+ );
33054
+ }
33055
+ if (platform2 === "win32") {
33056
+ return join18(homedir5(), ".aka", "native-messaging");
33057
+ }
33058
+ return join18(homedir5(), ".config", "google-chrome", "NativeMessagingHosts");
33059
+ }
33060
+ function resolveNodeForLauncher() {
33061
+ if (!isSea()) return process.execPath;
33062
+ try {
33063
+ const probe = process.platform === "win32" ? "where" : "which";
33064
+ const out = execFileSync3(probe, ["node"], { encoding: "utf8" }).split(/\r?\n/)[0]?.trim();
33065
+ if (out === void 0 || out === "") return null;
33066
+ return out;
33067
+ } catch {
33068
+ return null;
33069
+ }
33070
+ }
33071
+ function launcherPath(manifestDir, platform2) {
33072
+ return join18(
33073
+ manifestDir,
33074
+ platform2 === "win32" ? `${NATIVE_HOST_NAME}.cmd` : `${NATIVE_HOST_NAME}.sh`
33075
+ );
33076
+ }
33077
+ function posixQuote(value) {
33078
+ return `'${value.replaceAll("'", "'\\''")}'`;
33079
+ }
33080
+ function cmdQuote(value) {
33081
+ return `"${value.replaceAll("%", "%%")}"`;
33082
+ }
33083
+ function writeLauncher(manifestDir, hostScript, nodePath, platform2) {
33084
+ const path = launcherPath(manifestDir, platform2);
33085
+ const tmp = `${path}.tmp`;
33086
+ if (platform2 === "win32") {
33087
+ writeFileSync7(tmp, `@echo off\r
33088
+ ${cmdQuote(nodePath)} ${cmdQuote(hostScript)} %*\r
33089
+ `);
33090
+ } else {
33091
+ writeFileSync7(tmp, `#!/bin/sh
33092
+ exec ${posixQuote(nodePath)} ${posixQuote(hostScript)} "$@"
33093
+ `, {
33094
+ mode: 493
33095
+ });
33096
+ }
33097
+ renameSync5(tmp, path);
33098
+ return path;
33099
+ }
33100
+ function windowsRegistryKey() {
33101
+ return `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
33102
+ }
33103
+ function registerWindowsHost(manifestPath) {
33104
+ execFileSync3("reg", [
33105
+ "add",
33106
+ windowsRegistryKey(),
33107
+ "/ve",
33108
+ "/t",
33109
+ "REG_SZ",
33110
+ "/d",
33111
+ manifestPath,
33112
+ "/f"
33113
+ ]);
33114
+ }
33115
+ function runExtension(argv) {
33116
+ const sub = argv[0];
33117
+ if (sub === "install") {
33118
+ runInstall(chromeManifestDir());
33119
+ return;
33120
+ }
33121
+ if (sub === "status") {
33122
+ runStatus(chromeManifestDir());
33123
+ return;
33124
+ }
33125
+ process.stderr.write(
33126
+ `aka extension: unknown subcommand '${sub ?? ""}'
33127
+ Usage: aka extension <install|status>
33128
+ `
33129
+ );
33130
+ process.exitCode = 1;
33131
+ }
33132
+ function runInstall(manifestDir, hostScript = resolveHostScript(), extensionDist = resolveExtensionDist(), nodePath = resolveNodeForLauncher()) {
33133
+ if (!hostScript) {
33134
+ process.stderr.write(
33135
+ "aka extension install: could not find the native-messaging host script.\n In a workspace checkout, build it first: pnpm --filter @akasecurity/plugin-browser-extension build\n"
33136
+ );
33137
+ process.exitCode = 1;
33138
+ return;
33139
+ }
33140
+ if (!nodePath) {
33141
+ process.stderr.write(
33142
+ "aka extension install: no Node.js runtime found to run the native-messaging host.\n The host runs under Node; install Node.js (so `node` resolves on PATH) and re-run.\n"
33143
+ );
33144
+ process.exitCode = 1;
33145
+ return;
33146
+ }
33147
+ mkdirSync5(manifestDir, { recursive: true });
33148
+ const launcher = writeLauncher(manifestDir, hostScript, nodePath, process.platform);
33149
+ const manifestPath = join18(manifestDir, `${NATIVE_HOST_NAME}.json`);
33150
+ const tmp = `${manifestPath}.tmp`;
33151
+ writeFileSync7(tmp, `${JSON.stringify(nativeHostManifest(launcher), null, 2)}
33152
+ `, {
33153
+ mode: DATA_FILE_MODE
33154
+ });
33155
+ renameSync5(tmp, manifestPath);
33156
+ if (process.platform === "win32") {
33157
+ try {
33158
+ registerWindowsHost(manifestPath);
33159
+ } catch (err) {
33160
+ process.stderr.write(
33161
+ `aka extension install: could not register the Windows registry key (${err instanceof Error ? err.message : String(err)}) \u2014 the native host won't be discoverable by Chrome until this is fixed.
33162
+ `
33163
+ );
33164
+ process.exitCode = 1;
33165
+ return;
33166
+ }
33167
+ }
33168
+ process.stdout.write(
33169
+ `\u2713 Installed the AKA native-messaging host
33170
+ host script: ${hostScript}
33171
+ launcher: ${launcher}
33172
+ manifest: ${manifestPath}
33173
+ ` + (process.platform === "win32" ? ` registry: ${windowsRegistryKey()}
33174
+ ` : "") + `
33175
+ Next: load the extension in Chrome \u2014
33176
+ 1. Open chrome://extensions
33177
+ 2. Enable "Developer mode" (top right)
33178
+ 3. Click "Load unpacked" and select:
33179
+ ${extensionDist ?? "<run: pnpm --filter @akasecurity/plugin-browser-extension build>"}
33180
+ `
33181
+ );
33182
+ }
33183
+ function runStatus(manifestDir) {
33184
+ const manifestPath = join18(manifestDir, `${NATIVE_HOST_NAME}.json`);
33185
+ const installed = existsSync13(manifestPath);
33186
+ process.stdout.write(
33187
+ `native-messaging host: ${installed ? "installed" : "not installed"}
33188
+ manifest: ${manifestPath}
33189
+ ` + (installed ? "" : " run `aka extension install` to set it up\n")
33190
+ );
33191
+ }
33192
+
31510
33193
  // src/commands/init.ts
31511
- import { existsSync as existsSync12, statSync as statSync8 } from "fs";
31512
- import { join as join17 } from "path";
33194
+ import { existsSync as existsSync15, lstatSync as lstatSync2, readdirSync as readdirSync7, readlinkSync, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
33195
+ import { dirname as dirname8, join as join19, resolve as resolve5 } from "path";
31513
33196
  import * as readline from "readline/promises";
31514
33197
  import { parseArgs as parseArgs6 } from "util";
31515
33198
 
31516
33199
  // src/commands/plugins.ts
31517
- import { existsSync as existsSync11 } from "fs";
33200
+ import { existsSync as existsSync14 } from "fs";
31518
33201
  import { parseArgs as parseArgs5 } from "util";
31519
33202
  function runPlugins(argv) {
31520
33203
  const [sub, ...rest] = argv;
@@ -31531,7 +33214,7 @@ async function listPlugins(argv) {
31531
33214
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
31532
33215
  const home = homeBase(values.home);
31533
33216
  const active = /* @__PURE__ */ new Set();
31534
- if (existsSync11(dbPath(home))) {
33217
+ if (existsSync14(dbPath(home))) {
31535
33218
  const db = openLocalDatabase(dataDir(home));
31536
33219
  try {
31537
33220
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -31540,7 +33223,7 @@ async function listPlugins(argv) {
31540
33223
  db.close();
31541
33224
  }
31542
33225
  }
31543
- const installed = installedPluginVersions();
33226
+ const installed = installedAgentPluginVersions();
31544
33227
  const out = process.stdout;
31545
33228
  out.write("Agent plugins (the CLI is an optional hub \u2014 plugins also self-install):\n\n");
31546
33229
  for (const a of AGENT_PLUGINS) {
@@ -31571,42 +33254,44 @@ function installPlugin(argv) {
31571
33254
  return;
31572
33255
  }
31573
33256
  const ref = pluginRef(agent);
31574
- if (!ref) {
33257
+ const cliBin = agent.cliBin;
33258
+ if (!ref || !cliBin) {
31575
33259
  process.stdout.write(
31576
- `${agent.name} has no automated install path yet \u2014 install it from the AKA marketplace in Claude Code, then run \`aka init\`.
33260
+ agent.installHint ? `${agent.installHint}
33261
+ ` : `${agent.name} has no automated install path yet \u2014 install it from the AKA marketplace in ${agent.name}, then run \`aka init\`.
31577
33262
  `
31578
33263
  );
31579
33264
  return;
31580
33265
  }
31581
- if (!claudeAvailable()) {
33266
+ if (!createCliPluginManager(cliBin).available()) {
31582
33267
  process.stdout.write(
31583
33268
  `Installing ${agent.name}\u2026
31584
33269
 
31585
- The \`claude\` CLI isn't on your PATH, so I can't install it automatically.
31586
- Install it from inside Claude Code:
31587
- /plugin marketplace add ${agent.marketplaceSource ?? ""}
31588
- /plugin install ${ref}
33270
+ The \`${cliBin}\` CLI isn't on your PATH, so I can't install it automatically.
33271
+ Install it from inside ${agent.name}:
33272
+ ${cliBin} plugin marketplace add ${agent.marketplaceSource ?? ""}
33273
+ ${cliBin} plugin install ${ref}
31589
33274
 
31590
33275
  Then run \`aka init\` to set up the local store.
31591
33276
  `
31592
33277
  );
31593
33278
  return;
31594
33279
  }
31595
- process.stdout.write(`Installing ${agent.name} via Claude Code\u2026
33280
+ process.stdout.write(`Installing ${agent.name} via ${cliBin}\u2026
31596
33281
  `);
31597
33282
  const { ok } = installAgentPlugin(agent.id, "inherit");
31598
33283
  if (ok) {
31599
33284
  process.stdout.write(
31600
33285
  `
31601
33286
  \u2713 Installed ${agent.name}.
31602
- \u21BB Restart Claude Code to load it.
33287
+ \u21BB Restart ${agent.name} to load it.
31603
33288
  Run \`aka init\` to scaffold your local store (if you haven't already).
31604
33289
  `
31605
33290
  );
31606
33291
  } else {
31607
33292
  process.stderr.write(
31608
33293
  `
31609
- \u2717 Install failed \u2014 see the output above, or add it in Claude Code with \`/plugin install ${ref}\`.
33294
+ \u2717 Install failed \u2014 see the output above, or add it in ${agent.name} with \`${cliBin} plugin install ${ref}\`.
31610
33295
  `
31611
33296
  );
31612
33297
  process.exitCode = 1;
@@ -31615,40 +33300,159 @@ Then run \`aka init\` to set up the local store.
31615
33300
 
31616
33301
  // src/commands/init.ts
31617
33302
  var PLUGIN_OFFER_IDENTITY = `${PRODUCT_NAME} \u2014 ${PRODUCT_TAGLINE}`;
33303
+ var STORE_DB = "the store database (including the prompt corpus)";
33304
+ var STORE_SETTINGS = "your settings file";
33305
+ function storeContents(home) {
33306
+ return /* @__PURE__ */ new Map([
33307
+ [home, "the store (including the prompt corpus in aka.db)"],
33308
+ [settingsDir(home), STORE_SETTINGS],
33309
+ [dataDir(home), STORE_DB],
33310
+ [keysDir(home), "the vault key"],
33311
+ [join19(settingsDir(home), "settings.json"), STORE_SETTINGS],
33312
+ [dbPath(home), STORE_DB]
33313
+ ]);
33314
+ }
33315
+ function storeTargets(home) {
33316
+ return [...storeContents(home).keys()];
33317
+ }
33318
+ function storeModeTargets(home) {
33319
+ const targets = new Set(storeTargets(home));
33320
+ const data = dataDir(home);
33321
+ try {
33322
+ for (const name of readdirSync7(data)) {
33323
+ if (name.endsWith(".partial")) continue;
33324
+ targets.add(join19(data, name));
33325
+ }
33326
+ } catch {
33327
+ }
33328
+ return [...targets];
33329
+ }
31618
33330
  function looseStorePaths(home) {
31619
33331
  if (process.platform === "win32") return [];
31620
- const targets = [
31621
- home,
31622
- settingsDir(home),
31623
- dataDir(home),
31624
- join17(settingsDir(home), "settings.json"),
31625
- dbPath(home)
31626
- ];
31627
- return targets.filter((p) => {
33332
+ return storeModeTargets(home).filter((p) => {
31628
33333
  try {
31629
- return (statSync8(p).mode & 63) !== 0;
33334
+ if (lstatSync2(p).isSymbolicLink()) return false;
33335
+ return (statSync10(p).mode & 63) !== 0;
31630
33336
  } catch {
31631
33337
  return false;
31632
33338
  }
31633
33339
  });
31634
33340
  }
33341
+ function symlinkedStorePaths(home, platform2 = process.platform) {
33342
+ return [...storeContents(home)].flatMap(([path, holds]) => {
33343
+ try {
33344
+ if (!lstatSync2(path).isSymbolicLink()) return [];
33345
+ return [
33346
+ {
33347
+ path,
33348
+ target: linkTarget(path),
33349
+ holds,
33350
+ // existsSync follows the link, so a target that is gone reads as
33351
+ // absent here while lstat above still sees the link itself.
33352
+ missing: !existsSync15(path),
33353
+ mode: targetMode(path, platform2)
33354
+ }
33355
+ ];
33356
+ } catch {
33357
+ return [];
33358
+ }
33359
+ });
33360
+ }
33361
+ function linkTarget(path) {
33362
+ try {
33363
+ return realpathSync3(path);
33364
+ } catch {
33365
+ return resolve5(dirname8(path), readlinkSync(path));
33366
+ }
33367
+ }
33368
+ function targetMode(path, platform2) {
33369
+ if (platform2 === "win32") return void 0;
33370
+ try {
33371
+ return statSync10(path).mode & 511;
33372
+ } catch {
33373
+ return void 0;
33374
+ }
33375
+ }
33376
+ function symlinkWarnings(paths, platform2 = process.platform) {
33377
+ const seeAlso = '(see the "Data at rest" note in SECURITY.md)';
33378
+ return paths.map(({ path, target, holds, missing, mode }) => {
33379
+ if (missing) {
33380
+ return ` \u26A0 ${path} is a symlink to ${target}, which does not exist \u2014 ${holds} cannot be written there until you create that target or remove the link ${seeAlso}
33381
+ `;
33382
+ }
33383
+ const inherited = mode === void 0 ? "" : ` (currently ${formatMode(mode)}${(mode & 63) !== 0 ? ", NOT owner-only" : ""})`;
33384
+ const kept = platform2 === "win32" ? "" : "permissions are never changed through a symlink, so ";
33385
+ const under = platform2 === "win32" ? "" : " under that target's own permissions";
33386
+ return ` \u26A0 ${path} is a symlink to ${target}${inherited} \u2014 ${kept}${holds} is written there${under} ${seeAlso}
33387
+ `;
33388
+ }).join("");
33389
+ }
33390
+ function formatMode(mode) {
33391
+ return `0${mode.toString(8).padStart(3, "0")}`;
33392
+ }
33393
+ function assertStoreLinksResolve(home) {
33394
+ for (const dir of [home, settingsDir(home), dataDir(home)]) {
33395
+ if (!isBrokenLink(dir)) continue;
33396
+ throw new Error(
33397
+ `${dir} is a symlink to ${linkTarget(dir)}, which does not exist \u2014 create that target or remove the link, then re-run \`aka init\``
33398
+ );
33399
+ }
33400
+ }
33401
+ function isBrokenLink(path) {
33402
+ try {
33403
+ return lstatSync2(path).isSymbolicLink() && !existsSync15(path);
33404
+ } catch {
33405
+ return false;
33406
+ }
33407
+ }
33408
+ function assertStorePathsAreDirectories(home) {
33409
+ for (const dir of [home, settingsDir(home), dataDir(home), keysDir(home)]) {
33410
+ if (!existsAsNonDirectory(dir)) continue;
33411
+ const occupant = isSymlink2(dir) ? `is a symlink to ${linkTarget(dir)}, which is not a directory \u2014 remove the link` : "exists but is not a directory \u2014 move that file aside or remove it";
33412
+ throw new Error(`${dir} ${occupant}. AKA keeps its store there; re-run \`aka init\` after.`);
33413
+ }
33414
+ }
33415
+ function isSymlink2(path) {
33416
+ try {
33417
+ return lstatSync2(path).isSymbolicLink();
33418
+ } catch {
33419
+ return false;
33420
+ }
33421
+ }
33422
+ function existsAsNonDirectory(path) {
33423
+ try {
33424
+ return !statSync10(path).isDirectory();
33425
+ } catch {
33426
+ return false;
33427
+ }
33428
+ }
31635
33429
  async function runInit(argv) {
31636
33430
  const { values } = parseArgs6({
31637
33431
  args: argv,
31638
33432
  options: { ...HOME_OPTION, yes: { type: "boolean", short: "y" } }
31639
33433
  });
31640
33434
  const home = homeBase(values.home);
33435
+ assertStoreLinksResolve(home);
33436
+ assertStorePathsAreDirectories(home);
31641
33437
  ensureLayoutDirSync(home);
31642
33438
  const settings = settingsDir(home);
31643
33439
  ensureLayoutDirSync(settings);
31644
- const settingsFile = join17(settings, "settings.json");
31645
- const settingsCreated = !existsSync12(settingsFile);
31646
- if (settingsCreated) {
31647
- writeOwnerOnlyFileSync(
31648
- settingsFile,
31649
- `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
33440
+ const settingsFile = join19(settings, "settings.json");
33441
+ let settingsCreated = false;
33442
+ if (!existsSync15(settingsFile)) {
33443
+ try {
33444
+ settingsCreated = withFileLock(settingsFile, () => {
33445
+ if (existsSync15(settingsFile)) return false;
33446
+ writeOwnerOnlyFileSync(
33447
+ settingsFile,
33448
+ `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
31650
33449
  `
31651
- );
33450
+ );
33451
+ return true;
33452
+ });
33453
+ } catch (err) {
33454
+ if (!(err instanceof FileLockError) || err.reason !== "timeout") throw err;
33455
+ }
31652
33456
  }
31653
33457
  tightenFile(settingsFile);
31654
33458
  const db = openLocalDatabase(dataDir(home));
@@ -31664,14 +33468,15 @@ async function runInit(argv) {
31664
33468
  db.close();
31665
33469
  }
31666
33470
  const loose = looseStorePaths(home);
33471
+ const symlinked = symlinkedStorePaths(home);
31667
33472
  process.stdout.write(
31668
33473
  `\u2713 Initialized AKA at ${home}
31669
- settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
33474
+ settings: ${settingsFile}${settingsCreated ? "" : existsSync15(settingsFile) ? " (kept existing)" : " (not written \u2014 another process was writing it)"}
31670
33475
  database: ${dbPath(home)}
31671
33476
  seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
31672
33477
  ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
31673
33478
  ` : "") + (loose.length > 0 ? ` \u26A0 could not enforce owner-only permissions on ${loose.join(", ")} \u2014 this filesystem rejects chmod, so the store has no at-rest protection here (see the "Data at rest" note in SECURITY.md)
31674
- ` : "")
33479
+ ` : "") + symlinkWarnings(symlinked)
31675
33480
  );
31676
33481
  await offerPluginInstall(values.yes === true);
31677
33482
  }
@@ -31716,7 +33521,7 @@ async function confirm(question) {
31716
33521
  }
31717
33522
 
31718
33523
  // src/commands/scan.ts
31719
- import { statSync as statSync9 } from "fs";
33524
+ import { statSync as statSync11 } from "fs";
31720
33525
  import { parseArgs as parseArgs7 } from "util";
31721
33526
  function severityRank(s) {
31722
33527
  return Severity.options.indexOf(s);
@@ -31730,7 +33535,7 @@ function renderEgressLine(egress) {
31730
33535
  const cap = egress.truncated ? ` (capped at ${String(MAX_EGRESS_CALL_SITES_PER_PROJECT)} call sites)` : "";
31731
33536
  return `Data shares: ${String(egress.destinations)} destination(s) \xB7 ${String(egress.endpoints)} endpoint(s) \xB7 ${String(egress.callSites)} call site(s)${cap}`;
31732
33537
  }
31733
- function runScan(argv) {
33538
+ async function runScan(argv) {
31734
33539
  const { values, positionals } = parseArgs7({
31735
33540
  args: argv,
31736
33541
  options: {
@@ -31763,7 +33568,7 @@ function runScan(argv) {
31763
33568
  failOn = parsed.data;
31764
33569
  }
31765
33570
  try {
31766
- statSync9(target);
33571
+ statSync11(target);
31767
33572
  } catch {
31768
33573
  process.stderr.write(`aka scan: no such file or directory: ${target}
31769
33574
  `);
@@ -31778,7 +33583,11 @@ function runScan(argv) {
31778
33583
  let egress;
31779
33584
  try {
31780
33585
  const { ruleActions } = db.installedPacks.installedRuleset();
31781
- result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli", dataDir: storeDir });
33586
+ result = await scanPathIntoStore(db, target, {
33587
+ ruleActions,
33588
+ sourceTool: "cli",
33589
+ dataDir: storeDir
33590
+ });
31782
33591
  inventory = recordProjectInventory(db, target);
31783
33592
  egress = recordProjectEgress(db, target, result.egress, home);
31784
33593
  } finally {
@@ -32649,19 +34458,20 @@ function applyCliUpdate2() {
32649
34458
  function applyPluginUpdate2(status) {
32650
34459
  const agent = findAgent(status.id);
32651
34460
  const ref = agent ? pluginRef(agent) : void 0;
32652
- if (!agent || !ref) {
34461
+ const cliBin = agent?.cliBin;
34462
+ if (!agent || !ref || !cliBin) {
32653
34463
  process.stderr.write(`\u2717 ${status.name}: no update coordinates in the registry.
32654
34464
  `);
32655
34465
  return false;
32656
34466
  }
32657
- if (!claudeAvailable()) {
34467
+ if (!createCliPluginManager(cliBin).available()) {
32658
34468
  process.stderr.write(
32659
- `\u2717 ${status.name}: the \`claude\` CLI isn't on your PATH \u2014 install Claude Code, then run \`claude plugin update ${ref}\`.
34469
+ `\u2717 ${status.name}: the \`${cliBin}\` CLI isn't on your PATH \u2014 install ${agent.name}, then run \`${cliBin} plugin update ${ref}\`.
32660
34470
  `
32661
34471
  );
32662
34472
  return false;
32663
34473
  }
32664
- process.stdout.write(`Updating ${status.name} (claude plugin update ${ref})\u2026
34474
+ process.stdout.write(`Updating ${status.name} (${cliBin} plugin update ${ref})\u2026
32665
34475
  `);
32666
34476
  const { ok } = applyPluginUpdate(status.id, "inherit");
32667
34477
  process.stdout.write(ok ? `\u2713 ${status.name} updated.
@@ -32719,7 +34529,6 @@ async function runShow2(argv, io) {
32719
34529
  const vault = new SecretVault({
32720
34530
  repo: db.secretVault,
32721
34531
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
32722
- fingerprintKey: loadOrCreateFingerprintKey(dir),
32723
34532
  // Read live so a consent revocation applies to the very next call.
32724
34533
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
32725
34534
  });
@@ -32807,6 +34616,7 @@ var COMMANDS = {
32807
34616
  detections: runDetections,
32808
34617
  plugins: runPlugins,
32809
34618
  dashboard: runDashboard,
34619
+ extension: runExtension,
32810
34620
  exception: (argv) => runException(argv),
32811
34621
  vault: (argv) => runVault(argv),
32812
34622
  tui: runTui,