@akasecurity/cli 0.9.4 → 0.9.6

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 (163) hide show
  1. package/README.md +18 -0
  2. package/dist/cli.js +2394 -505
  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 +31139 -0
  12. package/native-host/scan-worker.js +18055 -0
  13. package/package.json +11 -7
  14. package/web-ui/package.json +14 -7
  15. package/web-ui/packages/dashboard-ui/package.json +2 -0
  16. package/web-ui/packages/detections/package.json +3 -2
  17. package/web-ui/packages/persistence/package.json +3 -2
  18. package/web-ui/packages/plugin-sdk/package.json +5 -2
  19. package/web-ui/packages/ui-kit/package.json +1 -0
  20. package/web-ui/rules/secrets/gcp-service-account.json +1 -1
  21. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  22. package/web-ui/web-ui/.next/app-build-manifest.json +194 -56
  23. package/web-ui/web-ui/.next/app-path-routes-manifest.json +9 -9
  24. package/web-ui/web-ui/.next/build-manifest.json +8 -8
  25. package/web-ui/web-ui/.next/prerender-manifest.json +3 -3
  26. package/web-ui/web-ui/.next/required-server-files.json +5 -0
  27. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  28. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  29. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  31. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  32. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  34. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  35. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  36. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  37. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  38. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  39. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  40. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  41. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  42. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
  43. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  44. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  45. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  46. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  47. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  48. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  49. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  50. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  51. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  52. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  53. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  54. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  55. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  56. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  57. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  58. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  59. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  60. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  61. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  62. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  63. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  64. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  65. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  66. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -2
  67. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -1
  68. package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -1
  69. package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
  70. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  71. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  72. package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
  73. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  74. package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
  75. package/web-ui/web-ui/.next/server/app/page.js +2 -2
  76. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/web-ui/web-ui/.next/server/app-paths-manifest.json +9 -9
  78. package/web-ui/web-ui/.next/server/chunks/114.js +792 -0
  79. package/web-ui/web-ui/.next/server/chunks/160.js +785 -0
  80. package/web-ui/web-ui/.next/server/chunks/243.js +1 -1
  81. package/web-ui/web-ui/.next/server/chunks/333.js +1 -0
  82. package/web-ui/web-ui/.next/server/chunks/363.js +12 -0
  83. package/web-ui/web-ui/.next/server/chunks/413.js +1 -0
  84. package/web-ui/web-ui/.next/server/chunks/528.js +73 -0
  85. package/web-ui/web-ui/.next/server/chunks/557.js +5 -0
  86. package/web-ui/web-ui/.next/server/chunks/561.js +123 -0
  87. package/web-ui/web-ui/.next/server/functions-config-manifest.json +4 -4
  88. package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
  89. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  90. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  91. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  92. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  93. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  94. package/web-ui/web-ui/.next/static/chunks/133-66e4c8e40b72a3e8.js +1 -0
  95. package/web-ui/web-ui/.next/static/chunks/539-b35890d398017f99.js +62 -0
  96. package/web-ui/web-ui/.next/static/chunks/992-ba273ebe2ac236a5.js +1 -0
  97. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/loading-0e999b82265bac84.js +1 -0
  98. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-1151de62cd9386c8.js +1 -0
  99. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/loading-0e999b82265bac84.js +1 -0
  100. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-eaee9ff5ba1056e3.js +1 -0
  101. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/loading-0e999b82265bac84.js +1 -0
  102. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-7fbcf876de9f09de.js +1 -0
  103. package/web-ui/web-ui/.next/static/chunks/app/(app)/error-092d6780c93209d5.js +1 -0
  104. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/loading-0e999b82265bac84.js +1 -0
  105. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-8f33eba279e45c1d.js +1 -0
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/loading-0e999b82265bac84.js +1 -0
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/loading-0e999b82265bac84.js +1 -0
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/{page-1d468a1a0cf12aea.js → page-bf9c74a239afdab2.js} +1 -1
  109. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-e17e9d7c8547bc32.js +1 -0
  110. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/loading-0e999b82265bac84.js +1 -0
  111. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-4bdf3d2ede57b3c7.js +1 -0
  112. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/loading-0e999b82265bac84.js +1 -0
  113. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-88b768ca343a8524.js +1 -0
  114. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-a346cd78f5f28936.js +1 -0
  115. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/loading-0e999b82265bac84.js +1 -0
  116. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-f79874ebd61fbf46.js +1 -0
  117. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/loading-0e999b82265bac84.js +1 -0
  118. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-86e7e860ce98d0a4.js +1 -0
  119. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/loading-0e999b82265bac84.js +1 -0
  120. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-557e1703a106d286.js +1 -0
  121. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/loading-0e999b82265bac84.js +1 -0
  122. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-a1956abf86b48b8b.js +1 -0
  123. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/loading-0e999b82265bac84.js +1 -0
  124. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-fe71f1fd0ab6cde6.js +1 -0
  125. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/loading-0e999b82265bac84.js +1 -0
  126. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-2473d9063bd5eb69.js +1 -0
  127. package/web-ui/web-ui/.next/static/chunks/app/page-315a9068831a3ad5.js +1 -0
  128. package/web-ui/web-ui/.next/static/chunks/{framework-3444de8758288822.js → framework-a566532c222ba944.js} +1 -1
  129. package/web-ui/web-ui/.next/static/chunks/{main-b13545f01f111855.js → main-f710385bb68f9de5.js} +1 -1
  130. package/web-ui/web-ui/.next/static/chunks/pages/{_app-7f0d920cc5af1c9f.js → _app-63f369037199ea68.js} +1 -1
  131. package/web-ui/web-ui/.next/static/chunks/pages/{_error-3d8b61361c4b2c6b.js → _error-41a11264bde7b852.js} +1 -1
  132. package/web-ui/web-ui/.next/static/css/2eca310301b96d98.css +2 -0
  133. package/web-ui/web-ui/.next/static/{cVKs25XbQsC1083sU5GOD → scT_YT84DnOMXJI2dA75N}/_buildManifest.js +1 -1
  134. package/web-ui/web-ui/dist/scan-worker.js +18055 -0
  135. package/web-ui/web-ui/package.json +4 -3
  136. package/web-ui/web-ui/server.js +1 -1
  137. package/web-ui/web-ui/.next/server/chunks/28.js +0 -1
  138. package/web-ui/web-ui/.next/server/chunks/330.js +0 -1
  139. package/web-ui/web-ui/.next/server/chunks/334.js +0 -2
  140. package/web-ui/web-ui/.next/server/chunks/514.js +0 -755
  141. package/web-ui/web-ui/.next/server/chunks/628.js +0 -84
  142. package/web-ui/web-ui/.next/server/chunks/817.js +0 -1
  143. package/web-ui/web-ui/.next/server/chunks/830.js +0 -750
  144. package/web-ui/web-ui/.next/server/chunks/949.js +0 -123
  145. package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +0 -1
  146. package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +0 -62
  147. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +0 -1
  148. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +0 -1
  149. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +0 -1
  150. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +0 -1
  151. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +0 -1
  152. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +0 -1
  153. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +0 -1
  154. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +0 -1
  155. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +0 -1
  156. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +0 -1
  157. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +0 -1
  158. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +0 -1
  159. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +0 -1
  160. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +0 -1
  161. package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +0 -1
  162. package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +0 -2
  163. /package/web-ui/web-ui/.next/static/{cVKs25XbQsC1083sU5GOD → scT_YT84DnOMXJI2dA75N}/_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, 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) {
@@ -2161,6 +2263,40 @@ function escapeRegExp2(value) {
2161
2263
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2162
2264
  }
2163
2265
 
2266
+ // ../packages/detections/src/regex-cache.ts
2267
+ var singles = /* @__PURE__ */ new WeakMap();
2268
+ var keywordLists = /* @__PURE__ */ new WeakMap();
2269
+ var labelLists = /* @__PURE__ */ new WeakMap();
2270
+ function listCache(kind) {
2271
+ return kind === "keyword" ? keywordLists : labelLists;
2272
+ }
2273
+ function memoizedRegExp(owner, build) {
2274
+ const cached2 = singles.get(owner);
2275
+ if (cached2 !== void 0) {
2276
+ cached2.lastIndex = 0;
2277
+ return cached2;
2278
+ }
2279
+ const compiled = build();
2280
+ singles.set(owner, compiled);
2281
+ return compiled;
2282
+ }
2283
+ function memoizedRegExpList(kind, owner, build) {
2284
+ const cache = listCache(kind);
2285
+ const cached2 = cache.get(owner);
2286
+ if (cached2 !== void 0) {
2287
+ if (cached2.stateful) {
2288
+ for (const re of cached2.entries) if (re !== void 0) re.lastIndex = 0;
2289
+ }
2290
+ return cached2.entries;
2291
+ }
2292
+ const entries = build();
2293
+ cache.set(owner, {
2294
+ entries,
2295
+ stateful: entries.some((re) => re !== void 0 && (re.global || re.sticky))
2296
+ });
2297
+ return entries;
2298
+ }
2299
+
2164
2300
  // ../packages/detections/src/matchers/limits.ts
2165
2301
  var MAX_MATCHES_PER_RULE = 1e4;
2166
2302
  var MAX_REGEX_INPUT_LENGTH = 2e5;
@@ -2171,10 +2307,17 @@ var KeywordMatcher = class {
2171
2307
  if (rule.matcher.type !== "keyword") return [];
2172
2308
  const { keywords, caseSensitive } = rule.matcher;
2173
2309
  const spans = [];
2174
- for (const kw of keywords) {
2175
- if (kw.length === 0) continue;
2310
+ const compiled = memoizedRegExpList(
2311
+ "keyword",
2312
+ rule.matcher,
2313
+ () => keywords.map((kw) => {
2314
+ if (kw.length === 0) return void 0;
2315
+ return new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
2316
+ })
2317
+ );
2318
+ for (const re of compiled) {
2319
+ if (re === void 0) continue;
2176
2320
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
2177
- const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
2178
2321
  let m;
2179
2322
  while ((m = re.exec(text)) !== null) {
2180
2323
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -2190,7 +2333,10 @@ var RegexMatcher = class {
2190
2333
  match(text, rule) {
2191
2334
  if (rule.matcher.type !== "regex") return [];
2192
2335
  const { pattern, flags, captureGroup } = rule.matcher;
2193
- const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
2336
+ const re = memoizedRegExp(
2337
+ rule.matcher,
2338
+ () => new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`)
2339
+ );
2194
2340
  const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
2195
2341
  const spans = [];
2196
2342
  let m;
@@ -2298,11 +2444,15 @@ function isCorroborated(candidate, candidates, text) {
2298
2444
  const labels = req.labels;
2299
2445
  if (labels && labels.length > 0) {
2300
2446
  const haystack = text.slice(Math.max(0, winStart), winEnd);
2301
- for (const label of labels) {
2302
- const trimmed = label.trim();
2303
- if (trimmed.length === 0) continue;
2304
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
2305
- if (re.test(haystack)) return true;
2447
+ for (const re of memoizedRegExpList(
2448
+ "label",
2449
+ req,
2450
+ () => labels.map((label) => {
2451
+ const trimmed = label.trim();
2452
+ return trimmed.length === 0 ? void 0 : new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
2453
+ })
2454
+ )) {
2455
+ if (re?.test(haystack)) return true;
2306
2456
  }
2307
2457
  }
2308
2458
  return false;
@@ -2486,6 +2636,14 @@ var SQLITE_MIGRATIONS = [
2486
2636
  {
2487
2637
  tag: "0018_serious_tana_nile",
2488
2638
  sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
2639
+ },
2640
+ {
2641
+ tag: "0019_audit_started_at_index",
2642
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
2643
+ },
2644
+ {
2645
+ tag: "0020_secret_vault_pagination_indexes",
2646
+ 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
2647
  }
2490
2648
  ];
2491
2649
 
@@ -17346,7 +17504,17 @@ var Finding = external_exports.object({
17346
17504
  }).meta({ id: "Finding" });
17347
17505
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
17348
17506
  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" });
17507
+ var FindingProvider = external_exports.enum([
17508
+ "claudecode",
17509
+ "claudedesktop",
17510
+ "cursor",
17511
+ "copilot",
17512
+ "chatgpt",
17513
+ "claudeai",
17514
+ "codex",
17515
+ "antigravity",
17516
+ "api"
17517
+ ]).meta({ id: "FindingProvider" });
17350
17518
  var FindingCategory = external_exports.enum([
17351
17519
  "secret",
17352
17520
  "pii",
@@ -17400,7 +17568,16 @@ var FindingInstance = external_exports.object({
17400
17568
  confidence: external_exports.number().min(0).max(1),
17401
17569
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
17402
17570
  // that predate the resolution feature stay valid.
17403
- status: FindingStatus.optional()
17571
+ status: FindingStatus.optional(),
17572
+ // The audit event this finding was captured from. Optional so callers that
17573
+ // do not project it stay valid. An at-rest finding is content-addressed by
17574
+ // finding_key and its row is upserted on re-detection, so this names the
17575
+ // MOST RECENT detection event, not the first.
17576
+ eventId: external_exports.string().optional(),
17577
+ // The session that event belongs to, when it has one — the seam a
17578
+ // per-instance "view session" link needs. Absent for events captured
17579
+ // outside a session.
17580
+ sessionId: external_exports.string().optional()
17404
17581
  }).meta({ id: "FindingInstance" });
17405
17582
  var FindingGroup = external_exports.object({
17406
17583
  id: external_exports.string(),
@@ -17444,7 +17621,11 @@ var FindingFacets = external_exports.object({
17444
17621
  // for every instance, so every group lands in a bucket; a status-less
17445
17622
  // group (possible only for callers whose rows carry no statuses) is
17446
17623
  // counted under no value.
17447
- status: external_exports.array(FindingFacetItem)
17624
+ status: external_exports.array(FindingFacetItem),
17625
+ // Host tool (attributes.tool_name). Present only on the instance-level
17626
+ // reads, which can filter by it; the grouped read omits the dimension
17627
+ // because a group spans tools.
17628
+ tool: external_exports.array(FindingFacetItem).optional()
17448
17629
  }).meta({ id: "FindingFacets" });
17449
17630
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
17450
17631
  var ListGroupedFindingsQuery = external_exports.object({
@@ -17462,6 +17643,16 @@ var ListGroupedFindingsQuery = external_exports.object({
17462
17643
  // Scope to findings whose event carries this session id (the Activity page's
17463
17644
  // session → findings drilldown). Findings without a session never match.
17464
17645
  sessionId: external_exports.string().optional(),
17646
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
17647
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
17648
+ // means all time — this list has no default window.
17649
+ from: external_exports.iso.datetime().optional(),
17650
+ // A group or instance id that must appear in the page even when the cursor
17651
+ // has already advanced past its sort position. This is what keeps the
17652
+ // Findings page's one-shot ?finding= deep link resolving once the list
17653
+ // paginates: the target group is appended out of sort order rather than
17654
+ // scanning forward for it. Never affects totals, facets or the cursor.
17655
+ includeId: external_exports.string().optional(),
17465
17656
  groupBy: external_exports.literal("type").optional(),
17466
17657
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
17467
17658
  cursor: external_exports.string().optional()
@@ -17506,15 +17697,110 @@ var FindingInstanceDetail = FindingInstance.extend({
17506
17697
  detection: FindingDetectionRef,
17507
17698
  policy: FindingPolicyRef
17508
17699
  }).meta({ id: "FindingInstanceDetail" });
17700
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
17701
+ var ListFindingInstancesQuery = external_exports.object({
17702
+ severity: external_exports.array(Severity).optional(),
17703
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
17704
+ subtype: external_exports.array(external_exports.string()).optional(),
17705
+ provider: external_exports.array(FindingProvider).optional(),
17706
+ action: external_exports.array(FindingAction).optional(),
17707
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
17708
+ // the grouped query's group-level fold.
17709
+ status: external_exports.array(FindingStatus).optional(),
17710
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
17711
+ // where the free-text `q` can only match the rendered "via Bash" label.
17712
+ tool: external_exports.array(external_exports.string()).optional(),
17713
+ // Exact repository / file-path matches, for the drill-down out of the
17714
+ // locations view. A row whose event carries no repo/file matches neither.
17715
+ repo: external_exports.string().optional(),
17716
+ file: 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(200).optional(),
17721
+ cursor: external_exports.string().optional()
17722
+ });
17723
+ var ListFindingInstancesResponse = external_exports.object({
17724
+ // Instances matching the filters across the whole scope, not just this
17725
+ // page — cursor-independent, like the grouped list's totals.
17726
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
17727
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
17728
+ // dimension still excludes its own filter.
17729
+ facets: FindingFacets,
17730
+ items: external_exports.array(FindingInstanceDetail),
17731
+ nextCursor: external_exports.string().nullable()
17732
+ }).meta({ id: "ListFindingInstancesResponse" });
17733
+ var FindingLocationFile = external_exports.object({
17734
+ // Empty when the instances carried no file path (a prompt or a tool call
17735
+ // with no file attribution).
17736
+ file: external_exports.string(),
17737
+ instanceCount: external_exports.number().int().nonnegative(),
17738
+ maxSeverity: Severity,
17739
+ latestDetectedAt: external_exports.iso.datetime(),
17740
+ // Folded from the instances' derived statuses with the same
17741
+ // open-dominates precedence a group uses.
17742
+ status: FindingStatus.optional(),
17743
+ // Distinct rules seen at this location, capped — the row shows them as
17744
+ // chips, and the count is what conveys scale.
17745
+ ruleIds: external_exports.array(external_exports.string())
17746
+ }).meta({ id: "FindingLocationFile" });
17747
+ var FindingLocationRepo = external_exports.object({
17748
+ /** Empty when the instances carried no repo attribute. */
17749
+ repo: external_exports.string(),
17750
+ instanceCount: external_exports.number().int().nonnegative(),
17751
+ maxSeverity: Severity,
17752
+ latestDetectedAt: external_exports.iso.datetime(),
17753
+ status: FindingStatus.optional(),
17754
+ files: external_exports.array(FindingLocationFile)
17755
+ }).meta({ id: "FindingLocationRepo" });
17756
+ var ListFindingLocationsQuery = external_exports.object({
17757
+ severity: external_exports.array(Severity).optional(),
17758
+ subtype: external_exports.array(external_exports.string()).optional(),
17759
+ provider: external_exports.array(FindingProvider).optional(),
17760
+ action: external_exports.array(FindingAction).optional(),
17761
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
17762
+ // instances that match, and folds its status from those.
17763
+ status: external_exports.array(FindingStatus).optional(),
17764
+ tool: external_exports.array(external_exports.string()).optional(),
17765
+ q: external_exports.string().optional(),
17766
+ sessionId: external_exports.string().optional(),
17767
+ from: external_exports.iso.datetime().optional(),
17768
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
17769
+ });
17770
+ var ListFindingLocationsResponse = external_exports.object({
17771
+ totals: external_exports.object({
17772
+ findings: external_exports.number().int().nonnegative(),
17773
+ repos: external_exports.number().int().nonnegative(),
17774
+ files: external_exports.number().int().nonnegative()
17775
+ }),
17776
+ /** Sorted by max severity, then most recent. */
17777
+ items: external_exports.array(FindingLocationRepo),
17778
+ /** Whether `limit` truncated the repo list. */
17779
+ hasMore: external_exports.boolean()
17780
+ }).meta({ id: "ListFindingLocationsResponse" });
17509
17781
 
17510
17782
  // ../packages/schema/src/zod/harness-map.ts
17511
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
17783
+ var Harness = external_exports.enum([
17784
+ "claudecode",
17785
+ "cursor",
17786
+ "copilot",
17787
+ "codex",
17788
+ "antigravity",
17789
+ "windsurf",
17790
+ "claudedesktop",
17791
+ "chatgpt",
17792
+ "claudeai",
17793
+ "api"
17794
+ ]).meta({ id: "Harness" });
17512
17795
  var TOOL_TO_HARNESS = {
17513
17796
  "claude-code": "claudecode",
17514
17797
  "claude-desktop": "claudedesktop",
17515
17798
  "github-copilot": "copilot",
17516
17799
  cursor: "cursor",
17517
- chatgpt: "chatgpt"
17800
+ chatgpt: "chatgpt",
17801
+ codex: "codex",
17802
+ antigravity: "antigravity",
17803
+ "claude-ai": "claudeai"
17518
17804
  };
17519
17805
 
17520
17806
  // ../packages/schema/src/zod/meta.ts
@@ -17972,7 +18258,18 @@ var ActivityOverviewResponse = external_exports.object({
17972
18258
  // ../packages/schema/src/zod/event.ts
17973
18259
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
17974
18260
  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" });
18261
+ var SourceTool = external_exports.enum([
18262
+ "claude-code",
18263
+ "claude-desktop",
18264
+ "cursor",
18265
+ "chatgpt",
18266
+ "claude-ai",
18267
+ "github-copilot",
18268
+ "codex",
18269
+ "antigravity",
18270
+ "cli",
18271
+ "unknown"
18272
+ ]).meta({ id: "SourceTool" });
17976
18273
  var EventMetadata = external_exports.object({
17977
18274
  sessionId: external_exports.string().optional(),
17978
18275
  repo: external_exports.string().optional(),
@@ -18043,7 +18340,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
18043
18340
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
18044
18341
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
18045
18342
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
18046
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
18343
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
18047
18344
  var AccessCounts = external_exports.object({
18048
18345
  open: external_exports.number().int().nonnegative(),
18049
18346
  approved: external_exports.number().int().nonnegative(),
@@ -18312,9 +18609,19 @@ var ExceptionBundleEntry = DetectionException.pick({
18312
18609
  useCount: true,
18313
18610
  conditions: true
18314
18611
  });
18612
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
18315
18613
  function isMatchableUnder(keyVersion, key) {
18316
18614
  return key.status === "present" && key.version === keyVersion;
18317
18615
  }
18616
+ var LEDGER_WINDOW_HOURS = 24;
18617
+ function rotationBlockedLedgerNote(stillApprovable) {
18618
+ if (stillApprovable <= 0) {
18619
+ 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.";
18620
+ }
18621
+ const window = `from the last ${String(LEDGER_WINDOW_HOURS)} hours`;
18622
+ 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`;
18623
+ return `${subject}. They stay listed as a record of what was blocked \u2014 trigger the detection again to approve under the new key.`;
18624
+ }
18318
18625
 
18319
18626
  // ../packages/schema/src/zod/rule.ts
18320
18627
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -19125,6 +19432,35 @@ var EgressWriteSummary = external_exports.object({
19125
19432
  droppedFiles: external_exports.array(external_exports.string()).default([])
19126
19433
  }).meta({ id: "EgressWriteSummary" });
19127
19434
 
19435
+ // ../packages/schema/src/zod/exception-action.ts
19436
+ var confirmation = external_exports.string().optional();
19437
+ var ApproveBlockedInput = external_exports.object({
19438
+ reference: external_exports.string(),
19439
+ scope: external_exports.string(),
19440
+ reason: external_exports.string(),
19441
+ confirmation
19442
+ });
19443
+ var AddExceptionInput = external_exports.object({
19444
+ ruleId: external_exports.string(),
19445
+ value: external_exports.string(),
19446
+ scope: external_exports.string(),
19447
+ reason: external_exports.string(),
19448
+ confirmation
19449
+ });
19450
+ var GrantRevealInput = external_exports.object({
19451
+ pointer: external_exports.string(),
19452
+ scope: external_exports.string(),
19453
+ justification: external_exports.string(),
19454
+ confirmation
19455
+ });
19456
+ var RevokeExceptionInput = external_exports.object({
19457
+ id: external_exports.string(),
19458
+ reason: external_exports.string()
19459
+ });
19460
+ var RotateKeyInput = external_exports.object({
19461
+ confirmation: external_exports.string()
19462
+ });
19463
+
19128
19464
  // ../packages/schema/src/zod/findings-group-build.ts
19129
19465
  function toApiAction(dbVal) {
19130
19466
  const map2 = {
@@ -19180,6 +19516,8 @@ function buildFindingGroups(rows, opts = {}) {
19180
19516
  repo: r.repo,
19181
19517
  file: r.file,
19182
19518
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
19519
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
19520
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
19183
19521
  action: toApiAction(effectiveDbAction),
19184
19522
  detectedAt: r.occurredAt,
19185
19523
  confidence: r.confidence,
@@ -19311,14 +19649,17 @@ function applyFindingFilters(groups, opts) {
19311
19649
  }
19312
19650
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
19313
19651
  var SEVERITY_RANK2 = SEVERITY_ORDER;
19652
+ function compareFindingGroupOrder(a, b) {
19653
+ const rankA = SEVERITY_RANK2[a.severity] ?? -1;
19654
+ const rankB = SEVERITY_RANK2[b.severity] ?? -1;
19655
+ const severityDiff = rankA - rankB;
19656
+ if (severityDiff !== 0) return severityDiff;
19657
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
19658
+ if (recencyDiff !== 0) return recencyDiff;
19659
+ return a.id.localeCompare(b.id);
19660
+ }
19314
19661
  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
- });
19662
+ return [...groups].sort(compareFindingGroupOrder);
19322
19663
  }
19323
19664
  function computeFindingFacets(allGroups, opts) {
19324
19665
  const forSeverity = applyFindingFilters(allGroups, {
@@ -19374,15 +19715,158 @@ function computeFindingFacets(allGroups, opts) {
19374
19715
  for (const g of forStatus) {
19375
19716
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
19376
19717
  }
19377
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
19718
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
19719
+ return {
19720
+ severity: toItems2(severityMap),
19721
+ provider: toItems2(providerMap),
19722
+ action: toItems2(actionMap),
19723
+ subtype: toItems2(subtypeMap),
19724
+ status: toItems2(statusMap)
19725
+ };
19726
+ }
19727
+
19728
+ // ../packages/schema/src/zod/findings-flat-build.ts
19729
+ function rowHaystack(row) {
19730
+ return [
19731
+ row.ruleId,
19732
+ row.category,
19733
+ row.maskedMatch,
19734
+ row.repo,
19735
+ row.file,
19736
+ row.toolName ? `via ${row.toolName}` : "",
19737
+ row.id
19738
+ ].join(" ").toLowerCase();
19739
+ }
19740
+ function matchesDimension(row, opts, dimension) {
19741
+ switch (dimension) {
19742
+ case "severity":
19743
+ return !opts.severity?.length || opts.severity.includes(row.severity);
19744
+ case "subtype":
19745
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
19746
+ case "providers":
19747
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
19748
+ case "actions":
19749
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
19750
+ case "statuses":
19751
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
19752
+ case "tools":
19753
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
19754
+ case "repo":
19755
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
19756
+ case "file":
19757
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
19758
+ case "q":
19759
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
19760
+ }
19761
+ }
19762
+ var DIMENSIONS = [
19763
+ "severity",
19764
+ "subtype",
19765
+ "providers",
19766
+ "actions",
19767
+ "statuses",
19768
+ "tools",
19769
+ "repo",
19770
+ "file",
19771
+ "q"
19772
+ ];
19773
+ function matchesInstanceFilters(row, opts, except) {
19774
+ for (const dimension of DIMENSIONS) {
19775
+ if (dimension === except) continue;
19776
+ if (!matchesDimension(row, opts, dimension)) return false;
19777
+ }
19778
+ return true;
19779
+ }
19780
+ function toItems(counts) {
19781
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
19782
+ }
19783
+ function bump(counts, value) {
19784
+ counts.set(value, (counts.get(value) ?? 0) + 1);
19785
+ }
19786
+ function createInstanceFacetAccumulator(opts) {
19787
+ const severity = /* @__PURE__ */ new Map();
19788
+ const subtype = /* @__PURE__ */ new Map();
19789
+ const provider = /* @__PURE__ */ new Map();
19790
+ const action = /* @__PURE__ */ new Map();
19791
+ const status = /* @__PURE__ */ new Map();
19792
+ const tool = /* @__PURE__ */ new Map();
19793
+ return {
19794
+ add(row) {
19795
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
19796
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
19797
+ if (matchesInstanceFilters(row, opts, "providers")) {
19798
+ bump(provider, toApiProvider(row.sourceTool));
19799
+ }
19800
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
19801
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
19802
+ bump(status, row.status);
19803
+ }
19804
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
19805
+ bump(tool, row.toolName);
19806
+ }
19807
+ },
19808
+ facets: () => ({
19809
+ severity: toItems(severity),
19810
+ subtype: toItems(subtype),
19811
+ provider: toItems(provider),
19812
+ action: toItems(action),
19813
+ status: toItems(status),
19814
+ tool: toItems(tool)
19815
+ })
19816
+ };
19817
+ }
19818
+ function toInstanceDetail(row) {
19819
+ const category = toApiCategory(row.category);
19820
+ return {
19821
+ id: row.id,
19822
+ provider: toApiProvider(row.sourceTool),
19823
+ repo: row.repo,
19824
+ file: row.file,
19825
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
19826
+ eventId: row.eventId,
19827
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
19828
+ action: toApiAction(row.actionTaken),
19829
+ detectedAt: row.occurredAt,
19830
+ confidence: row.confidence,
19831
+ ...row.status === void 0 ? {} : { status: row.status },
19832
+ groupId: row.ruleId,
19833
+ category,
19834
+ subtype: row.ruleId,
19835
+ severity: row.severity,
19836
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
19837
+ detection: { id: row.ruleId, name: null },
19838
+ policy: { id: `category:${category}`, name: category }
19839
+ };
19840
+ }
19841
+ var SEVERITY_ORDER2 = {
19842
+ critical: 0,
19843
+ high: 1,
19844
+ medium: 2,
19845
+ low: 3
19846
+ };
19847
+ function newLocationAccumulator() {
19378
19848
  return {
19379
- severity: toItems(severityMap),
19380
- provider: toItems(providerMap),
19381
- action: toItems(actionMap),
19382
- subtype: toItems(subtypeMap),
19383
- status: toItems(statusMap)
19849
+ instanceCount: 0,
19850
+ // Sorts after every known severity, so the first row always wins the
19851
+ // comparison below rather than an unknown value pinning the location.
19852
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
19853
+ maxSeverity: "low",
19854
+ latestDetectedAt: "",
19855
+ statuses: [],
19856
+ ruleIds: /* @__PURE__ */ new Set()
19384
19857
  };
19385
19858
  }
19859
+ function addToLocation(acc, row) {
19860
+ acc.instanceCount += 1;
19861
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
19862
+ if (rank < acc.maxSeverityRank) {
19863
+ acc.maxSeverityRank = rank;
19864
+ acc.maxSeverity = row.severity;
19865
+ }
19866
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
19867
+ acc.statuses.push(row.status);
19868
+ acc.ruleIds.add(row.ruleId);
19869
+ }
19386
19870
 
19387
19871
  // ../packages/schema/src/zod/installed-pack.ts
19388
19872
  var InstalledPack = external_exports.object({
@@ -19523,6 +20007,50 @@ var VaultInventoryEntry = external_exports.object({
19523
20007
  revealGrantId: external_exports.string().nullable(),
19524
20008
  sightings: external_exports.array(VaultSighting)
19525
20009
  });
20010
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
20011
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
20012
+ var MAX_VAULT_PAGE_LIMIT = 200;
20013
+ var ListVaultInventoryQuery = external_exports.object({
20014
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
20015
+ // Opaque; names the last row of the page just served.
20016
+ cursor: external_exports.string().optional()
20017
+ });
20018
+ var ListVaultInventoryResponse = external_exports.object({
20019
+ // Vaulted values across the whole store, not just this page — cursor-
20020
+ // independent, so paging never changes what the count claims.
20021
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
20022
+ items: external_exports.array(VaultInventoryEntry),
20023
+ // `null` once the last page is reached.
20024
+ nextCursor: external_exports.string().nullable()
20025
+ });
20026
+ var ListVaultReuseQuery = external_exports.object({
20027
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
20028
+ cursor: external_exports.string().optional()
20029
+ });
20030
+ var ListVaultReuseResponse = external_exports.object({
20031
+ // Reused values across the whole store — the number the section's claim
20032
+ // ("values detected in more than one place") is about.
20033
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
20034
+ items: external_exports.array(VaultInventoryEntry),
20035
+ nextCursor: external_exports.string().nullable()
20036
+ });
20037
+ var ListVaultDerefsQuery = external_exports.object({
20038
+ // Include the batched, high-volume reasons (display, view-render). Omitted
20039
+ // hides them and counts them into `hiddenBatched` instead, so the model
20040
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
20041
+ // over a Server Action, which preserves the type, never as a URL param.
20042
+ includeBatched: external_exports.boolean().optional(),
20043
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
20044
+ cursor: external_exports.string().optional()
20045
+ });
20046
+ var ListVaultDerefsResponse = external_exports.object({
20047
+ items: external_exports.array(VaultDeref),
20048
+ nextCursor: external_exports.string().nullable(),
20049
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
20050
+ // this page — it is the count the "N hidden" line and its toggle speak for.
20051
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
20052
+ hiddenBatched: external_exports.number().int().nonnegative()
20053
+ });
19526
20054
  var VaultKeyCustody = external_exports.string();
19527
20055
  var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
19528
20056
  var VAULT_CONSENT_VERSION = 1;
@@ -19902,7 +20430,7 @@ var TopSourcesQuery = external_exports.object({
19902
20430
  // Omit for both kinds.
19903
20431
  kind: external_exports.enum(SOURCE_KINDS).optional()
19904
20432
  });
19905
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
20433
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
19906
20434
  var ScanCoverageProvider = external_exports.object({
19907
20435
  provider: Provider,
19908
20436
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -20214,9 +20742,8 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
20214
20742
  );
20215
20743
 
20216
20744
  // ../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";
20745
+ import { randomUUID as randomUUID10 } from "crypto";
20746
+ import { join as join3, sep } from "path";
20220
20747
  import { DatabaseSync } from "node:sqlite";
20221
20748
 
20222
20749
  // ../packages/persistence/src/ids.ts
@@ -20250,74 +20777,280 @@ function captureId(sessionId, contentHash, filePath = null) {
20250
20777
  );
20251
20778
  }
20252
20779
 
20253
- // ../packages/persistence/src/internal/sql-text.ts
20254
- function escapeLikePattern(s) {
20255
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
20780
+ // ../packages/persistence/src/internal/snapshot.ts
20781
+ import { randomUUID } from "crypto";
20782
+ import { existsSync as existsSync2, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
20783
+ import { basename, dirname as dirname2, join as join2 } from "path";
20784
+
20785
+ // ../packages/persistence/src/paths.ts
20786
+ import {
20787
+ chmodSync,
20788
+ linkSync,
20789
+ lstatSync,
20790
+ mkdirSync,
20791
+ renameSync,
20792
+ rmSync,
20793
+ writeFileSync
20794
+ } from "fs";
20795
+ import { threadId } from "worker_threads";
20796
+ var DATA_DIR_MODE = 448;
20797
+ var DATA_FILE_MODE = 384;
20798
+ var DB_FILENAME = "aka.db";
20799
+ function isSymlink(path) {
20800
+ try {
20801
+ return lstatSync(path).isSymbolicLink();
20802
+ } catch {
20803
+ return false;
20804
+ }
20256
20805
  }
20257
- function placeholders(n) {
20258
- return Array.from({ length: n }, () => "?").join(", ");
20806
+ function chmodBestEffort(path, mode) {
20807
+ if (isSymlink(path)) return;
20808
+ try {
20809
+ chmodSync(path, mode);
20810
+ } catch {
20811
+ }
20259
20812
  }
20260
- function containsPattern(q) {
20261
- return `%${escapeLikePattern(q)}%`;
20813
+ function tightenDir(dir) {
20814
+ chmodBestEffort(dir, DATA_DIR_MODE);
20262
20815
  }
20263
- function likeAny(exprs) {
20264
- return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
20816
+ function ensureDataDirSync(dir) {
20817
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
20818
+ tightenDir(dir);
20265
20819
  }
20266
-
20267
- // ../packages/persistence/src/internal/transactions.ts
20268
- var savepointSeq = 0;
20269
- function withTransaction(db, fn, mode = "DEFERRED") {
20270
- if (db.isTransaction) {
20271
- const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
20272
- db.exec(`SAVEPOINT ${savepoint}`);
20820
+ function dbSidecars(file2) {
20821
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
20822
+ }
20823
+ function tightenFile(file2) {
20824
+ chmodBestEffort(file2, DATA_FILE_MODE);
20825
+ }
20826
+ function tightenPerms(file2) {
20827
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
20828
+ }
20829
+ function writeOwnerOnlyFileSync(file2, data) {
20830
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
20831
+ try {
20832
+ rmSync(tmp, { force: true });
20833
+ } catch {
20834
+ }
20835
+ try {
20836
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20837
+ renameSync(tmp, file2);
20838
+ } finally {
20273
20839
  try {
20274
- fn();
20275
- db.exec(`RELEASE ${savepoint}`);
20276
- } catch (error51) {
20277
- try {
20278
- db.exec(`ROLLBACK TO ${savepoint}`);
20279
- db.exec(`RELEASE ${savepoint}`);
20280
- } catch {
20281
- }
20282
- throw error51;
20840
+ rmSync(tmp, { force: true });
20841
+ } catch {
20283
20842
  }
20284
- return;
20285
20843
  }
20286
- db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
20844
+ tightenFile(file2);
20845
+ }
20846
+ function classifyOccupant(file2) {
20287
20847
  try {
20288
- fn();
20289
- db.exec("COMMIT");
20290
- } catch (error51) {
20848
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
20849
+ return { kind: "gone" };
20850
+ } catch (err) {
20851
+ if (err.code === "ENOENT") return { kind: "gone" };
20852
+ return { kind: "unknown", cause: err };
20853
+ }
20854
+ }
20855
+ var KeyUnclaimableError = class extends Error {
20856
+ code = "key-unclaimable";
20857
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
20858
+ // defines the property anyway, so an error carrying nothing would still answer
20859
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
20860
+ // captured and then lost, which is worse than its plain absence.
20861
+ constructor(message, cause) {
20862
+ super(message, cause === void 0 ? void 0 : { cause });
20863
+ this.name = "KeyUnclaimableError";
20864
+ }
20865
+ };
20866
+ function createOwnerOnlyFileSync(file2, data) {
20867
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
20868
+ try {
20869
+ rmSync(tmp, { force: true });
20870
+ } catch {
20871
+ }
20872
+ let created;
20873
+ try {
20874
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
20875
+ created = publishByLink(tmp, file2, data);
20876
+ } finally {
20291
20877
  try {
20292
- db.exec("ROLLBACK");
20878
+ rmSync(tmp, { force: true });
20293
20879
  } catch {
20294
20880
  }
20295
- throw error51;
20296
20881
  }
20882
+ if (created) tightenFile(file2);
20883
+ return created;
20297
20884
  }
20298
- function failOpenTransaction(db, fn, mode = "DEFERRED") {
20299
- const nested = db.isTransaction;
20885
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
20886
+ function publishByLink(tmp, file2, data) {
20300
20887
  try {
20301
- withTransaction(db, fn, mode);
20888
+ linkSync(tmp, file2);
20302
20889
  return true;
20303
- } catch (error51) {
20304
- if (!db.isTransaction && nested) throw error51;
20305
- return false;
20890
+ } catch (err) {
20891
+ const code = err.code;
20892
+ if (code === "EEXIST") return false;
20893
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
20894
+ }
20895
+ try {
20896
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
20897
+ return true;
20898
+ } catch (err) {
20899
+ if (err.code === "EEXIST") return false;
20900
+ throw err;
20306
20901
  }
20307
20902
  }
20308
20903
 
20309
- // ../packages/persistence/src/internal/warn.ts
20310
- function akaWarn(message) {
20311
- process.stderr.write(`[aka] ${message}
20312
- `);
20904
+ // ../packages/persistence/src/internal/snapshot.ts
20905
+ function backupPath(file2, tag) {
20906
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
20313
20907
  }
20314
-
20315
- // ../packages/persistence/src/db/migrations/introspection.ts
20316
- function evidenceObjects(sql) {
20317
- const objects = [];
20318
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
20319
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
20320
- objects.push({ kind: "table", name: m[1] });
20908
+ var STALE_PARTIAL_MS = 5 * 6e4;
20909
+ function reapStalePartials(file2) {
20910
+ const dir = dirname2(file2);
20911
+ const prefix = `${basename(file2)}.`;
20912
+ let entries;
20913
+ try {
20914
+ entries = readdirSync2(dir);
20915
+ } catch {
20916
+ return;
20917
+ }
20918
+ for (const name of entries) {
20919
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
20920
+ const partial2 = join2(dir, name);
20921
+ try {
20922
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
20923
+ rmSync2(partial2, { force: true });
20924
+ }
20925
+ } catch {
20926
+ }
20927
+ }
20928
+ }
20929
+ function snapshotStore(db, backup) {
20930
+ const partial2 = `${backup}.partial`;
20931
+ try {
20932
+ rmSync2(partial2, { force: true });
20933
+ db.prepare("VACUUM INTO ?").run(partial2);
20934
+ tightenFile(partial2);
20935
+ renameSync2(partial2, backup);
20936
+ } catch (error51) {
20937
+ try {
20938
+ rmSync2(partial2, { force: true });
20939
+ } catch {
20940
+ }
20941
+ throw error51;
20942
+ }
20943
+ }
20944
+ function moveStoreAside(file2, backup) {
20945
+ const undo = [];
20946
+ renameSync2(file2, backup);
20947
+ undo.push([backup, file2]);
20948
+ try {
20949
+ for (const sidecar of dbSidecars(file2)) {
20950
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
20951
+ try {
20952
+ renameSync2(sidecar, moved);
20953
+ undo.push([moved, sidecar]);
20954
+ } catch {
20955
+ rmSync2(sidecar, { force: true });
20956
+ }
20957
+ }
20958
+ } catch (error51) {
20959
+ for (const [from, to] of undo.reverse()) {
20960
+ try {
20961
+ renameSync2(from, to);
20962
+ } catch {
20963
+ }
20964
+ }
20965
+ throw error51;
20966
+ }
20967
+ tightenPerms(backup);
20968
+ }
20969
+ function discardStore(file2, backup) {
20970
+ try {
20971
+ rmSync2(file2, { force: true });
20972
+ for (const sidecar of dbSidecars(file2)) {
20973
+ rmSync2(sidecar, { force: true });
20974
+ }
20975
+ } catch (error51) {
20976
+ if (existsSync2(file2)) {
20977
+ try {
20978
+ rmSync2(backup, { force: true });
20979
+ } catch {
20980
+ }
20981
+ }
20982
+ throw error51;
20983
+ }
20984
+ }
20985
+
20986
+ // ../packages/persistence/src/internal/sql-text.ts
20987
+ function escapeLikePattern(s) {
20988
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
20989
+ }
20990
+ function placeholders(n) {
20991
+ return Array.from({ length: n }, () => "?").join(", ");
20992
+ }
20993
+ function containsPattern(q) {
20994
+ return `%${escapeLikePattern(q)}%`;
20995
+ }
20996
+ function likeAny(exprs) {
20997
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
20998
+ }
20999
+
21000
+ // ../packages/persistence/src/internal/transactions.ts
21001
+ var savepointSeq = 0;
21002
+ function withTransaction(db, fn, mode = "DEFERRED") {
21003
+ if (db.isTransaction) {
21004
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
21005
+ db.exec(`SAVEPOINT ${savepoint}`);
21006
+ try {
21007
+ fn();
21008
+ db.exec(`RELEASE ${savepoint}`);
21009
+ } catch (error51) {
21010
+ try {
21011
+ db.exec(`ROLLBACK TO ${savepoint}`);
21012
+ db.exec(`RELEASE ${savepoint}`);
21013
+ } catch {
21014
+ }
21015
+ throw error51;
21016
+ }
21017
+ return;
21018
+ }
21019
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
21020
+ try {
21021
+ fn();
21022
+ db.exec("COMMIT");
21023
+ } catch (error51) {
21024
+ try {
21025
+ db.exec("ROLLBACK");
21026
+ } catch {
21027
+ }
21028
+ throw error51;
21029
+ }
21030
+ }
21031
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
21032
+ const nested = db.isTransaction;
21033
+ try {
21034
+ withTransaction(db, fn, mode);
21035
+ return true;
21036
+ } catch (error51) {
21037
+ if (!db.isTransaction && nested) throw error51;
21038
+ return false;
21039
+ }
21040
+ }
21041
+
21042
+ // ../packages/persistence/src/internal/warn.ts
21043
+ function akaWarn(message) {
21044
+ process.stderr.write(`[aka] ${message}
21045
+ `);
21046
+ }
21047
+
21048
+ // ../packages/persistence/src/db/migrations/introspection.ts
21049
+ function evidenceObjects(sql) {
21050
+ const objects = [];
21051
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
21052
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
21053
+ objects.push({ kind: "table", name: m[1] });
20321
21054
  }
20322
21055
  }
20323
21056
  for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
@@ -20391,55 +21124,6 @@ function mapRowsTolerant(rows, map2) {
20391
21124
  return out;
20392
21125
  }
20393
21126
 
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
21127
  // ../packages/persistence/src/migrations.ts
20444
21128
  function describeObject(object2) {
20445
21129
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -20555,9 +21239,9 @@ function applyLegacyDropMigration(db, file2) {
20555
21239
  }
20556
21240
  }
20557
21241
  function backupBeforeLegacyDrop(db, file2) {
20558
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
20559
- db.prepare("VACUUM INTO ?").run(backup);
20560
- tightenFile(backup);
21242
+ reapStalePartials(file2);
21243
+ const backup = backupPath(file2, "pre-drop");
21244
+ snapshotStore(db, backup);
20561
21245
  return backup;
20562
21246
  }
20563
21247
  var TOKEN_USAGE_COLUMNS = [
@@ -20901,6 +21585,25 @@ function parseJsonObject(s) {
20901
21585
  return void 0;
20902
21586
  }
20903
21587
 
21588
+ // ../packages/persistence/src/internal/keyset-cursor.ts
21589
+ function encodeKeysetCursor(payload) {
21590
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
21591
+ }
21592
+ function decodeKeysetCursor(cursor) {
21593
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
21594
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
21595
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
21596
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
21597
+ // a null cursor, which a caller reads as "end of list". That is the one
21598
+ // outcome a cursor that does not decode must never produce, since the
21599
+ // documented behaviour above is to restart from the top. (`1e999` is valid
21600
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
21601
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
21602
+ return parsed;
21603
+ }
21604
+ return null;
21605
+ }
21606
+
20904
21607
  // ../packages/persistence/src/repositories/activity.ts
20905
21608
  var DAY_MS = 864e5;
20906
21609
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -20946,16 +21649,6 @@ function utcWindow(nowMs) {
20946
21649
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
20947
21650
  return { startMs, endMs: startMs + DAY_MS };
20948
21651
  }
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
21652
  var DB_EVENT_TYPE_TO_KIND = {
20960
21653
  session: "session",
20961
21654
  prompt: "prompt",
@@ -21100,7 +21793,7 @@ var SqliteActivityRepository = class {
21100
21793
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
21101
21794
  }
21102
21795
  listSessions(query) {
21103
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
21796
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
21104
21797
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
21105
21798
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
21106
21799
  const conditions = [SESSION_ROOT];
@@ -21174,7 +21867,7 @@ var SqliteActivityRepository = class {
21174
21867
  )
21175
21868
  );
21176
21869
  const last = page[page.length - 1];
21177
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
21870
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
21178
21871
  return Promise.resolve({ items, nextCursor, emptyCount });
21179
21872
  }
21180
21873
  getSession(sessionId) {
@@ -22047,7 +22740,7 @@ var SqliteEventsRepository = class {
22047
22740
  };
22048
22741
 
22049
22742
  // ../packages/persistence/src/repositories/exceptions.ts
22050
- import { randomUUID } from "crypto";
22743
+ import { randomUUID as randomUUID2 } from "crypto";
22051
22744
 
22052
22745
  // ../packages/persistence/src/internal/sqlite-errors.ts
22053
22746
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -22083,8 +22776,9 @@ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
22083
22776
  AND conditions IS NULL
22084
22777
  AND ${ACTIVE_PREDICATE}`;
22085
22778
  var SqliteExceptionsRepository = class {
22086
- constructor(db) {
22779
+ constructor(db, now = () => Date.now()) {
22087
22780
  this.db = db;
22781
+ this.now = now;
22088
22782
  this.consumeStmt = db.prepare(
22089
22783
  `UPDATE exceptions
22090
22784
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -22102,6 +22796,7 @@ var SqliteExceptionsRepository = class {
22102
22796
  );
22103
22797
  }
22104
22798
  db;
22799
+ now;
22105
22800
  consumeStmt;
22106
22801
  insertBlockedStmt;
22107
22802
  sweepBlockedStmt;
@@ -22128,8 +22823,8 @@ var SqliteExceptionsRepository = class {
22128
22823
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
22129
22824
  );
22130
22825
  }
22131
- const id = randomUUID();
22132
- const now = Date.now();
22826
+ const id = randomUUID2();
22827
+ const now = this.now();
22133
22828
  try {
22134
22829
  this.insertExceptionRow(id, input, now);
22135
22830
  } catch (err) {
@@ -22207,7 +22902,7 @@ var SqliteExceptionsRepository = class {
22207
22902
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
22208
22903
  const rows = allRows(
22209
22904
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
22210
- opts?.includeTerminal ? {} : { now: Date.now() }
22905
+ opts?.includeTerminal ? {} : { now: this.now() }
22211
22906
  );
22212
22907
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
22213
22908
  return Promise.resolve(exceptions);
@@ -22242,7 +22937,7 @@ var SqliteExceptionsRepository = class {
22242
22937
  * already revoked.
22243
22938
  */
22244
22939
  revoke(id, revokedBy, reason) {
22245
- const now = Date.now();
22940
+ const now = this.now();
22246
22941
  const result = this.db.prepare(
22247
22942
  `UPDATE exceptions
22248
22943
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -22256,7 +22951,7 @@ var SqliteExceptionsRepository = class {
22256
22951
  * callers must treat identically — means it does not and the detection is
22257
22952
  * enforced as usual. Deliberately NOT wrapped in try/catch.
22258
22953
  */
22259
- consume(id, now = Date.now()) {
22954
+ consume(id, now = this.now()) {
22260
22955
  const result = this.consumeStmt.run({ id, now });
22261
22956
  return Promise.resolve(Number(result.changes) === 1);
22262
22957
  }
@@ -22265,7 +22960,7 @@ var SqliteExceptionsRepository = class {
22265
22960
  * version — what rides the policy bundle to the hook. Grants written under
22266
22961
  * a different (rotated-away) key never match, so they are excluded at read.
22267
22962
  */
22268
- activeBundleEntries(keyVersion, now = Date.now()) {
22963
+ activeBundleEntries(keyVersion, now = this.now()) {
22269
22964
  const rows = allRows(
22270
22965
  this.db.prepare(
22271
22966
  `SELECT * FROM exceptions
@@ -22297,7 +22992,7 @@ var SqliteExceptionsRepository = class {
22297
22992
  * than the retention window on every write, so the ledger self-limits.
22298
22993
  */
22299
22994
  recordBlocked(entry) {
22300
- const now = Date.now();
22995
+ const now = this.now();
22301
22996
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
22302
22997
  this.insertBlockedStmt.run({
22303
22998
  reference: entry.reference,
@@ -22320,7 +23015,7 @@ var SqliteExceptionsRepository = class {
22320
23015
  WHERE blocked_at > :cutoff
22321
23016
  ORDER BY blocked_at DESC, rowid DESC`
22322
23017
  ),
22323
- { cutoff: Date.now() - windowMs }
23018
+ { cutoff: this.now() - windowMs }
22324
23019
  );
22325
23020
  return Promise.resolve(
22326
23021
  rows.map((row) => ({
@@ -22348,8 +23043,9 @@ var SqliteExceptionsRepository = class {
22348
23043
  * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
22349
23044
  * grant instead. Fail closed until reveal-side condition evaluation exists.
22350
23045
  */
22351
- activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
23046
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
22352
23047
  try {
23048
+ const at = now ?? this.now();
22353
23049
  const row = getRow(
22354
23050
  this.db.prepare(
22355
23051
  `SELECT id FROM exceptions
@@ -22358,7 +23054,7 @@ var SqliteExceptionsRepository = class {
22358
23054
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22359
23055
  LIMIT 1`
22360
23056
  ),
22361
- { ruleId, valueFingerprint, keyVersion, now }
23057
+ { ruleId, valueFingerprint, keyVersion, now: at }
22362
23058
  );
22363
23059
  return Promise.resolve(row ?? null);
22364
23060
  } catch (err) {
@@ -22372,7 +23068,7 @@ var SqliteExceptionsRepository = class {
22372
23068
  * predicate, so correctness never depends on this sweep; it only bounds how
22373
23069
  * long the audit evidence is kept locally. Returns the deleted count.
22374
23070
  */
22375
- sweepTerminal(retentionMs, now = Date.now()) {
23071
+ sweepTerminal(retentionMs, now = this.now()) {
22376
23072
  const result = this.db.prepare(
22377
23073
  `DELETE FROM exceptions
22378
23074
  WHERE updated_at < :cutoff
@@ -22435,6 +23131,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
22435
23131
 
22436
23132
  // ../packages/persistence/src/repositories/findings.ts
22437
23133
  var PREVIEW_INSTANCES_PER_GROUP = 200;
23134
+ var SCAN_BATCH_ROWS = 1e3;
23135
+ var DEFAULT_LOCATIONS_LIMIT = 100;
23136
+ var LOCATION_RULE_IDS_CAP = 20;
23137
+ function compareLocationOrder(a, b) {
23138
+ return compareFindingGroupOrder(
23139
+ {
23140
+ severity: a.maxSeverity,
23141
+ latestDetectedAt: a.latestDetectedAt,
23142
+ id: ""
23143
+ },
23144
+ {
23145
+ severity: b.maxSeverity,
23146
+ latestDetectedAt: b.latestDetectedAt,
23147
+ id: ""
23148
+ }
23149
+ );
23150
+ }
22438
23151
  var CONCAT_SEP = ",";
22439
23152
  var TUPLE_SEP = "|";
22440
23153
  function splitConcat(value) {
@@ -22447,6 +23160,33 @@ function deriveInstanceStatus(row) {
22447
23160
  latestResolutionStatus: row.latest_status
22448
23161
  });
22449
23162
  }
23163
+ function encodeGroupCursor(group) {
23164
+ const payload = {
23165
+ sev: group.severity,
23166
+ t: group.latestDetectedAt,
23167
+ id: group.id
23168
+ };
23169
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23170
+ }
23171
+ function decodeGroupCursor(cursor) {
23172
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23173
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
23174
+ return {
23175
+ severity: parsed.sev,
23176
+ latestDetectedAt: parsed.t,
23177
+ id: parsed.id
23178
+ };
23179
+ }
23180
+ return null;
23181
+ }
23182
+ function firstAfter(sorted, cursor) {
23183
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
23184
+ return index === -1 ? sorted.length : index;
23185
+ }
23186
+ function findDeepLinked(sorted, page, id) {
23187
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
23188
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
23189
+ }
22450
23190
  var DAY_MS3 = 864e5;
22451
23191
  var SqliteFindingsRepository = class {
22452
23192
  constructor(db) {
@@ -22556,8 +23296,13 @@ var SqliteFindingsRepository = class {
22556
23296
  */
22557
23297
  listGroupedFindings(query) {
22558
23298
  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 } : {};
23299
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
23300
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
23301
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
23302
+ const sessionParams = {
23303
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
23304
+ ...fromMs === void 0 ? {} : { fromMs }
23305
+ };
22561
23306
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
22562
23307
  predicate,
22563
23308
  params: sessionParams
@@ -22565,7 +23310,8 @@ var SqliteFindingsRepository = class {
22565
23310
  const rows = allRows(
22566
23311
  this.db.prepare(
22567
23312
  `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
23313
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
23314
+ kind, finding_key, latest_status
22569
23315
  FROM (
22570
23316
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
22571
23317
  d.severity AS severity, f.masked_match AS masked_match,
@@ -22575,6 +23321,7 @@ var SqliteFindingsRepository = class {
22575
23321
  json_extract(e.attributes, '$.repo') AS repo,
22576
23322
  json_extract(e.attributes, '$.file_path') AS file,
22577
23323
  json_extract(e.attributes, '$.tool_name') AS tool_name,
23324
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
22578
23325
  e.event_type AS kind, f.finding_key AS finding_key,
22579
23326
  latest.status AS latest_status,
22580
23327
  ROW_NUMBER() OVER (
@@ -22606,6 +23353,8 @@ var SqliteFindingsRepository = class {
22606
23353
  repo: r.repo ?? "",
22607
23354
  file: r.file ?? "",
22608
23355
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
23356
+ eventId: r.event_id,
23357
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
22609
23358
  status: deriveInstanceStatus(r)
22610
23359
  }));
22611
23360
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -22629,18 +23378,23 @@ var SqliteFindingsRepository = class {
22629
23378
  groups: sorted.length
22630
23379
  };
22631
23380
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
23381
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
23382
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
23383
+ const page = sorted.slice(start, start + limit);
23384
+ const lastOnPage = page.at(-1);
23385
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
23386
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
22632
23387
  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
- );
23388
+ const narrow = (g) => statusSet ? {
23389
+ ...g,
23390
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
23391
+ } : g;
23392
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
22639
23393
  return Promise.resolve({
22640
23394
  totals,
22641
23395
  facets,
22642
23396
  items,
22643
- nextCursor: null,
23397
+ nextCursor,
22644
23398
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
22645
23399
  });
22646
23400
  }
@@ -22672,6 +23426,266 @@ var SqliteFindingsRepository = class {
22672
23426
  * request actually carries a `q`. (Substring matching is unaffected by a
22673
23427
  * path repeating across tuples.)
22674
23428
  */
23429
+ /**
23430
+ * The instance-level (flat) findings list: one row per finding, newest first,
23431
+ * paged by keyset.
23432
+ *
23433
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
23434
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
23435
+ * them changes no reported number. Severity, subtype, provider, action,
23436
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
23437
+ * facet excludes its own filter, so a row the filter rejects still has to be
23438
+ * counted. Pushing any of them into SQL would silently empty its own facet.
23439
+ * Several could not be expressed there anyway: status comes from the one
23440
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
23441
+ * none of the mappers names", which no IN-list can say.
23442
+ *
23443
+ * The scan runs from the top of the scope on every request, not from the
23444
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
23445
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
23446
+ * while the counting runs, and only the page itself is retained.
23447
+ */
23448
+ listFindingInstances(query) {
23449
+ const opts = {
23450
+ severity: query.severity,
23451
+ subtype: query.subtype,
23452
+ providers: query.provider,
23453
+ actions: query.action,
23454
+ statuses: query.status,
23455
+ tools: query.tool,
23456
+ repo: query.repo,
23457
+ file: query.file,
23458
+ q: query.q
23459
+ };
23460
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
23461
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23462
+ const accumulator = createInstanceFacetAccumulator(opts);
23463
+ const items = [];
23464
+ let total = 0;
23465
+ let last;
23466
+ let hasMore = false;
23467
+ for (const row of this.scanFindingRows({
23468
+ sessionId: query.sessionId,
23469
+ from: query.from
23470
+ })) {
23471
+ accumulator.add(row);
23472
+ if (!matchesInstanceFilters(row, opts)) continue;
23473
+ total += 1;
23474
+ if (items.length < limit) {
23475
+ items.push(toInstanceDetail(row));
23476
+ last = row;
23477
+ } else {
23478
+ hasMore = true;
23479
+ }
23480
+ }
23481
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
23482
+ if (cursor !== null) {
23483
+ const resumed = this.pageAfter(cursor, opts, limit, query);
23484
+ return Promise.resolve({
23485
+ totals: { findings: total },
23486
+ facets: accumulator.facets(),
23487
+ items: resumed.items,
23488
+ nextCursor: resumed.nextCursor
23489
+ });
23490
+ }
23491
+ return Promise.resolve({
23492
+ totals: { findings: total },
23493
+ facets: accumulator.facets(),
23494
+ items,
23495
+ nextCursor
23496
+ });
23497
+ }
23498
+ /**
23499
+ * The page of matching rows strictly after `cursor`. Separate from the
23500
+ * counting pass because that one starts at the top of the scope by design;
23501
+ * this one narrows the scan with the same keyset predicate the activity list
23502
+ * uses, so a later page costs less than the first rather than more.
23503
+ */
23504
+ pageAfter(cursor, opts, limit, query) {
23505
+ const items = [];
23506
+ let last;
23507
+ let hasMore = false;
23508
+ for (const row of this.scanFindingRows({
23509
+ sessionId: query.sessionId,
23510
+ from: query.from,
23511
+ after: cursor
23512
+ })) {
23513
+ if (!matchesInstanceFilters(row, opts)) continue;
23514
+ if (items.length < limit) {
23515
+ items.push(toInstanceDetail(row));
23516
+ last = row;
23517
+ } else {
23518
+ hasMore = true;
23519
+ break;
23520
+ }
23521
+ }
23522
+ return {
23523
+ items,
23524
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
23525
+ };
23526
+ }
23527
+ /**
23528
+ * The same findings folded by location: repository, then file within it.
23529
+ *
23530
+ * The grouping keys come from the capturing event's attributes, which is what
23531
+ * the local store relates a finding to — there is no finding↔asset row to
23532
+ * group by instead. A repo or file the event did not record folds into the
23533
+ * empty-string bucket, which the view renders but does not link, since no
23534
+ * filter can name it.
23535
+ */
23536
+ listFindingLocations(query) {
23537
+ const opts = {
23538
+ severity: query.severity,
23539
+ subtype: query.subtype,
23540
+ providers: query.provider,
23541
+ actions: query.action,
23542
+ statuses: query.status,
23543
+ tools: query.tool,
23544
+ q: query.q
23545
+ };
23546
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
23547
+ const byRepo = /* @__PURE__ */ new Map();
23548
+ let total = 0;
23549
+ for (const row of this.scanFindingRows({
23550
+ sessionId: query.sessionId,
23551
+ from: query.from
23552
+ })) {
23553
+ if (!matchesInstanceFilters(row, opts)) continue;
23554
+ total += 1;
23555
+ let files = byRepo.get(row.repo);
23556
+ if (files === void 0) {
23557
+ files = /* @__PURE__ */ new Map();
23558
+ byRepo.set(row.repo, files);
23559
+ }
23560
+ let acc = files.get(row.file);
23561
+ if (acc === void 0) {
23562
+ acc = newLocationAccumulator();
23563
+ files.set(row.file, acc);
23564
+ }
23565
+ addToLocation(acc, row);
23566
+ }
23567
+ let fileCount = 0;
23568
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
23569
+ fileCount += files.size;
23570
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
23571
+ file: file2,
23572
+ instanceCount: acc.instanceCount,
23573
+ maxSeverity: acc.maxSeverity,
23574
+ latestDetectedAt: acc.latestDetectedAt,
23575
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
23576
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
23577
+ })).sort(compareLocationOrder);
23578
+ const rollup = fileRows.reduce(
23579
+ (a, f) => ({
23580
+ instanceCount: a.instanceCount + f.instanceCount,
23581
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
23582
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
23583
+ }),
23584
+ {
23585
+ instanceCount: 0,
23586
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
23587
+ latestDetectedAt: ""
23588
+ }
23589
+ );
23590
+ const statuses = fileRows.map((f) => f.status);
23591
+ const folded = foldGroupStatus(statuses);
23592
+ return {
23593
+ repo,
23594
+ instanceCount: rollup.instanceCount,
23595
+ maxSeverity: rollup.maxSeverity,
23596
+ latestDetectedAt: rollup.latestDetectedAt,
23597
+ ...folded === void 0 ? {} : { status: folded },
23598
+ files: fileRows
23599
+ };
23600
+ });
23601
+ repos.sort(compareLocationOrder);
23602
+ return Promise.resolve({
23603
+ totals: { findings: total, repos: repos.length, files: fileCount },
23604
+ items: repos.slice(0, limit),
23605
+ hasMore: repos.length > limit
23606
+ });
23607
+ }
23608
+ /**
23609
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
23610
+ *
23611
+ * A generator so a caller streams the scope without it ever being an array:
23612
+ * the flat list counts and facets the whole filtered scope, which on a large
23613
+ * store is far more rows than any page. Each batch advances the same keyset
23614
+ * predicate the page read uses, so the scan is a sequence of bounded reads
23615
+ * rather than one unbounded result set.
23616
+ *
23617
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
23618
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
23619
+ * makes it a point lookup per row, and the derived table would re-materialize
23620
+ * a window over the whole resolution table once per batch.
23621
+ *
23622
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
23623
+ * would be missing from its own facet, which is computed by excluding that
23624
+ * dimension — see listFindingInstances.
23625
+ */
23626
+ *scanFindingRows(scope) {
23627
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
23628
+ const params = [];
23629
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
23630
+ conditions.push("e.root_session_id = ?");
23631
+ params.push(scope.sessionId);
23632
+ }
23633
+ if (scope.from !== void 0) {
23634
+ conditions.push("e.started_at >= ?");
23635
+ params.push(isoToEpochMillis(scope.from));
23636
+ }
23637
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
23638
+ d.severity AS severity, f.masked_match AS masked_match,
23639
+ f.action_taken AS action_taken, f.confidence AS confidence,
23640
+ e.started_at AS occurred_at,
23641
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
23642
+ json_extract(e.attributes, '$.repo') AS repo,
23643
+ json_extract(e.attributes, '$.file_path') AS file,
23644
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
23645
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
23646
+ e.event_type AS kind, f.finding_key AS finding_key,
23647
+ ${latestResolutionStatusSql("f")} AS latest_status
23648
+ FROM inspection_findings f
23649
+ JOIN audit_events e ON e.id = f.audit_event_id
23650
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
23651
+ WHERE ${conditions.join(" AND ")}
23652
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
23653
+ ORDER BY e.started_at DESC, f.id DESC
23654
+ LIMIT ?`;
23655
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
23656
+ for (; ; ) {
23657
+ const rows = allRows(this.db.prepare(sql), [
23658
+ ...params,
23659
+ after.startedAtMs,
23660
+ after.startedAtMs,
23661
+ after.id,
23662
+ SCAN_BATCH_ROWS
23663
+ ]);
23664
+ for (const r of rows) {
23665
+ yield {
23666
+ id: r.id,
23667
+ ruleId: r.rule_id,
23668
+ category: r.category,
23669
+ severity: r.severity,
23670
+ maskedMatch: r.masked_match,
23671
+ actionTaken: r.action_taken,
23672
+ confidence: r.confidence,
23673
+ occurredAt: epochMillisToIso(r.occurred_at),
23674
+ sourceTool: r.source_tool,
23675
+ repo: r.repo ?? "",
23676
+ file: r.file ?? "",
23677
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
23678
+ eventId: r.event_id,
23679
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
23680
+ status: deriveInstanceStatus(r)
23681
+ };
23682
+ }
23683
+ if (rows.length < SCAN_BATCH_ROWS) return;
23684
+ const lastRow = rows[rows.length - 1];
23685
+ if (lastRow === void 0) return;
23686
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
23687
+ }
23688
+ }
22675
23689
  groupAggregates(withSearchText, scope) {
22676
23690
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
22677
23691
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -22932,7 +23946,7 @@ var SqliteInspectionFindingsRepository = class {
22932
23946
  };
22933
23947
 
22934
23948
  // ../packages/persistence/src/repositories/installed-packs.ts
22935
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
23949
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
22936
23950
 
22937
23951
  // ../packages/persistence/src/semver.ts
22938
23952
  function parse4(version2) {
@@ -23083,7 +24097,7 @@ var SqliteInstalledPacksRepository = class {
23083
24097
  let behind = false;
23084
24098
  for (const row of rows) {
23085
24099
  const params = {
23086
- id: randomUUID2(),
24100
+ id: randomUUID3(),
23087
24101
  namespace: row.namespace,
23088
24102
  packId: row.packId,
23089
24103
  version: row.version,
@@ -23095,7 +24109,7 @@ var SqliteInstalledPacksRepository = class {
23095
24109
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
23096
24110
  this.upsertAvailableStmt.run({
23097
24111
  ...params,
23098
- id: randomUUID2(),
24112
+ id: randomUUID3(),
23099
24113
  recordedBy: meta3?.recordedBy ?? null
23100
24114
  });
23101
24115
  } else {
@@ -23418,14 +24432,15 @@ var SqliteInventoryRepository = class {
23418
24432
  };
23419
24433
 
23420
24434
  // ../packages/persistence/src/repositories/inventory-assets.ts
23421
- import { randomUUID as randomUUID3 } from "crypto";
24435
+ import { randomUUID as randomUUID4 } from "crypto";
23422
24436
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
23423
24437
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
23424
24438
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
23425
24439
  var HARNESS_LABELS = {
23426
24440
  claudecode: "Claude Code",
23427
24441
  cursor: "Cursor",
23428
- codex: "Codex"
24442
+ codex: "Codex",
24443
+ antigravity: "Antigravity"
23429
24444
  };
23430
24445
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
23431
24446
  var EMPTY_PROJECT_AGG = {
@@ -23440,6 +24455,7 @@ function resolveHarnessId(attrs, row) {
23440
24455
  if (t.includes("claudecode") || t === "claude") return "claudecode";
23441
24456
  if (t.includes("cursor")) return "cursor";
23442
24457
  if (t.includes("codex")) return "codex";
24458
+ if (t.includes("antigravity")) return "antigravity";
23443
24459
  return null;
23444
24460
  }
23445
24461
  function isLiveRealClaudeCode(rows) {
@@ -23898,7 +24914,7 @@ var SqliteInventoryAssetsRepository = class {
23898
24914
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
23899
24915
  VALUES (:id, :projectId, :path, :access, :now, :now)
23900
24916
  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() });
24917
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
23902
24918
  }
23903
24919
  return true;
23904
24920
  }
@@ -23919,7 +24935,7 @@ var SqliteInventoryAssetsRepository = class {
23919
24935
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
23920
24936
  VALUES (:id, :assetId, :trust, :now, :now)
23921
24937
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
23922
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
24938
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
23923
24939
  }
23924
24940
  this.configRowsCache = void 0;
23925
24941
  return "ok";
@@ -24216,7 +25232,7 @@ var SqliteInventoryAssetsRepository = class {
24216
25232
  };
24217
25233
 
24218
25234
  // ../packages/persistence/src/repositories/policies.ts
24219
- import { randomUUID as randomUUID4 } from "crypto";
25235
+ import { randomUUID as randomUUID5 } from "crypto";
24220
25236
  var SqlitePoliciesRepository = class {
24221
25237
  constructor(db) {
24222
25238
  this.db = db;
@@ -24251,7 +25267,7 @@ var SqlitePoliciesRepository = class {
24251
25267
  failOpenTransaction(this.db, () => {
24252
25268
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
24253
25269
  stmt.run({
24254
- id: randomUUID4(),
25270
+ id: randomUUID5(),
24255
25271
  target: JSON.stringify({ category }),
24256
25272
  action,
24257
25273
  now: Date.now()
@@ -24271,7 +25287,7 @@ var SqlitePoliciesRepository = class {
24271
25287
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
24272
25288
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
24273
25289
  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 });
25290
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
24275
25291
  }
24276
25292
  // Caps every global per-category policy currently set to block/redact down
24277
25293
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -24339,7 +25355,7 @@ var SqlitePolicyCatalogRepository = class {
24339
25355
  };
24340
25356
 
24341
25357
  // ../packages/persistence/src/repositories/project-files.ts
24342
- import { randomUUID as randomUUID5 } from "crypto";
25358
+ import { randomUUID as randomUUID6 } from "crypto";
24343
25359
  var SqliteProjectFilesRepository = class {
24344
25360
  constructor(db) {
24345
25361
  this.db = db;
@@ -24371,7 +25387,7 @@ var SqliteProjectFilesRepository = class {
24371
25387
  const stamp = Math.max(now, maxStamp + 1);
24372
25388
  for (const file2 of scan2.files) {
24373
25389
  this.upsertStmt.run({
24374
- id: randomUUID5(),
25390
+ id: randomUUID6(),
24375
25391
  projectId,
24376
25392
  path: file2.path,
24377
25393
  name: file2.name,
@@ -24385,7 +25401,7 @@ var SqliteProjectFilesRepository = class {
24385
25401
  };
24386
25402
 
24387
25403
  // ../packages/persistence/src/repositories/resolutions.ts
24388
- import { randomUUID as randomUUID6 } from "crypto";
25404
+ import { randomUUID as randomUUID7 } from "crypto";
24389
25405
  var SqliteResolutionsRepository = class {
24390
25406
  constructor(db, now = () => Date.now()) {
24391
25407
  this.db = db;
@@ -24439,7 +25455,7 @@ var SqliteResolutionsRepository = class {
24439
25455
  */
24440
25456
  insertResolution(r) {
24441
25457
  this.insertStmt.run({
24442
- id: randomUUID6(),
25458
+ id: randomUUID7(),
24443
25459
  findingKey: r.findingKey,
24444
25460
  status: FindingStatus.parse(r.status),
24445
25461
  method: ResolutionMethod.parse(r.method),
@@ -24498,13 +25514,51 @@ var SqliteRuleProbeCacheRepository = class {
24498
25514
  this.readStmt = db.prepare(
24499
25515
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
24500
25516
  );
25517
+ this.countQuarantinedStmt = db.prepare(
25518
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
25519
+ );
25520
+ this.clearQuarantinedStmt = db.prepare(
25521
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
25522
+ );
24501
25523
  }
24502
25524
  db;
24503
25525
  upsertStmt;
24504
25526
  readStmt;
25527
+ countQuarantinedStmt;
25528
+ clearQuarantinedStmt;
24505
25529
  getVerdict(ruleKey) {
24506
25530
  return getRow(this.readStmt, { ruleKey });
24507
25531
  }
25532
+ /** How many rules are currently excluded by a cached quarantine verdict. */
25533
+ countQuarantined() {
25534
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
25535
+ }
25536
+ /**
25537
+ * Forgets every quarantine verdict, so the rules behind them are measured
25538
+ * again on the next load. This is the undo for a verdict the machine reached
25539
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
25540
+ * every later scan, and a timing verdict is a wall-clock judgement that a
25541
+ * loaded or slow machine can reach about a rule that is in fact fine.
25542
+ *
25543
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
25544
+ * keeping, and dropping it would make every rule pay the battery again.
25545
+ *
25546
+ * Reports `refused` from the write's own result rather than inferring it from
25547
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
25548
+ * swallows a contended DELETE (another writer holding the lock past
25549
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
25550
+ * leaves the count unchanged, which is indistinguishable from "there was
25551
+ * nothing to clear". An undo that reports success while the quarantines are
25552
+ * still in place is worse than one that fails, because the rules it claimed
25553
+ * to restore are silently still disabled.
25554
+ */
25555
+ clearQuarantined() {
25556
+ const before = this.countQuarantined();
25557
+ const committed = failOpenTransaction(this.db, () => {
25558
+ this.clearQuarantinedStmt.run();
25559
+ });
25560
+ return { refused: !committed, cleared: before - this.countQuarantined() };
25561
+ }
24508
25562
  setVerdict(ruleKey, verdict, worstProbeMs) {
24509
25563
  failOpenTransaction(this.db, () => {
24510
25564
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -24559,7 +25613,39 @@ var SqliteScanLedgerRepository = class {
24559
25613
  };
24560
25614
 
24561
25615
  // ../packages/persistence/src/repositories/secret-vault.ts
24562
- import { randomUUID as randomUUID7 } from "crypto";
25616
+ import { randomUUID as randomUUID8 } from "crypto";
25617
+ function pageLimit(requested, fallback) {
25618
+ if (requested === void 0) return fallback;
25619
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
25620
+ }
25621
+ function encodeReuseCursor(payload) {
25622
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
25623
+ }
25624
+ function decodeReuseCursor(cursor) {
25625
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
25626
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
25627
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
25628
+ // null cursor, which the caller reads as "end of list" — the one outcome a
25629
+ // malformed cursor must never produce, since restarting from the top is the
25630
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
25631
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
25632
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
25633
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
25634
+ }
25635
+ return null;
25636
+ }
25637
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
25638
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
25639
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
25640
+ v.occurrence_count, v.first_seen, v.last_seen`;
25641
+ function toSighting(row) {
25642
+ return {
25643
+ location: row.location,
25644
+ kind: row.kind,
25645
+ firstSeen: new Date(row.first_seen).toISOString(),
25646
+ lastSeen: new Date(row.last_seen).toISOString()
25647
+ };
25648
+ }
24563
25649
  var SELECT_COLUMNS = `
24564
25650
  pointer_id AS pointerId,
24565
25651
  value_fingerprint AS valueFingerprint,
@@ -24743,39 +25829,67 @@ var SqliteSecretVaultRepository = class {
24743
25829
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
24744
25830
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
24745
25831
  ).run({
24746
- id: randomUUID7(),
25832
+ id: randomUUID8(),
24747
25833
  pointerId: entry.pointerId,
24748
25834
  location: entry.location,
24749
25835
  kind: entry.kind,
24750
25836
  now
24751
25837
  });
24752
25838
  }
24753
- listSightings(pointerId) {
25839
+ /**
25840
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
25841
+ * than one query per row. A pointer with no sightings still gets an entry, so
25842
+ * the caller never has to distinguish "none" from "missing".
25843
+ *
25844
+ * The `IN` list is sized to the page, so this statement cannot be cached on
25845
+ * the instance the way the fixed-shape ones in the constructor are.
25846
+ */
25847
+ sightingsFor(pointerIds) {
25848
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
25849
+ if (pointerIds.length === 0) return byPointer;
24754
25850
  const rows = allRows(
24755
25851
  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`
25852
+ `SELECT pointer_id, location, kind, first_seen, last_seen
25853
+ FROM secret_vault_sighting
25854
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
25855
+ ORDER BY last_seen DESC`
24758
25856
  ),
24759
- { pointerId }
25857
+ pointerIds
24760
25858
  );
25859
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
25860
+ return byPointer;
25861
+ }
25862
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
25863
+ toInventoryEntries(rows) {
25864
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
24761
25865
  return rows.map((r) => ({
24762
- location: r.location,
24763
- kind: r.kind,
25866
+ pointerId: r.pointer_id,
25867
+ category: r.category,
25868
+ ...r.provider === null ? {} : { provider: r.provider },
25869
+ maskedMatch: r.masked_match,
25870
+ occurrences: r.occurrence_count,
24764
25871
  firstSeen: new Date(r.first_seen).toISOString(),
24765
- lastSeen: new Date(r.last_seen).toISOString()
25872
+ lastSeen: new Date(r.last_seen).toISOString(),
25873
+ revealGrantId: r.grant_id,
25874
+ sightings: sightings.get(r.pointer_id) ?? []
24766
25875
  }));
24767
25876
  }
24768
25877
  /**
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.
25878
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
25879
+ * value's descriptor data joined with its sightings and the active
25880
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
25881
+ * the fingerprint nor the ciphertext columns are selected.
25882
+ *
25883
+ * `totals.values` counts the whole store, not the page, so the count a reader
25884
+ * sees never depends on how far they have paged.
24773
25885
  */
24774
- listInventory(now = Date.now()) {
25886
+ listInventory(query = {}, now = Date.now()) {
25887
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
25888
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
25889
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
24775
25890
  const rows = allRows(
24776
25891
  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,
25892
+ `SELECT ${INVENTORY_COLUMNS},
24779
25893
  (SELECT e.id FROM exceptions e
24780
25894
  WHERE e.rule_id = v.rule_id
24781
25895
  AND e.value_fingerprint = v.value_fingerprint
@@ -24783,45 +25897,109 @@ var SqliteSecretVaultRepository = class {
24783
25897
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
24784
25898
  LIMIT 1) AS grant_id
24785
25899
  FROM secret_vault v
24786
- ORDER BY v.last_seen DESC`
25900
+ ${where}
25901
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
25902
+ LIMIT :limit`
24787
25903
  ),
24788
- { now }
25904
+ bindParams({
25905
+ now,
25906
+ limit: limit + 1,
25907
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
25908
+ })
24789
25909
  );
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
- }));
25910
+ const hasMore = rows.length > limit;
25911
+ const page = hasMore ? rows.slice(0, limit) : rows;
25912
+ const last = page[page.length - 1];
25913
+ return {
25914
+ totals: { values: this.countEntries() },
25915
+ items: this.toInventoryEntries(page),
25916
+ // Minted from the last row of the PAGE, never the extra probe row.
25917
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
25918
+ };
25919
+ }
25920
+ /**
25921
+ * Values reused on this machine — detected more than once, or written to more
25922
+ * than one location — most-reused first, one page at a time.
25923
+ *
25924
+ * Its own read rather than a filter over an inventory page: reuse is a
25925
+ * property of the whole store, and deriving it from 50 newest rows would
25926
+ * under-report exactly the values a reader most needs to see.
25927
+ */
25928
+ listReuse(query = {}, now = Date.now()) {
25929
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
25930
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
25931
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
25932
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
25933
+ const rows = allRows(
25934
+ this.db.prepare(
25935
+ `SELECT ${INVENTORY_COLUMNS},
25936
+ (SELECT e.id FROM exceptions e
25937
+ WHERE e.rule_id = v.rule_id
25938
+ AND e.value_fingerprint = v.value_fingerprint
25939
+ AND e.key_version = v.fingerprint_key_version
25940
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
25941
+ LIMIT 1) AS grant_id
25942
+ FROM secret_vault v
25943
+ WHERE ${REUSED_PREDICATE} ${after}
25944
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
25945
+ LIMIT :limit`
25946
+ ),
25947
+ bindParams({
25948
+ now,
25949
+ limit: limit + 1,
25950
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
25951
+ })
25952
+ );
25953
+ const hasMore = rows.length > limit;
25954
+ const page = hasMore ? rows.slice(0, limit) : rows;
25955
+ const last = page[page.length - 1];
25956
+ return {
25957
+ totals: { reused: this.countReused() },
25958
+ items: this.toInventoryEntries(page),
25959
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
25960
+ };
24801
25961
  }
24802
25962
  /**
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.
25963
+ * The de-reference trail, newest first, one page at a time. By default the
25964
+ * batched, high-volume reasons (display, view-render) are hidden and counted
25965
+ * instead — the rows that matter as a signal are the model crossings, and
25966
+ * burying them under render noise would defeat the audit's purpose.
25967
+ *
25968
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
25969
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
25970
+ * the reader pages.
24807
25971
  */
24808
- listDerefs(opts) {
24809
- const limit = opts?.limit ?? 200;
24810
- const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
25972
+ listDerefs(query = {}) {
25973
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
25974
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
25975
+ const conditions = [];
25976
+ if (query.includeBatched !== true) {
25977
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
25978
+ }
25979
+ if (cursor !== null) {
25980
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
25981
+ }
25982
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
24811
25983
  const rows = allRows(
24812
25984
  this.db.prepare(
24813
25985
  `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
24814
25986
  FROM secret_vault_deref ${where}
24815
- ORDER BY at DESC, rowid DESC LIMIT :limit`
25987
+ ORDER BY at DESC, id DESC LIMIT :limit`
24816
25988
  ),
24817
- { limit }
25989
+ bindParams({
25990
+ limit: limit + 1,
25991
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
25992
+ })
24818
25993
  );
24819
- const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
25994
+ const hasMore = rows.length > limit;
25995
+ const page = hasMore ? rows.slice(0, limit) : rows;
25996
+ const last = page[page.length - 1];
25997
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
24820
25998
  this.db,
24821
25999
  `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
24822
26000
  );
24823
26001
  return {
24824
- rows: rows.map((r) => ({
26002
+ items: page.map((r) => ({
24825
26003
  id: r.id,
24826
26004
  pointerId: r.pointer_id,
24827
26005
  at: new Date(r.at).toISOString(),
@@ -24831,12 +26009,20 @@ var SqliteSecretVaultRepository = class {
24831
26009
  ...r.grant_id === null ? {} : { grantId: r.grant_id },
24832
26010
  pointerCount: r.pointer_count
24833
26011
  })),
26012
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
24834
26013
  hiddenBatched
24835
26014
  };
24836
26015
  }
24837
26016
  countEntries() {
24838
26017
  return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
24839
26018
  }
26019
+ /** Values reused on this machine — the reuse list's page-independent total. */
26020
+ countReused() {
26021
+ return countScalar(
26022
+ this.db,
26023
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
26024
+ );
26025
+ }
24840
26026
  };
24841
26027
 
24842
26028
  // ../packages/persistence/src/repositories/security.ts
@@ -24851,7 +26037,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
24851
26037
  var SCAN_COVERAGE = [
24852
26038
  { provider: "claudecode", coverage: 100, supported: true },
24853
26039
  { provider: "cursor", coverage: 0, supported: false },
24854
- { provider: "codex", coverage: 0, supported: false },
26040
+ { provider: "codex", coverage: 80, supported: true },
26041
+ { provider: "antigravity", coverage: 60, supported: true },
26042
+ { provider: "claudeai", coverage: 0, supported: false },
24855
26043
  { provider: "chatgpt", coverage: 0, supported: false },
24856
26044
  { provider: "copilot", coverage: 0, supported: false },
24857
26045
  { provider: "api", coverage: 0, supported: false }
@@ -25184,7 +26372,7 @@ var SqliteSecurityRepository = class {
25184
26372
  };
25185
26373
 
25186
26374
  // ../packages/persistence/src/repositories/shares.ts
25187
- import { randomUUID as randomUUID8 } from "crypto";
26375
+ import { randomUUID as randomUUID9 } from "crypto";
25188
26376
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
25189
26377
  var IN_CHUNK = 500;
25190
26378
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -25440,7 +26628,7 @@ var SqliteSharesRepository = class {
25440
26628
  (id, destination_id, host, decision, created_at, updated_at)
25441
26629
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
25442
26630
  ).run({
25443
- id: randomUUID8(),
26631
+ id: randomUUID9(),
25444
26632
  destinationId,
25445
26633
  host: dest.host,
25446
26634
  decision,
@@ -25589,7 +26777,7 @@ var SqliteSharesRepository = class {
25589
26777
  let destinationId = destIds.get(hit.host);
25590
26778
  if (destinationId === void 0) {
25591
26779
  destStmt.run({
25592
- id: randomUUID8(),
26780
+ id: randomUUID9(),
25593
26781
  kind: hit.kind,
25594
26782
  name: hit.name,
25595
26783
  host: hit.host,
@@ -25605,7 +26793,7 @@ var SqliteSharesRepository = class {
25605
26793
  let endpointId = endpointIds.get(endpointKey);
25606
26794
  if (endpointId === void 0) {
25607
26795
  endpointStmt.run({
25608
- id: randomUUID8(),
26796
+ id: randomUUID9(),
25609
26797
  destinationId,
25610
26798
  method: hit.method,
25611
26799
  transport: hit.transport,
@@ -25618,7 +26806,7 @@ var SqliteSharesRepository = class {
25618
26806
  endpointIds.set(endpointKey, endpointId);
25619
26807
  }
25620
26808
  siteStmt.run({
25621
- id: randomUUID8(),
26809
+ id: randomUUID9(),
25622
26810
  endpointId,
25623
26811
  project: input.project,
25624
26812
  projectKey: input.projectKey,
@@ -25983,6 +27171,9 @@ function purgeSampleData(db) {
25983
27171
  }
25984
27172
 
25985
27173
  // ../packages/persistence/src/database.ts
27174
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
27175
+ "aka.persistence.unsafeTestOnlyRawHandle"
27176
+ );
25986
27177
  function linkHost(input, hostId) {
25987
27178
  return hostId ? { ...input, hostId } : input;
25988
27179
  }
@@ -26004,21 +27195,34 @@ function openWithPragmas(file2) {
26004
27195
  }
26005
27196
  return db;
26006
27197
  }
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);
27198
+ function backupLegacyStore(db, file2) {
27199
+ reapStalePartials(file2);
27200
+ const backup = backupPath(file2, "legacy");
27201
+ let snapshotted = false;
27202
+ let snapshotError;
27203
+ try {
27204
+ snapshotStore(db, backup);
27205
+ snapshotted = true;
27206
+ } catch (error51) {
27207
+ snapshotError = error51;
27208
+ } finally {
27209
+ db.close();
27210
+ }
27211
+ if (!snapshotted) {
27212
+ akaWarn(
27213
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
27214
+ );
27215
+ moveStoreAside(file2, backup);
27216
+ return backup;
26013
27217
  }
27218
+ discardStore(file2, backup);
26014
27219
  return backup;
26015
27220
  }
26016
27221
  function openAndInitialize(file2) {
26017
27222
  let db = openWithPragmas(file2);
26018
27223
  try {
26019
27224
  if (isForeignSqliteLineage(db)) {
26020
- db.close();
26021
- const backup = backupLegacyStore(file2);
27225
+ const backup = backupLegacyStore(db, file2);
26022
27226
  db = openWithPragmas(file2);
26023
27227
  akaWarn(
26024
27228
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -26062,7 +27266,7 @@ function openAndInitialize(file2) {
26062
27266
  }
26063
27267
  function openLocalDatabase(dir) {
26064
27268
  ensureDataDirSync(dir);
26065
- const file2 = join2(dir, DB_FILENAME);
27269
+ const file2 = join3(dir, DB_FILENAME);
26066
27270
  const {
26067
27271
  db,
26068
27272
  events,
@@ -26179,7 +27383,7 @@ function openLocalDatabase(dir) {
26179
27383
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
26180
27384
  if (!definitionId) continue;
26181
27385
  inspectionFindings.insertFinding({
26182
- id: randomUUID9(),
27386
+ id: randomUUID10(),
26183
27387
  auditEventId: record2.scanEvent.id,
26184
27388
  inspectionDefinitionId: definitionId,
26185
27389
  span: finding.span,
@@ -26285,8 +27489,224 @@ function openLocalDatabase(dir) {
26285
27489
  transaction,
26286
27490
  close: () => {
26287
27491
  db.close();
27492
+ },
27493
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
27494
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
27495
+ };
27496
+ }
27497
+
27498
+ // ../packages/persistence/src/file-lock.ts
27499
+ import { randomUUID as randomUUID11 } from "crypto";
27500
+ import {
27501
+ closeSync,
27502
+ existsSync as existsSync3,
27503
+ openSync,
27504
+ readFileSync as readFileSync2,
27505
+ rmSync as rmSync3,
27506
+ statSync as statSync2,
27507
+ writeFileSync as writeFileSync2
27508
+ } from "fs";
27509
+ import { hostname as hostname3 } from "os";
27510
+ var LOCK_SUFFIX = ".lock";
27511
+ var DEFAULT_TIMEOUT_MS = 5e3;
27512
+ var DEFAULT_STALE_MS = 2e3;
27513
+ var RETRY_INTERVAL_MS = 5;
27514
+ var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
27515
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
27516
+ function sleepSync(ms) {
27517
+ Atomics.wait(PARK, 0, 0, ms);
27518
+ }
27519
+ var FileLockError = class extends Error {
27520
+ reason;
27521
+ file;
27522
+ holderPid;
27523
+ constructor(reason, file2, detail, holderPid, options) {
27524
+ super(
27525
+ `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
27526
+ options
27527
+ );
27528
+ this.name = "FileLockError";
27529
+ this.reason = reason;
27530
+ this.file = file2;
27531
+ this.holderPid = holderPid;
27532
+ }
27533
+ };
27534
+ function lockPathFor(file2) {
27535
+ return `${file2}${LOCK_SUFFIX}`;
27536
+ }
27537
+ function readLockBody(lock) {
27538
+ let raw;
27539
+ try {
27540
+ raw = readFileSync2(lock, "utf8");
27541
+ } catch {
27542
+ return null;
27543
+ }
27544
+ try {
27545
+ const parsed = JSON.parse(raw);
27546
+ const { pid, token, at, host } = parsed;
27547
+ if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
27548
+ return { pid, token, at, host: typeof host === "string" ? host : "" };
27549
+ } catch {
27550
+ return null;
27551
+ }
27552
+ }
27553
+ function holderIsAlive(pid) {
27554
+ if (!Number.isInteger(pid) || pid <= 0) return false;
27555
+ try {
27556
+ process.kill(pid, 0);
27557
+ return true;
27558
+ } catch (err) {
27559
+ return err.code !== "ESRCH";
27560
+ }
27561
+ }
27562
+ function directoryAcceptsCreates(lock) {
27563
+ const probe = `${lock}.probe-${randomUUID11()}`;
27564
+ try {
27565
+ closeSync(openSync(probe, "wx", DATA_FILE_MODE));
27566
+ return true;
27567
+ } catch {
27568
+ return false;
27569
+ } finally {
27570
+ try {
27571
+ rmSync3(probe, { force: true });
27572
+ } catch {
27573
+ }
27574
+ }
27575
+ }
27576
+ function tryAcquire(lock, file2) {
27577
+ const token = randomUUID11();
27578
+ let fd;
27579
+ try {
27580
+ fd = openSync(lock, "wx", DATA_FILE_MODE);
27581
+ } catch (err) {
27582
+ const code = err.code ?? "";
27583
+ if (code === "EEXIST") return null;
27584
+ if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync3(lock) || directoryAcceptsCreates(lock))) {
27585
+ return null;
27586
+ }
27587
+ throw new FileLockError(
27588
+ "unavailable",
27589
+ file2,
27590
+ err instanceof Error ? err.message : String(err),
27591
+ void 0,
27592
+ { cause: err }
27593
+ );
27594
+ }
27595
+ const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
27596
+ try {
27597
+ writeFileSync2(fd, `${JSON.stringify(body)}
27598
+ `);
27599
+ } catch {
27600
+ try {
27601
+ closeSync(fd);
27602
+ } catch {
27603
+ }
27604
+ rmSync3(lock, { force: true });
27605
+ return null;
27606
+ }
27607
+ try {
27608
+ closeSync(fd);
27609
+ } catch {
27610
+ }
27611
+ return token;
27612
+ }
27613
+ function isAbandoned(body, lock, staleMs) {
27614
+ if (!body) {
27615
+ try {
27616
+ return Date.now() - statSync2(lock).mtimeMs >= staleMs;
27617
+ } catch {
27618
+ return false;
27619
+ }
27620
+ }
27621
+ if (Date.now() - body.at < staleMs) return false;
27622
+ if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
27623
+ return Date.now() - body.at >= abandonWindow(staleMs);
27624
+ }
27625
+ function breakIfStale(lock, staleMs) {
27626
+ const breaker = `${lock}.break`;
27627
+ let fd;
27628
+ try {
27629
+ fd = openSync(breaker, "wx", DATA_FILE_MODE);
27630
+ } catch {
27631
+ reapAbandonedBreaker(breaker);
27632
+ return false;
27633
+ }
27634
+ try {
27635
+ closeSync(fd);
27636
+ } catch {
27637
+ }
27638
+ try {
27639
+ if (!existsSync3(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
27640
+ rmSync3(lock, { force: true });
27641
+ return true;
27642
+ } catch {
27643
+ return false;
27644
+ } finally {
27645
+ try {
27646
+ rmSync3(breaker, { force: true });
27647
+ } catch {
26288
27648
  }
26289
- };
27649
+ }
27650
+ }
27651
+ var BREAKER_ABANDONED_MS = 1e4;
27652
+ function reapAbandonedBreaker(breaker) {
27653
+ try {
27654
+ if (Date.now() - statSync2(breaker).mtimeMs >= BREAKER_ABANDONED_MS) {
27655
+ rmSync3(breaker, { force: true });
27656
+ }
27657
+ } catch {
27658
+ }
27659
+ }
27660
+ function abandonWindow(staleMs) {
27661
+ return Math.max(staleMs * 30, 6e4);
27662
+ }
27663
+ function release(lock, token) {
27664
+ try {
27665
+ if (readLockBody(lock)?.token !== token) return;
27666
+ rmSync3(lock, { force: true });
27667
+ } catch {
27668
+ }
27669
+ }
27670
+ function withFileLock(file2, fn, options = {}) {
27671
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27672
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
27673
+ const lock = lockPathFor(file2);
27674
+ const deadline = Date.now() + timeoutMs;
27675
+ let token = tryAcquire(lock, file2);
27676
+ while (token === null) {
27677
+ if (breakIfStale(lock, staleMs)) {
27678
+ token = tryAcquire(lock, file2);
27679
+ continue;
27680
+ }
27681
+ if (Date.now() >= deadline) {
27682
+ throw new FileLockError(
27683
+ "timeout",
27684
+ file2,
27685
+ `still held after ${String(timeoutMs)}ms`,
27686
+ readLockBody(lock)?.pid
27687
+ );
27688
+ }
27689
+ sleepSync(RETRY_INTERVAL_MS);
27690
+ token = tryAcquire(lock, file2);
27691
+ }
27692
+ try {
27693
+ const result = fn();
27694
+ if (isThenable(result)) {
27695
+ void result.then(
27696
+ () => void 0,
27697
+ () => void 0
27698
+ );
27699
+ throw new TypeError(
27700
+ `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.`
27701
+ );
27702
+ }
27703
+ return result;
27704
+ } finally {
27705
+ release(lock, token);
27706
+ }
27707
+ }
27708
+ function isThenable(value) {
27709
+ return typeof value === "object" && value !== null && typeof value.then === "function";
26290
27710
  }
26291
27711
 
26292
27712
  // ../packages/persistence/src/finding-key.ts
@@ -26301,13 +27721,13 @@ function computeFindingKey(input) {
26301
27721
 
26302
27722
  // ../packages/persistence/src/fingerprint.ts
26303
27723
  import { createHmac, randomBytes } from "crypto";
26304
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
26305
- import { join as join3 } from "path";
27724
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
27725
+ import { join as join4 } from "path";
26306
27726
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
26307
- var KEY_FILENAME = "exception.key";
27727
+ var EXCEPTION_KEY_FILENAME = "exception.key";
26308
27728
  var KEY_MATERIAL_BYTES = 32;
26309
27729
  function keyFilePath(dataDir2) {
26310
- return join3(dataDir2, KEY_FILENAME);
27730
+ return join4(dataDir2, EXCEPTION_KEY_FILENAME);
26311
27731
  }
26312
27732
  function parseKeyFile(raw) {
26313
27733
  const parsed = JSON.parse(raw);
@@ -26345,8 +27765,8 @@ var FloorUnreadableError = class extends Error {
26345
27765
  }
26346
27766
  };
26347
27767
  function storedKeyVersionFloor(dataDir2) {
26348
- const file2 = join3(dataDir2, DB_FILENAME);
26349
- if (!existsSync3(file2)) return 0;
27768
+ const file2 = join4(dataDir2, DB_FILENAME);
27769
+ if (!existsSync4(file2)) return 0;
26350
27770
  let db;
26351
27771
  try {
26352
27772
  db = new DatabaseSync2(file2, { readOnly: true });
@@ -26371,18 +27791,42 @@ function storedKeyVersionFloor(dataDir2) {
26371
27791
  db?.close();
26372
27792
  }
26373
27793
  }
27794
+ function serializeKey(key) {
27795
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
27796
+ }
26374
27797
  function writeKeyFile(dataDir2, key) {
26375
27798
  ensureDataDirSync(dataDir2);
26376
- const file2 = keyFilePath(dataDir2);
26377
- const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
26378
- writeOwnerOnlyFileSync(file2, `${body}
27799
+ writeOwnerOnlyFileSync(keyFilePath(dataDir2), `${serializeKey(key)}
26379
27800
  `);
26380
27801
  return key;
26381
27802
  }
27803
+ function createKeyFile(dataDir2, key) {
27804
+ ensureDataDirSync(dataDir2);
27805
+ const file2 = keyFilePath(dataDir2);
27806
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
27807
+ `)) return key;
27808
+ const winner = readFingerprintKey(dataDir2);
27809
+ if (winner) {
27810
+ tightenFile(file2);
27811
+ return winner;
27812
+ }
27813
+ const occupant = classifyOccupant(file2);
27814
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
27815
+ }
27816
+ function occupantMessage(file2, kind) {
27817
+ switch (kind) {
27818
+ case "symlink":
27819
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
27820
+ case "gone":
27821
+ return "exception key file was removed while it was being created";
27822
+ case "unknown":
27823
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
27824
+ }
27825
+ }
26382
27826
  function readFingerprintKey(dataDir2) {
26383
27827
  let raw;
26384
27828
  try {
26385
- raw = readFileSync2(keyFilePath(dataDir2), "utf8");
27829
+ raw = readFileSync3(keyFilePath(dataDir2), "utf8");
26386
27830
  } catch (err) {
26387
27831
  if (err.code === "ENOENT") return null;
26388
27832
  throw err instanceof Error ? err : new Error(String(err));
@@ -26395,7 +27839,7 @@ function loadOrCreateFingerprintKey(dataDir2) {
26395
27839
  tightenFile(keyFilePath(dataDir2));
26396
27840
  return existing;
26397
27841
  }
26398
- return writeKeyFile(dataDir2, {
27842
+ return createKeyFile(dataDir2, {
26399
27843
  version: storedKeyVersionFloor(dataDir2) + 1,
26400
27844
  material: randomBytes(KEY_MATERIAL_BYTES)
26401
27845
  });
@@ -26421,31 +27865,32 @@ function keyStateOf(key) {
26421
27865
  import { renameSync as renameSync3 } from "fs";
26422
27866
  import { mkdir } from "fs/promises";
26423
27867
  import { homedir as homedir3 } from "os";
26424
- import { join as join4 } from "path";
27868
+ import { join as join5 } from "path";
26425
27869
  function defaultDataDir() {
26426
- return join4(homedir3(), ".aka");
27870
+ return join5(homedir3(), ".aka");
26427
27871
  }
26428
27872
  function settingsDir(base = defaultDataDir()) {
26429
- return join4(base, "settings");
27873
+ return join5(base, "settings");
26430
27874
  }
26431
27875
  function dataDir(base = defaultDataDir()) {
26432
- return join4(base, "data");
27876
+ return join5(base, "data");
26433
27877
  }
26434
27878
  function dbPath(base = defaultDataDir()) {
26435
- return join4(dataDir(base), "aka.db");
27879
+ return join5(dataDir(base), "aka.db");
26436
27880
  }
26437
27881
  function keysDir(base = defaultDataDir()) {
26438
- return join4(base, "keys");
27882
+ return join5(base, "keys");
26439
27883
  }
26440
27884
  function ensureLayoutDirSync(dir = defaultDataDir()) {
26441
27885
  ensureDataDirSync(dir);
26442
27886
  }
26443
27887
 
26444
27888
  // ../packages/persistence/src/settings.ts
26445
- import { readFileSync as readFileSync3 } from "fs";
26446
- import { join as join5 } from "path";
27889
+ import { readFileSync as readFileSync4 } from "fs";
27890
+ import { join as join6 } from "path";
27891
+ var SETTINGS_FILENAME = "settings.json";
26447
27892
  function readWorkspaceSettings(base = defaultDataDir()) {
26448
- const record2 = readJson(join5(settingsDir(base), "settings.json"));
27893
+ const record2 = readJson(join6(settingsDir(base), SETTINGS_FILENAME));
26449
27894
  if (!record2) return defaultWorkspaceSettings();
26450
27895
  try {
26451
27896
  return WorkspaceSettings.parse(record2);
@@ -26456,7 +27901,7 @@ function readWorkspaceSettings(base = defaultDataDir()) {
26456
27901
  function readJson(file2) {
26457
27902
  let text;
26458
27903
  try {
26459
- text = readFileSync3(file2, "utf8");
27904
+ text = readFileSync4(file2, "utf8");
26460
27905
  } catch {
26461
27906
  return null;
26462
27907
  }
@@ -26577,13 +28022,18 @@ import { randomBytes as randomBytes2 } from "crypto";
26577
28022
  import {
26578
28023
  chmodSync as chmodSync2,
26579
28024
  mkdirSync as mkdirSync2,
26580
- readFileSync as readFileSync4,
28025
+ readFileSync as readFileSync5,
26581
28026
  renameSync as renameSync4,
26582
- rmSync as rmSync3,
26583
- statSync,
26584
- writeFileSync as writeFileSync2
28027
+ rmSync as rmSync4,
28028
+ statSync as statSync3,
28029
+ writeFileSync as writeFileSync3
26585
28030
  } from "fs";
26586
- import { join as join6 } from "path";
28031
+ import { join as join7 } from "path";
28032
+ var VAULT_OCCUPANT_REASON = {
28033
+ symlink: "the path is a symlink; remove it so a keyring can be created",
28034
+ gone: "the path was occupied but holds no keyring (removed while it was being created)",
28035
+ unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
28036
+ };
26587
28037
  var VaultKeyEpochMissingError = class extends Error {
26588
28038
  version;
26589
28039
  constructor(version2) {
@@ -26676,28 +28126,28 @@ function claimRotationLock(lock, owner) {
26676
28126
  throw asError(err);
26677
28127
  }
26678
28128
  try {
26679
- writeFileSync2(join6(lock, LOCK_OWNER_FILE), `${owner}
28129
+ writeFileSync3(join7(lock, LOCK_OWNER_FILE), `${owner}
26680
28130
  `, { mode: DATA_FILE_MODE });
26681
28131
  return true;
26682
28132
  } catch (err) {
26683
- rmSync3(lock, { recursive: true, force: true });
28133
+ rmSync4(lock, { recursive: true, force: true });
26684
28134
  throw asError(err);
26685
28135
  }
26686
28136
  }
26687
28137
  function acquireRotationLock(keysDir2) {
26688
- const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
28138
+ const lock = join7(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
26689
28139
  const owner = randomBytes2(16).toString("hex");
26690
28140
  if (claimRotationLock(lock, owner)) return { lock, owner };
26691
28141
  let held;
26692
28142
  try {
26693
- held = statSync(lock);
28143
+ held = statSync3(lock);
26694
28144
  } catch {
26695
28145
  throw new Error(ROTATION_IN_PROGRESS);
26696
28146
  }
26697
28147
  if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
26698
28148
  const aside = `${lock}.stale.${owner}`;
26699
28149
  try {
26700
- const now = statSync(lock);
28150
+ const now = statSync3(lock);
26701
28151
  if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
26702
28152
  throw new Error(ROTATION_IN_PROGRESS);
26703
28153
  }
@@ -26706,17 +28156,17 @@ function acquireRotationLock(keysDir2) {
26706
28156
  if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
26707
28157
  throw new Error(ROTATION_IN_PROGRESS, { cause: err });
26708
28158
  }
26709
- rmSync3(aside, { recursive: true, force: true });
28159
+ rmSync4(aside, { recursive: true, force: true });
26710
28160
  if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
26711
28161
  return { lock, owner };
26712
28162
  }
26713
28163
  function releaseRotationLock(lease) {
26714
28164
  try {
26715
- if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
28165
+ if (readFileSync5(join7(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
26716
28166
  } catch {
26717
28167
  return;
26718
28168
  }
26719
- rmSync3(lease.lock, { recursive: true, force: true });
28169
+ rmSync4(lease.lock, { recursive: true, force: true });
26720
28170
  }
26721
28171
  function withRotationLock(keysDir2, work) {
26722
28172
  ensureDataDirSync(keysDir2);
@@ -26733,7 +28183,7 @@ var FileKeyProvider = class {
26733
28183
  this.#keysDir = keysDir2;
26734
28184
  }
26735
28185
  get filePath() {
26736
- return join6(this.#keysDir, VAULT_KEY_FILENAME);
28186
+ return join7(this.#keysDir, VAULT_KEY_FILENAME);
26737
28187
  }
26738
28188
  loadOrCreate() {
26739
28189
  return asAsync(() => {
@@ -26763,7 +28213,7 @@ var FileKeyProvider = class {
26763
28213
  #read() {
26764
28214
  let raw;
26765
28215
  try {
26766
- raw = readFileSync4(this.filePath, "utf8");
28216
+ raw = readFileSync5(this.filePath, "utf8");
26767
28217
  } catch (err) {
26768
28218
  if (err.code === "ENOENT") return null;
26769
28219
  throw err instanceof Error ? err : new Error(String(err));
@@ -26771,34 +28221,32 @@ var FileKeyProvider = class {
26771
28221
  return parseKeyring(raw);
26772
28222
  }
26773
28223
  /**
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.
28224
+ * First mint: the keyring is CREATED, never replaced, so two processes racing
28225
+ * a fresh machine cannot each mint a different epoch 1 — with tmp + rename
28226
+ * the loser's replace would orphan everything the winner had already sealed.
28227
+ * The loser re-reads and adopts the winner's keyring; it minted nothing.
28228
+ *
28229
+ * `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
28230
+ * at the final path, so the keyring never exists at zero length: a reader —
28231
+ * including the loser, re-reading in order to adopt — sees the file absent or
28232
+ * whole, and never mistakes a live keyring for a corrupt one. A corrupt file
28233
+ * still throws from the parse and is never re-minted over.
26782
28234
  */
26783
28235
  #createExclusive() {
26784
28236
  ensureDataDirSync(this.#keysDir);
26785
28237
  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;
28238
+ if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
28239
+ `)) return keyring;
28240
+ const winner = this.#read();
28241
+ if (!winner) {
28242
+ const occupant = classifyOccupant(this.filePath);
28243
+ throw new KeyUnclaimableError(
28244
+ `vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
28245
+ occupant.cause
28246
+ );
26799
28247
  }
26800
28248
  tightenFileMode(this.filePath);
26801
- return keyring;
28249
+ return winner;
26802
28250
  }
26803
28251
  /**
26804
28252
  * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
@@ -26809,7 +28257,7 @@ var FileKeyProvider = class {
26809
28257
  ensureDataDirSync(this.#keysDir);
26810
28258
  const file2 = this.filePath;
26811
28259
  const tmp = `${file2}.tmp`;
26812
- writeFileSync2(tmp, `${serializeKeyring(keyring)}
28260
+ writeFileSync3(tmp, `${serializeKeyring(keyring)}
26813
28261
  `, { mode: DATA_FILE_MODE });
26814
28262
  renameSync4(tmp, file2);
26815
28263
  tightenFileMode(file2);
@@ -26935,7 +28383,7 @@ function createKeyProvider(custody, keysDir2) {
26935
28383
  }
26936
28384
 
26937
28385
  // ../packages/persistence/src/vault/vault.ts
26938
- import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
28386
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
26939
28387
  var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
26940
28388
  var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
26941
28389
  var VAULT_PURGE_POINTER_ID = "*";
@@ -26962,14 +28410,12 @@ function parsePointer(token) {
26962
28410
  var SecretVault = class {
26963
28411
  #repo;
26964
28412
  #keys;
26965
- #fingerprintKey;
26966
28413
  #isConsented;
26967
28414
  #verifyGrant;
26968
28415
  #now;
26969
28416
  constructor(deps) {
26970
28417
  this.#repo = deps.repo;
26971
28418
  this.#keys = deps.keys;
26972
- this.#fingerprintKey = deps.fingerprintKey;
26973
28419
  this.#isConsented = deps.isConsented;
26974
28420
  this.#verifyGrant = deps.verifyGrant;
26975
28421
  this.#now = deps.now ?? (() => Date.now());
@@ -26978,10 +28424,23 @@ var SecretVault = class {
26978
28424
  * Store a value and return the pointer that stands for it. The same value
26979
28425
  * always yields the same pointer on this machine — one row, one pointer id,
26980
28426
  * one category — which is what makes dedup and reuse counting work.
28427
+ *
28428
+ * `fingerprintKey` is the exception-key epoch this value's fingerprint is
28429
+ * derived under — a different key from the vault's, with different rotation
28430
+ * semantics. It is a parameter of the WRITE rather than a constructor dep,
28431
+ * and a thunk rather than a value, so that the only way to reach a key is to
28432
+ * store something: a read-only caller never names it, and a caller whose
28433
+ * source mints on absence mints only once consent has actually opened the
28434
+ * write. `refreshFingerprints` takes its key the same way, for the same
28435
+ * reason.
28436
+ *
28437
+ * Resolved once per call, so the fingerprint and the version it is recorded
28438
+ * under can never come from two different epochs.
26981
28439
  */
26982
- async tokenize(raw, meta3) {
28440
+ async tokenize(raw, meta3, fingerprintKey) {
26983
28441
  if (!this.#isConsented()) return CONSENT_ABSENT;
26984
- const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
28442
+ const fpKey = fingerprintKey();
28443
+ const valueFingerprint = fingerprintValue(fpKey, raw);
26985
28444
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
26986
28445
  const now = this.#now();
26987
28446
  if (existing) {
@@ -26997,7 +28456,7 @@ var SecretVault = class {
26997
28456
  {
26998
28457
  pointerId: base32Encode(pointerId),
26999
28458
  valueFingerprint,
27000
- fingerprintKeyVersion: this.#fingerprintKey.version,
28459
+ fingerprintKeyVersion: fpKey.version,
27001
28460
  keyVersion: version2,
27002
28461
  // Recorded so the row stays OPENABLE if the wire-format constant ever
27003
28462
  // moves: it is part of this row's AEAD AAD. It is not a tag input —
@@ -27241,7 +28700,7 @@ var SecretVault = class {
27241
28700
  purgeVault() {
27242
28701
  const destroyed = this.#repo.purgeAll();
27243
28702
  this.#repo.recordDeref({
27244
- id: randomUUID10(),
28703
+ id: randomUUID12(),
27245
28704
  pointerId: VAULT_PURGE_POINTER_ID,
27246
28705
  at: this.#now(),
27247
28706
  target: "human",
@@ -27310,7 +28769,7 @@ var SecretVault = class {
27310
28769
  }
27311
28770
  #audit(pointerId, opts, outcome) {
27312
28771
  this.#repo.recordDeref({
27313
- id: randomUUID10(),
28772
+ id: randomUUID12(),
27314
28773
  pointerId,
27315
28774
  at: this.#now(),
27316
28775
  target: opts.target,
@@ -27325,12 +28784,12 @@ var SecretVault = class {
27325
28784
  };
27326
28785
 
27327
28786
  // ../packages/persistence/src/warn-era-cap.ts
27328
- import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
27329
- import { join as join7 } from "path";
28787
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
28788
+ import { join as join8 } from "path";
27330
28789
 
27331
28790
  // ../packages/plugin-sdk/src/config.ts
27332
- import { existsSync as existsSync5 } from "fs";
27333
- import { join as join8 } from "path";
28791
+ import { existsSync as existsSync6 } from "fs";
28792
+ import { join as join9 } from "path";
27334
28793
 
27335
28794
  // ../packages/plugin-sdk/src/provider-env.ts
27336
28795
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -27351,9 +28810,9 @@ var providerEnvShape = {
27351
28810
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
27352
28811
 
27353
28812
  // ../packages/plugin-sdk/src/config-inventory.ts
27354
- import { readdirSync, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
28813
+ import { readdirSync as readdirSync3, readFileSync as readFileSync7, realpathSync, statSync as statSync5 } from "fs";
27355
28814
  import { homedir as homedir4 } from "os";
27356
- import { basename as basename2, join as join10 } from "path";
28815
+ import { basename as basename3, join as join11 } from "path";
27357
28816
 
27358
28817
  // ../rules/code-flaws/auth-jwt-no-verify.json
27359
28818
  var auth_jwt_no_verify_default = {
@@ -29009,7 +30468,7 @@ var gcp_service_account_default = {
29009
30468
  severity: "critical",
29010
30469
  matcher: {
29011
30470
  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",
30471
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
29013
30472
  flags: "g"
29014
30473
  },
29015
30474
  examples: [
@@ -29388,8 +30847,8 @@ function bundledDetections() {
29388
30847
  }
29389
30848
 
29390
30849
  // ../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";
30850
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
30851
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join10, sep as sep2 } from "path";
29393
30852
  function resolveRepoIdentity(cwd) {
29394
30853
  try {
29395
30854
  const root = findGitRoot(cwd);
@@ -29402,7 +30861,7 @@ function resolveRepoIdentity(cwd) {
29402
30861
  // win32) so the persistence layer's `/`-separated checkout-path patterns
29403
30862
  // (the ghost sweep + the read-side worktree filter) match it as written.
29404
30863
  url: url2 ?? headRoot.split(sep2).join("/"),
29405
- name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
30864
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
29406
30865
  };
29407
30866
  } catch {
29408
30867
  return void 0;
@@ -29427,36 +30886,36 @@ function resolveHeadRoot(cwd) {
29427
30886
  function findGitRoot(start) {
29428
30887
  let dir = start;
29429
30888
  for (; ; ) {
29430
- if (existsSync6(join9(dir, ".git"))) return dir;
29431
- const parent = dirname2(dir);
30889
+ if (existsSync7(join10(dir, ".git"))) return dir;
30890
+ const parent = dirname3(dir);
29432
30891
  if (parent === dir) return void 0;
29433
30892
  dir = parent;
29434
30893
  }
29435
30894
  }
29436
30895
  function resolveGitContext(root) {
29437
- const dotGit = join9(root, ".git");
30896
+ const dotGit = join10(root, ".git");
29438
30897
  try {
29439
- if (statSync2(dotGit).isDirectory()) {
29440
- return { configPath: join9(dotGit, "config"), headRoot: root };
30898
+ if (statSync4(dotGit).isDirectory()) {
30899
+ return { configPath: join10(dotGit, "config"), headRoot: root };
29441
30900
  }
29442
30901
  } catch {
29443
30902
  return void 0;
29444
30903
  }
29445
30904
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
29446
30905
  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 };
30906
+ const gitdir = isAbsolute(target) ? target : join10(root, target);
30907
+ if (existsSync7(join10(gitdir, "config"))) {
30908
+ return { configPath: join10(gitdir, "config"), headRoot: root };
29450
30909
  }
29451
- const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
30910
+ const commonRaw = safeRead(join10(gitdir, "commondir"))?.trim();
29452
30911
  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 };
30912
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join10(gitdir, commonRaw);
30913
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
30914
+ return { configPath: join10(commonGitDir, "config"), headRoot };
29456
30915
  }
29457
30916
  function safeRead(path) {
29458
30917
  try {
29459
- return readFileSync5(path, "utf8");
30918
+ return readFileSync6(path, "utf8");
29460
30919
  } catch {
29461
30920
  return void 0;
29462
30921
  }
@@ -29494,18 +30953,23 @@ function slugFromUrl(url2) {
29494
30953
  }
29495
30954
 
29496
30955
  // ../packages/plugin-sdk/src/events.ts
29497
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
30956
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
30957
+
30958
+ // ../packages/plugin-sdk/src/isolated-scan.ts
30959
+ import { existsSync as existsSync8 } from "fs";
30960
+ import { fileURLToPath as fileURLToPath2 } from "url";
30961
+ import { Worker } from "worker_threads";
29498
30962
 
29499
30963
  // ../packages/plugin-sdk/src/inventory-resolver.ts
29500
- import { arch, hostname as hostname3, platform, release } from "os";
30964
+ import { arch, hostname as hostname4, platform, release as release2 } from "os";
29501
30965
 
29502
30966
  // ../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";
30967
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
30968
+ import { join as join12 } from "path";
29505
30969
 
29506
30970
  // ../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";
30971
+ import { readdirSync as readdirSync4, realpathSync as realpathSync2 } from "fs";
30972
+ import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
29509
30973
  function toPosix(path) {
29510
30974
  return path.split(sep3).join("/");
29511
30975
  }
@@ -29515,7 +30979,7 @@ function findProjectRoot(startDir, recognizeMarker) {
29515
30979
  let root = null;
29516
30980
  for (let level = 0; level < MAX_PROJECT_ROOT_LEVELS; level += 1) {
29517
30981
  if (directoryHasMarker(dir, recognizeMarker)) root = dir;
29518
- const parent = dirname3(dir);
30982
+ const parent = dirname4(dir);
29519
30983
  if (parent === dir) break;
29520
30984
  dir = parent;
29521
30985
  }
@@ -29523,7 +30987,7 @@ function findProjectRoot(startDir, recognizeMarker) {
29523
30987
  }
29524
30988
  function directoryHasMarker(dir, recognizeMarker) {
29525
30989
  try {
29526
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
30990
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29527
30991
  if (entry.isFile() && recognizeMarker(entry.name) != null) return true;
29528
30992
  }
29529
30993
  } catch {
@@ -29534,13 +30998,13 @@ function directoryHasMarker(dir, recognizeMarker) {
29534
30998
  function resolveNonGitProject(startDir, recognizeMarker) {
29535
30999
  const projectRoot = findProjectRoot(startDir, recognizeMarker);
29536
31000
  const realRoot = realpathSync2(projectRoot);
29537
- return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename3(realRoot) };
31001
+ return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename4(realRoot) };
29538
31002
  }
29539
31003
 
29540
31004
  // ../packages/plugin-sdk/src/project-files.ts
29541
31005
  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";
31006
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
31007
+ import { basename as basename5, join as join13 } from "path";
29544
31008
  var SKIP_DIRS = /* @__PURE__ */ new Set([
29545
31009
  ".git",
29546
31010
  "node_modules",
@@ -29558,23 +31022,28 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
29558
31022
  ".cache"
29559
31023
  ]);
29560
31024
  var MAX_FILES = 2e4;
29561
- function readIgnoreLayer(dir) {
31025
+ function readIgnoreLayer(dir, anchorLen) {
29562
31026
  try {
29563
- const content = readFileSync8(join12(dir, ".gitignore"), "utf8");
29564
- return { base: dir, matcher: (0, import_ignore.default)().add(content) };
31027
+ const content = readFileSync9(join13(dir, ".gitignore"), "utf8");
31028
+ return { matcher: (0, import_ignore.default)().add(content), anchorLen };
29565
31029
  } catch {
29566
31030
  return void 0;
29567
31031
  }
29568
31032
  }
29569
- function isIgnored(layers, absPath, isDir) {
29570
- let ignored = false;
29571
- for (const layer of layers) {
29572
- const rel = relative(layer.base, absPath).split(sep4).join("/") + (isDir ? "/" : "");
29573
- const verdict = layer.matcher.test(rel);
29574
- if (verdict.ignored) ignored = true;
29575
- else if (verdict.unignored) ignored = false;
31033
+ function relToAnchor(dirRel, anchorLen, name) {
31034
+ if (anchorLen === dirRel.length) return name;
31035
+ return `${dirRel.slice(anchorLen === 0 ? 0 : anchorLen + 1)}/${name}`;
31036
+ }
31037
+ function isIgnored(layers, dirRel, name, isDir) {
31038
+ const suffix = isDir ? "/" : "";
31039
+ for (let i = layers.length - 1; i >= 0; i--) {
31040
+ const layer = layers[i];
31041
+ if (layer === void 0) continue;
31042
+ const verdict = layer.matcher.test(relToAnchor(dirRel, layer.anchorLen, name) + suffix);
31043
+ if (verdict.ignored) return true;
31044
+ if (verdict.unignored) return false;
29576
31045
  }
29577
- return ignored;
31046
+ return false;
29578
31047
  }
29579
31048
  var GENERATED_FILES = /* @__PURE__ */ new Set([
29580
31049
  "pnpm-lock.yaml",
@@ -29627,32 +31096,33 @@ function classifyOrigin(relPath, name) {
29627
31096
  }
29628
31097
  function resolveProjectFiles(cwd) {
29629
31098
  try {
29630
- let visit3 = function(dir, layers) {
31099
+ let visit3 = function(dir, dirRel, layers) {
29631
31100
  let dirents;
29632
31101
  try {
29633
- dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
31102
+ dirents = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
29634
31103
  } catch {
29635
31104
  walk.lostSubtree = true;
29636
31105
  return false;
29637
31106
  }
29638
- const layer = readIgnoreLayer(dir);
31107
+ const layer = readIgnoreLayer(dir, dirRel.length);
29639
31108
  const dirLayers = layer ? [...layers, layer] : layers;
29640
31109
  for (const entry of dirents) {
29641
- const fullPath = join12(dir, entry.name);
29642
31110
  if (entry.isDirectory()) {
29643
- if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
29644
- if (existsSync7(join12(fullPath, ".git"))) continue;
29645
- if (visit3(fullPath, dirLayers)) return true;
31111
+ if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, dirRel, entry.name, true)) continue;
31112
+ const fullPath = join13(dir, entry.name);
31113
+ if (existsSync9(join13(fullPath, ".git"))) continue;
31114
+ const childRel = dirRel === "" ? entry.name : `${dirRel}/${entry.name}`;
31115
+ if (visit3(fullPath, childRel, dirLayers)) return true;
29646
31116
  continue;
29647
31117
  }
29648
31118
  if (!entry.isFile()) continue;
29649
31119
  if (entry.name === ".git") continue;
29650
- if (isIgnored(dirLayers, fullPath, false)) continue;
31120
+ if (isIgnored(dirLayers, dirRel, entry.name, false)) continue;
29651
31121
  if (files.length >= MAX_FILES) return true;
29652
- const relPath = relative(root, fullPath).split(sep4).join("/");
31122
+ const relPath = dirRel === "" ? entry.name : `${dirRel}/${entry.name}`;
29653
31123
  files.push({
29654
31124
  path: relPath,
29655
- name: basename4(entry.name),
31125
+ name: basename5(entry.name),
29656
31126
  origin: classifyOrigin(relPath, entry.name),
29657
31127
  defaultAccess: "approved"
29658
31128
  });
@@ -29666,7 +31136,7 @@ function resolveProjectFiles(cwd) {
29666
31136
  const isLinkedCheckout = resolveHeadRoot(cwd) !== worktree;
29667
31137
  const files = [];
29668
31138
  const walk = { lostSubtree: false };
29669
- const truncated = visit3(root, []) || walk.lostSubtree || isLinkedCheckout;
31139
+ const truncated = visit3(root, "", []) || walk.lostSubtree || isLinkedCheckout;
29670
31140
  if (files.length === 0) return void 0;
29671
31141
  files.sort((a, b) => a.path.localeCompare(b.path));
29672
31142
  return { files, truncated, scannedAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -29675,22 +31145,48 @@ function resolveProjectFiles(cwd) {
29675
31145
  }
29676
31146
  }
29677
31147
 
31148
+ // ../packages/plugin-sdk/src/provider-env-antigravity.ts
31149
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
31150
+ if (typeof v === "string" && v.trim() === "") return void 0;
31151
+ return v;
31152
+ }, external_exports.string().optional()).catch(void 0);
31153
+ var optionalFlag = external_exports.preprocess((v) => {
31154
+ if (typeof v !== "string") return false;
31155
+ const normalized = v.trim().toLowerCase();
31156
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
31157
+ }, external_exports.boolean()).catch(false);
31158
+ var antigravityProviderEnvShape = {
31159
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
31160
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
31161
+ };
31162
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
31163
+
31164
+ // ../packages/plugin-sdk/src/provider-env-codex.ts
31165
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
31166
+ if (typeof v === "string" && v.trim() === "") return void 0;
31167
+ return v;
31168
+ }, external_exports.string().optional()).catch(void 0);
31169
+ var codexProviderEnvShape = {
31170
+ OPENAI_BASE_URL: optionalBaseUrl3
31171
+ };
31172
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
31173
+
29678
31174
  // ../packages/plugin-sdk/src/runtime.ts
29679
- import { randomUUID as randomUUID12 } from "crypto";
31175
+ import { randomUUID as randomUUID14 } from "crypto";
29680
31176
 
29681
31177
  // ../packages/plugin-sdk/src/suppressions.ts
29682
31178
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
29683
31179
 
29684
31180
  // ../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";
31181
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
31182
+ import { join as join14 } from "path";
29687
31183
 
29688
31184
  // ../packages/local-ops/src/egress-record.ts
29689
31185
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29690
31186
  try {
29691
31187
  if (!readWorkspaceSettings(base).dataSharesInPlace) return null;
29692
31188
  const abs = resolve(target);
29693
- const targetDir = statSync5(abs).isDirectory() ? abs : dirname4(abs);
31189
+ const targetDir = statSync7(abs).isDirectory() ? abs : dirname5(abs);
29694
31190
  const identity = resolveRepoIdentity(abs);
29695
31191
  const worktreeRoot = resolveWorktreeRoot(abs);
29696
31192
  let root;
@@ -29712,7 +31208,7 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29712
31208
  }
29713
31209
  const files = [];
29714
31210
  for (const hit of egress.files) {
29715
- const file2 = toPosix(relative2(root, hit.file));
31211
+ const file2 = toPosix(relative(root, hit.file));
29716
31212
  if (file2 === "" || file2.startsWith("../")) continue;
29717
31213
  if (file2.startsWith(".") || file2.includes("/.")) continue;
29718
31214
  files.push({ ...hit, file: file2, vendored: isVendoredPath(file2) });
@@ -29721,7 +31217,7 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29721
31217
  projectKey,
29722
31218
  project,
29723
31219
  projectId,
29724
- reconcile: { mode: "walk", walkedPrefix: toPosix(relative2(root, abs)) },
31220
+ reconcile: { mode: "walk", walkedPrefix: toPosix(relative(root, abs)) },
29725
31221
  hits: resolveEgress(files)
29726
31222
  });
29727
31223
  return { ...summary, project };
@@ -29731,9 +31227,9 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
29731
31227
  }
29732
31228
 
29733
31229
  // ../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";
31230
+ import { createHash as createHash5, randomUUID as randomUUID15 } from "crypto";
31231
+ import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync8 } from "fs";
31232
+ import { basename as basename6, extname, join as join15, relative as relative2, resolve as resolve2 } from "path";
29737
31233
  var import_ignore2 = __toESM(require_ignore(), 1);
29738
31234
  var AKAIGNORE_FILENAME = ".akaignore";
29739
31235
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
@@ -29749,7 +31245,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
29749
31245
  var MAX_BYTES = 1e6;
29750
31246
  function readIgnoreLayer2(dir, filename) {
29751
31247
  try {
29752
- const content = readFileSync9(join14(dir, filename), "utf8");
31248
+ const content = readFileSync10(join15(dir, filename), "utf8");
29753
31249
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
29754
31250
  } catch {
29755
31251
  return void 0;
@@ -29758,7 +31254,7 @@ function readIgnoreLayer2(dir, filename) {
29758
31254
  function evaluate(layers, absPath, isDir) {
29759
31255
  let state = "unmatched";
29760
31256
  for (const layer of layers) {
29761
- const rel = toPosix(relative3(layer.base, absPath)) + (isDir ? "/" : "");
31257
+ const rel = toPosix(relative2(layer.base, absPath)) + (isDir ? "/" : "");
29762
31258
  const verdict = layer.matcher.test(rel);
29763
31259
  if (verdict.ignored) state = "ignored";
29764
31260
  else if (verdict.unignored) state = "unignored";
@@ -29768,7 +31264,7 @@ function evaluate(layers, absPath, isDir) {
29768
31264
  function* collectFiles(target) {
29769
31265
  let st;
29770
31266
  try {
29771
- st = statSync6(target);
31267
+ st = statSync8(target);
29772
31268
  } catch {
29773
31269
  return;
29774
31270
  }
@@ -29784,8 +31280,8 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29784
31280
  const dirMarkLayers = markLayer ? [...markLayers, markLayer] : markLayers;
29785
31281
  const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
29786
31282
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
29787
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29788
- const path = join14(dir, entry.name);
31283
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
31284
+ const path = join15(dir, entry.name);
29789
31285
  if (entry.isDirectory()) {
29790
31286
  const skipState = evaluate(dirSkipLayers, path, true);
29791
31287
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
@@ -29797,7 +31293,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29797
31293
  if (evaluate(dirSkipLayers, path, false) === "ignored") continue;
29798
31294
  let size;
29799
31295
  try {
29800
- size = statSync6(path).size;
31296
+ size = statSync8(path).size;
29801
31297
  } catch {
29802
31298
  continue;
29803
31299
  }
@@ -29811,7 +31307,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29811
31307
  }
29812
31308
  function extractFileEgress(file2, text) {
29813
31309
  if (text.includes("\0")) return null;
29814
- const name = basename5(file2);
31310
+ const name = basename6(file2);
29815
31311
  if (LOCKFILE_BASENAMES.has(name)) return null;
29816
31312
  const kind = manifestKindOf(name);
29817
31313
  const sdkHits = kind === null ? [] : extractManifestSdks(text, kind);
@@ -29819,7 +31315,8 @@ function extractFileEgress(file2, text) {
29819
31315
  if (endpoints.length === 0 && sdkHits.length === 0) return null;
29820
31316
  return { file: file2, vendored: isVendoredPath(toPosix(file2)), endpoints, sdkHits };
29821
31317
  }
29822
- function scanPathIntoStore(db, target, opts = {}) {
31318
+ async function scanPathIntoStore(db, target, opts = {}) {
31319
+ const matchText = opts.scanText ?? ((text) => Promise.resolve(scan(text, opts.rules)));
29823
31320
  let scanned = 0;
29824
31321
  let findingCount = 0;
29825
31322
  const files = [];
@@ -29838,7 +31335,7 @@ function scanPathIntoStore(db, target, opts = {}) {
29838
31335
  for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
29839
31336
  let text;
29840
31337
  try {
29841
- text = readFileSync9(file2, "utf8");
31338
+ text = readFileSync10(file2, "utf8");
29842
31339
  } catch {
29843
31340
  continue;
29844
31341
  }
@@ -29846,9 +31343,9 @@ function scanPathIntoStore(db, target, opts = {}) {
29846
31343
  const fileEgress = extractFileEgress(file2, text);
29847
31344
  if (fileEgress) egressFiles.push(fileEgress);
29848
31345
  const shielded = shieldPointers(text);
29849
- const matches = dropShieldedFindings(scan(shielded.text, opts.rules), shielded.spans);
31346
+ const matches = dropShieldedFindings(await matchText(shielded.text), shielded.spans);
29850
31347
  if (matches.length === 0) continue;
29851
- const eventId = randomUUID13();
31348
+ const eventId = randomUUID15();
29852
31349
  const metadata = { filePath: file2 };
29853
31350
  if (gitignored) metadata.gitignored = true;
29854
31351
  const event = {
@@ -29866,7 +31363,7 @@ function scanPathIntoStore(db, target, opts = {}) {
29866
31363
  const key = resolveFingerprintKey();
29867
31364
  const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
29868
31365
  return {
29869
- id: randomUUID13(),
31366
+ id: randomUUID15(),
29870
31367
  eventId,
29871
31368
  ruleId: m.ruleId,
29872
31369
  category: m.category,
@@ -29891,12 +31388,12 @@ function scanPathIntoStore(db, target, opts = {}) {
29891
31388
  }
29892
31389
 
29893
31390
  // ../packages/local-ops/src/project-inventory.ts
29894
- import { statSync as statSync7 } from "fs";
31391
+ import { statSync as statSync9 } from "fs";
29895
31392
  import { resolve as resolve3 } from "path";
29896
31393
  function recordProjectInventory(db, target) {
29897
31394
  try {
29898
31395
  const abs = resolve3(target);
29899
- statSync7(abs);
31396
+ statSync9(abs);
29900
31397
  const identity = resolveRepoIdentity(abs);
29901
31398
  if (!identity) return null;
29902
31399
  const sourceProjectId2 = db.sourceProject.upsert({
@@ -29945,17 +31442,17 @@ function reinvokeArgv(subcommand, extraArgs = []) {
29945
31442
 
29946
31443
  // ../packages/local-ops/src/update-cache.ts
29947
31444
  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";
31445
+ import { existsSync as existsSync10, readFileSync as readFileSync11, rmSync as rmSync5 } from "fs";
31446
+ import { join as join16 } from "path";
29950
31447
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
29951
31448
  function cachePath(home) {
29952
- return join15(dataDir(home), "update-check.json");
31449
+ return join16(dataDir(home), "update-check.json");
29953
31450
  }
29954
31451
  function readCache(home) {
29955
31452
  const path = cachePath(home);
29956
- if (!existsSync8(path)) return null;
31453
+ if (!existsSync10(path)) return null;
29957
31454
  try {
29958
- const raw = JSON.parse(readFileSync10(path, "utf8"));
31455
+ const raw = JSON.parse(readFileSync11(path, "utf8"));
29959
31456
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
29960
31457
  return raw;
29961
31458
  } catch {
@@ -29963,9 +31460,9 @@ function readCache(home) {
29963
31460
  }
29964
31461
  }
29965
31462
  function writeCache(home, cache) {
29966
- if (!existsSync8(dataDir(home))) return;
31463
+ if (!existsSync10(dataDir(home))) return;
29967
31464
  try {
29968
- writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
31465
+ writeOwnerOnlyFileSync(cachePath(home), `${JSON.stringify(cache, null, 2)}
29969
31466
  `);
29970
31467
  } catch {
29971
31468
  }
@@ -29975,7 +31472,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
29975
31472
  }
29976
31473
  function clearCache(home) {
29977
31474
  try {
29978
- rmSync4(cachePath(home), { force: true });
31475
+ rmSync5(cachePath(home), { force: true });
29979
31476
  } catch {
29980
31477
  }
29981
31478
  }
@@ -30082,9 +31579,9 @@ var COMMAND_SPECS = [
30082
31579
  { name: "stats", summary: "Print findings / enforcement / detections from the local store" },
30083
31580
  {
30084
31581
  name: "detections",
30085
- argHint: "[update]",
31582
+ argHint: "[update|unquarantine]",
30086
31583
  summary: "List installed detection packs + available updates (updates are manual)",
30087
- args: ["update"]
31584
+ args: ["update", "unquarantine"]
30088
31585
  },
30089
31586
  {
30090
31587
  name: "plugins",
@@ -30092,6 +31589,11 @@ var COMMAND_SPECS = [
30092
31589
  args: ["list", "install"]
30093
31590
  },
30094
31591
  { name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
31592
+ {
31593
+ name: "extension",
31594
+ summary: "Install/check the native-messaging host for the browser extension (Chrome)",
31595
+ args: ["install", "status"]
31596
+ },
30095
31597
  {
30096
31598
  name: "exception",
30097
31599
  summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
@@ -30134,13 +31636,14 @@ function commandsHelp() {
30134
31636
  }
30135
31637
 
30136
31638
  // src/commands/check-updates.ts
30137
- import { existsSync as existsSync9 } from "fs";
31639
+ import { existsSync as existsSync11 } from "fs";
30138
31640
  import { parseArgs } from "util";
30139
31641
 
30140
31642
  // src/lib/args.ts
31643
+ import { resolve as resolve4 } from "path";
30141
31644
  var HOME_OPTION = { home: { type: "string" } };
30142
31645
  function homeBase(home) {
30143
- return home ?? defaultDataDir();
31646
+ return home === void 0 ? defaultDataDir() : resolve4(home);
30144
31647
  }
30145
31648
 
30146
31649
  // src/commands/check-updates.ts
@@ -30165,7 +31668,7 @@ function runCheckUpdates(argv) {
30165
31668
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
30166
31669
  `);
30167
31670
  }
30168
- if (existsSync9(dataDir(home))) {
31671
+ if (existsSync11(dataDir(home))) {
30169
31672
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
30170
31673
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
30171
31674
  }
@@ -30319,41 +31822,48 @@ function runCompletion(argv) {
30319
31822
 
30320
31823
  // src/commands/dashboard.ts
30321
31824
  import { spawn as spawn3 } from "child_process";
30322
- import { existsSync as existsSync10 } from "fs";
31825
+ import { existsSync as existsSync12 } from "fs";
30323
31826
  import { createRequire as createRequire2 } from "module";
30324
31827
  import { createServer } from "net";
30325
- import { dirname as dirname5, join as join16 } from "path";
30326
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
31828
+ import { dirname as dirname6, join as join17 } from "path";
31829
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
30327
31830
  import { parseArgs as parseArgs2 } from "util";
30328
31831
 
30329
31832
  // src/lib/open-url.ts
30330
31833
  import { spawn as spawn2 } from "child_process";
30331
- function openUrl(url2) {
30332
- const platform2 = process.platform;
30333
- const command = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
30334
- const args = platform2 === "win32" ? ["/c", "start", "", url2] : [url2];
31834
+ function openUrlCommand(platform2, url2) {
31835
+ if (platform2 === "darwin") return { command: "open", args: [url2] };
31836
+ if (platform2 === "win32") return { command: "cmd", args: ["/c", "start", "", url2] };
31837
+ return { command: "xdg-open", args: [url2] };
31838
+ }
31839
+ var DETACHED = Object.freeze({ stdio: "ignore", detached: true });
31840
+ function openUrl(url2, deps = {}) {
31841
+ const { command, args } = openUrlCommand(deps.platform ?? process.platform, url2);
30335
31842
  try {
30336
- spawn2(command, args, { stdio: "ignore", detached: true }).unref();
31843
+ const child = (deps.spawn ?? spawn2)(command, [...args], DETACHED);
31844
+ child.on("error", () => {
31845
+ });
31846
+ child.unref();
30337
31847
  } catch {
30338
31848
  }
30339
31849
  }
30340
31850
 
30341
31851
  // src/commands/dashboard.ts
30342
31852
  var require3 = createRequire2(import.meta.url);
30343
- var here = dirname5(fileURLToPath2(import.meta.url));
31853
+ var here = dirname6(fileURLToPath3(import.meta.url));
30344
31854
  var BIND_HOST = "127.0.0.1";
30345
31855
  function pkgBase() {
30346
- return isSea() ? dirname5(process.execPath) : join16(here, "..");
31856
+ return isSea() ? dirname6(process.execPath) : join17(here, "..");
30347
31857
  }
30348
31858
  function isPortFree(port) {
30349
- return new Promise((resolve4) => {
31859
+ return new Promise((resolve6) => {
30350
31860
  const probe = createServer();
30351
31861
  probe.once("error", () => {
30352
- resolve4(false);
31862
+ resolve6(false);
30353
31863
  });
30354
31864
  probe.once("listening", () => {
30355
31865
  probe.close(() => {
30356
- resolve4(true);
31866
+ resolve6(true);
30357
31867
  });
30358
31868
  });
30359
31869
  probe.listen(port, BIND_HOST);
@@ -30397,14 +31907,14 @@ async function runDashboard(argv) {
30397
31907
  refreshUpdateMirror(dataDir());
30398
31908
  const base = pkgBase();
30399
31909
  const bundledServer = [
30400
- join16(base, "web-ui", "server.js"),
30401
- join16(base, "web-ui", "web-ui", "server.js")
30402
- ].find((p) => existsSync10(p));
31910
+ join17(base, "web-ui", "server.js"),
31911
+ join17(base, "web-ui", "web-ui", "server.js")
31912
+ ].find((p) => existsSync12(p));
30403
31913
  if (bundledServer) {
30404
31914
  launchStandalone(bundledServer, port, shouldOpen, url2);
30405
31915
  return;
30406
31916
  }
30407
- const webUiDir = dirname5(require3.resolve("@akasecurity/web-ui/package.json"));
31917
+ const webUiDir = dirname6(require3.resolve("@akasecurity/web-ui/package.json"));
30408
31918
  const nextBin = require3.resolve("next/dist/bin/next", { paths: [webUiDir] });
30409
31919
  const startNext = () => {
30410
31920
  const child = spawn3(
@@ -30418,7 +31928,7 @@ async function runDashboard(argv) {
30418
31928
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
30419
31929
  `);
30420
31930
  };
30421
- if (existsSync10(join16(webUiDir, ".next", "BUILD_ID"))) {
31931
+ if (existsSync12(join17(webUiDir, ".next", "BUILD_ID"))) {
30422
31932
  startNext();
30423
31933
  return;
30424
31934
  }
@@ -30441,7 +31951,7 @@ function launchStandalone(serverJs, port, shouldOpen, url2) {
30441
31951
  return;
30442
31952
  }
30443
31953
  const child = spawn3(reinvoke.command, reinvoke.args, {
30444
- cwd: dirname5(serverJs),
31954
+ cwd: dirname6(serverJs),
30445
31955
  stdio: ["inherit", "pipe", "inherit"],
30446
31956
  // Inherit the parent env so the child server sees PATH etc.; PORT/HOSTNAME tell
30447
31957
  // Next's standalone server where to bind (its only port input).
@@ -30484,16 +31994,18 @@ function onReadyOpen(child, shouldOpen, url2, banner) {
30484
31994
 
30485
31995
  // src/commands/detections.ts
30486
31996
  import { parseArgs as parseArgs3 } from "util";
31997
+ var SUBCOMMANDS = ["update", "unquarantine"];
30487
31998
  async function runDetections(argv) {
30488
31999
  const { values, positionals } = parseArgs3({
30489
32000
  args: argv,
30490
32001
  options: { ...HOME_OPTION, all: { type: "boolean" } },
30491
32002
  allowPositionals: true
30492
32003
  });
30493
- const sub = positionals[0] === "update" ? "update" : "list";
32004
+ const named = SUBCOMMANDS.find((s) => s === positionals[0]);
32005
+ const sub = named ?? "list";
30494
32006
  if (sub === "list" && positionals.length > 0) {
30495
32007
  process.stderr.write(
30496
- `aka detections: unknown subcommand '${positionals[0] ?? ""}' (did you mean \`aka detections update\`?)
32008
+ `aka detections: unknown subcommand '${positionals[0] ?? ""}' (expected one of: ${SUBCOMMANDS.join(", ")})
30497
32009
  `
30498
32010
  );
30499
32011
  process.exitCode = 1;
@@ -30505,6 +32017,8 @@ async function runDetections(argv) {
30505
32017
  db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
30506
32018
  if (sub === "update") {
30507
32019
  await runUpdateSub(db, positionals.slice(1), values.all === true);
32020
+ } else if (sub === "unquarantine") {
32021
+ runUnquarantineSub(db);
30508
32022
  } else {
30509
32023
  await runListSub(db);
30510
32024
  }
@@ -30527,6 +32041,15 @@ async function runListSub(db) {
30527
32041
  ${String(items.length)} pack(s) \xB7 ${String(items.reduce((n, i) => n + i.ruleCount, 0))} rule(s) \xB7 ${String(active)} enabled
30528
32042
  `
30529
32043
  );
32044
+ const quarantined = db.ruleProbeCache.countQuarantined();
32045
+ if (quarantined > 0) {
32046
+ out.write(
32047
+ `
32048
+ \u26A0 ${String(quarantined)} rule(s) quarantined for exceeding the ReDoS timing budget \u2014 they are excluded from every scan.
32049
+ aka detections unquarantine # forget the verdicts and measure them again
32050
+ `
32051
+ );
32052
+ }
30530
32053
  if (counts.updates > 0) {
30531
32054
  out.write(
30532
32055
  `
@@ -30539,6 +32062,20 @@ ${String(items.length)} pack(s) \xB7 ${String(items.reduce((n, i) => n + i.ruleC
30539
32062
  out.write("\n\u2713 All detection packs are up to date with this CLI.\n");
30540
32063
  }
30541
32064
  }
32065
+ function runUnquarantineSub(db) {
32066
+ const { refused, cleared } = db.ruleProbeCache.clearQuarantined();
32067
+ if (refused) {
32068
+ process.stderr.write(
32069
+ "aka detections unquarantine: the store refused the write (another process may be holding it). Nothing was cleared \u2014 try again.\n"
32070
+ );
32071
+ process.exitCode = 1;
32072
+ return;
32073
+ }
32074
+ process.stdout.write(
32075
+ 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.
32076
+ `
32077
+ );
32078
+ }
30542
32079
  async function runUpdateSub(db, ids, all) {
30543
32080
  const { items } = await db.detections.listDetections({ filter: "all" });
30544
32081
  const byId = new Map(items.map((i) => [i.id, i]));
@@ -30650,21 +32187,39 @@ function processHiddenChar(state, ch) {
30650
32187
  return { ...state, value: state.value + ch };
30651
32188
  }
30652
32189
  var CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
30653
- function terminalPrompter() {
32190
+ function processIo() {
32191
+ return {
32192
+ input: process.stdin,
32193
+ output: process.stdout,
32194
+ errorOutput: process.stderr,
32195
+ signals: {
32196
+ once: (signal, handler) => {
32197
+ process.once(signal, handler);
32198
+ },
32199
+ off: (signal, handler) => {
32200
+ process.removeListener(signal, handler);
32201
+ },
32202
+ raise: (signal) => {
32203
+ process.kill(process.pid, signal);
32204
+ }
32205
+ }
32206
+ };
32207
+ }
32208
+ function terminalPrompter(io = processIo()) {
30654
32209
  return {
30655
32210
  out: (text) => {
30656
- process.stdout.write(text);
32211
+ io.output.write(text);
30657
32212
  },
30658
32213
  err: (text) => {
30659
- process.stderr.write(text);
32214
+ io.errorOutput.write(text);
30660
32215
  },
30661
- isInteractive: process.stdin.isTTY && process.stdout.isTTY,
32216
+ isInteractive: io.input.isTTY && io.output.isTTY,
30662
32217
  ask(question) {
30663
- return new Promise((resolve4) => {
30664
- const rl = createInterface({ input: process.stdin, output: process.stdout });
32218
+ return new Promise((resolve6) => {
32219
+ const rl = createInterface({ input: io.input, output: io.output });
30665
32220
  rl.question(question, (answer) => {
30666
32221
  rl.close();
30667
- resolve4(answer);
32222
+ resolve6(answer);
30668
32223
  });
30669
32224
  });
30670
32225
  },
@@ -30675,9 +32230,9 @@ function terminalPrompter() {
30675
32230
  // fatal signals: a security tool must never brick the terminal it was
30676
32231
  // invoked from.
30677
32232
  askHidden(question) {
30678
- return new Promise((resolve4, reject) => {
30679
- const stdin = process.stdin;
30680
- process.stdout.write(question);
32233
+ return new Promise((resolve6, reject) => {
32234
+ const stdin = io.input;
32235
+ io.output.write(question);
30681
32236
  stdin.setRawMode(true);
30682
32237
  stdin.resume();
30683
32238
  let state = { value: "", esc: "none" };
@@ -30687,7 +32242,7 @@ function terminalPrompter() {
30687
32242
  restored = true;
30688
32243
  stdin.removeListener("data", onData);
30689
32244
  stdin.removeListener("error", onError);
30690
- for (const sig of CLEANUP_SIGNALS) process.removeListener(sig, onSignal);
32245
+ for (const sig of CLEANUP_SIGNALS) io.signals.off(sig, onSignal);
30691
32246
  try {
30692
32247
  stdin.setRawMode(false);
30693
32248
  } catch {
@@ -30696,16 +32251,16 @@ function terminalPrompter() {
30696
32251
  };
30697
32252
  const finish = (err) => {
30698
32253
  restore();
30699
- process.stdout.write("\n");
32254
+ io.output.write("\n");
30700
32255
  if (err) reject(err);
30701
- else resolve4(state.value);
32256
+ else resolve6(state.value);
30702
32257
  };
30703
32258
  const onError = (err) => {
30704
32259
  finish(err);
30705
32260
  };
30706
32261
  const onSignal = (sig) => {
30707
32262
  restore();
30708
- process.kill(process.pid, sig);
32263
+ io.signals.raise(sig);
30709
32264
  };
30710
32265
  const onData = (chunk) => {
30711
32266
  try {
@@ -30726,12 +32281,12 @@ function terminalPrompter() {
30726
32281
  };
30727
32282
  stdin.on("data", onData);
30728
32283
  stdin.once("error", onError);
30729
- for (const sig of CLEANUP_SIGNALS) process.once(sig, onSignal);
32284
+ for (const sig of CLEANUP_SIGNALS) io.signals.once(sig, onSignal);
30730
32285
  });
30731
32286
  },
30732
32287
  async readAllStdin() {
30733
32288
  const chunks = [];
30734
- for await (const chunk of process.stdin) {
32289
+ for await (const chunk of io.input) {
30735
32290
  chunks.push(chunk);
30736
32291
  }
30737
32292
  return Buffer.concat(chunks).toString("utf8");
@@ -31052,7 +32607,6 @@ async function runApproveReveal(pointer, values, io, base) {
31052
32607
  const vault = new SecretVault({
31053
32608
  repo: db.secretVault,
31054
32609
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31055
- fingerprintKey: loadOrCreateFingerprintKey(dir),
31056
32610
  // Read live so a consent revocation applies to the very next call.
31057
32611
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31058
32612
  });
@@ -31344,11 +32898,11 @@ async function runShow(argv, io) {
31344
32898
  ensurePacks();
31345
32899
  const rule = getLoadedRules().find((r) => r.id === ex.ruleId);
31346
32900
  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
32901
  const label = (name) => ` ${name.padEnd(10)}`;
31349
32902
  const lines = [
32903
+ `${label("Id")}${shortId(ex.id)}`,
31350
32904
  `${label("Rule")}${ruleLine}`,
31351
- `${label("Value")}${ex.maskedValue} fingerprint ${fp} (key v${String(ex.keyVersion)})`,
32905
+ `${label("Value")}${ex.maskedValue} (fingerprint key v${String(ex.keyVersion)})`,
31352
32906
  `${label("Scope")}${scopeSummary(ex)}${ex.maxUses !== null ? ` \xB7 max ${String(ex.maxUses)} use${ex.maxUses === 1 ? "" : "s"}` : ""}`,
31353
32907
  `${label("Reason")}"${ex.justification}"`,
31354
32908
  `${label("Created")}${formatTimestamp(ex.createdAt)} by ${ex.createdBy} via ${viaLabel(ex.createdVia)}`,
@@ -31412,6 +32966,17 @@ async function runRevoke(argv, io) {
31412
32966
  db.close();
31413
32967
  }
31414
32968
  }
32969
+ async function countApprovableBlocked(db, dir) {
32970
+ const key = (() => {
32971
+ try {
32972
+ return keyStateOf(readFingerprintKey(dir));
32973
+ } catch {
32974
+ return { status: "unreadable" };
32975
+ }
32976
+ })();
32977
+ const rows = await db.exceptions.recentBlocked(BLOCKED_DETECTIONS_RETENTION_MS);
32978
+ return rows.filter((row) => isMatchableUnder(row.keyVersion, key)).length;
32979
+ }
31415
32980
  async function runRotateKey(argv, io) {
31416
32981
  const { values } = parseArgs4({
31417
32982
  args: argv,
@@ -31438,6 +33003,10 @@ written under the old key simply stop matching. They remain listed for audit.
31438
33003
  `);
31439
33004
  }
31440
33005
  }
33006
+ io.out(`
33007
+ ${rotationBlockedLedgerNote(await countApprovableBlocked(db, dir))}
33008
+
33009
+ `);
31441
33010
  if (values.yes !== true) {
31442
33011
  if (!io.isInteractive) {
31443
33012
  throw new Error("pass --yes to rotate non-interactively");
@@ -31466,7 +33035,6 @@ Existing grants no longer match; re-approve deliberately where still needed.
31466
33035
  const vault = new SecretVault({
31467
33036
  repo: db.secretVault,
31468
33037
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31469
- fingerprintKey: next,
31470
33038
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31471
33039
  });
31472
33040
  const refreshed = await vault.refreshFingerprints(next);
@@ -31507,14 +33075,208 @@ async function runException(argv, io = terminalPrompter()) {
31507
33075
  await handler(rest, io);
31508
33076
  }
31509
33077
 
33078
+ // src/commands/extension.ts
33079
+ import { execFileSync as execFileSync3 } from "child_process";
33080
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
33081
+ import { createRequire as createRequire3 } from "module";
33082
+ import { homedir as homedir5 } from "os";
33083
+ import { dirname as dirname7, join as join18 } from "path";
33084
+ import { fileURLToPath as fileURLToPath4 } from "url";
33085
+ var require4 = createRequire3(import.meta.url);
33086
+ var here2 = dirname7(fileURLToPath4(import.meta.url));
33087
+ function cliRoot() {
33088
+ return isSea() ? dirname7(process.execPath) : join18(here2, "..");
33089
+ }
33090
+ var EXTENSION_ID = "mdoiaiemcnjnaokmcmgbikcdhgiemdof";
33091
+ var NATIVE_HOST_NAME = "com.akasecurity.aka";
33092
+ function nativeHostManifest(hostPath) {
33093
+ return {
33094
+ name: NATIVE_HOST_NAME,
33095
+ description: "AI Traffic Control native messaging host \u2014 bridges the browser extension to the local ~/.aka SQLite store.",
33096
+ path: hostPath,
33097
+ type: "stdio",
33098
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`]
33099
+ };
33100
+ }
33101
+ function resolveHostScript(root = cliRoot()) {
33102
+ const bundled = join18(root, "native-host", "host.js");
33103
+ if (existsSync13(bundled)) return bundled;
33104
+ try {
33105
+ const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
33106
+ const devBuilt = join18(dirname7(pkgJson), "native-host", "host.js");
33107
+ if (existsSync13(devBuilt)) return devBuilt;
33108
+ } catch {
33109
+ }
33110
+ return null;
33111
+ }
33112
+ function resolveExtensionDist(root = cliRoot()) {
33113
+ const bundled = join18(root, "extension");
33114
+ if (existsSync13(bundled)) return bundled;
33115
+ try {
33116
+ const pkgJson = require4.resolve("@akasecurity/plugin-browser-extension/package.json");
33117
+ const devDist = join18(dirname7(pkgJson), "dist");
33118
+ if (existsSync13(devDist)) return devDist;
33119
+ } catch {
33120
+ }
33121
+ return null;
33122
+ }
33123
+ function chromeManifestDir(platform2 = process.platform) {
33124
+ if (platform2 === "darwin") {
33125
+ return join18(
33126
+ homedir5(),
33127
+ "Library",
33128
+ "Application Support",
33129
+ "Google",
33130
+ "Chrome",
33131
+ "NativeMessagingHosts"
33132
+ );
33133
+ }
33134
+ if (platform2 === "win32") {
33135
+ return join18(homedir5(), ".aka", "native-messaging");
33136
+ }
33137
+ return join18(homedir5(), ".config", "google-chrome", "NativeMessagingHosts");
33138
+ }
33139
+ function resolveNodeForLauncher() {
33140
+ if (!isSea()) return process.execPath;
33141
+ try {
33142
+ const probe = process.platform === "win32" ? "where" : "which";
33143
+ const out = execFileSync3(probe, ["node"], { encoding: "utf8" }).split(/\r?\n/)[0]?.trim();
33144
+ if (out === void 0 || out === "") return null;
33145
+ return out;
33146
+ } catch {
33147
+ return null;
33148
+ }
33149
+ }
33150
+ function launcherPath(manifestDir, platform2) {
33151
+ return join18(
33152
+ manifestDir,
33153
+ platform2 === "win32" ? `${NATIVE_HOST_NAME}.cmd` : `${NATIVE_HOST_NAME}.sh`
33154
+ );
33155
+ }
33156
+ function posixQuote(value) {
33157
+ return `'${value.replaceAll("'", "'\\''")}'`;
33158
+ }
33159
+ function cmdQuote(value) {
33160
+ return `"${value.replaceAll("%", "%%")}"`;
33161
+ }
33162
+ function writeLauncher(manifestDir, hostScript, nodePath, platform2) {
33163
+ const path = launcherPath(manifestDir, platform2);
33164
+ const tmp = `${path}.tmp`;
33165
+ if (platform2 === "win32") {
33166
+ writeFileSync7(tmp, `@echo off\r
33167
+ ${cmdQuote(nodePath)} ${cmdQuote(hostScript)} %*\r
33168
+ `);
33169
+ } else {
33170
+ writeFileSync7(tmp, `#!/bin/sh
33171
+ exec ${posixQuote(nodePath)} ${posixQuote(hostScript)} "$@"
33172
+ `, {
33173
+ mode: 493
33174
+ });
33175
+ }
33176
+ renameSync5(tmp, path);
33177
+ return path;
33178
+ }
33179
+ function windowsRegistryKey() {
33180
+ return `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
33181
+ }
33182
+ function registerWindowsHost(manifestPath) {
33183
+ execFileSync3("reg", [
33184
+ "add",
33185
+ windowsRegistryKey(),
33186
+ "/ve",
33187
+ "/t",
33188
+ "REG_SZ",
33189
+ "/d",
33190
+ manifestPath,
33191
+ "/f"
33192
+ ]);
33193
+ }
33194
+ function runExtension(argv) {
33195
+ const sub = argv[0];
33196
+ if (sub === "install") {
33197
+ runInstall(chromeManifestDir());
33198
+ return;
33199
+ }
33200
+ if (sub === "status") {
33201
+ runStatus(chromeManifestDir());
33202
+ return;
33203
+ }
33204
+ process.stderr.write(
33205
+ `aka extension: unknown subcommand '${sub ?? ""}'
33206
+ Usage: aka extension <install|status>
33207
+ `
33208
+ );
33209
+ process.exitCode = 1;
33210
+ }
33211
+ function runInstall(manifestDir, hostScript = resolveHostScript(), extensionDist = resolveExtensionDist(), nodePath = resolveNodeForLauncher()) {
33212
+ if (!hostScript) {
33213
+ process.stderr.write(
33214
+ "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"
33215
+ );
33216
+ process.exitCode = 1;
33217
+ return;
33218
+ }
33219
+ if (!nodePath) {
33220
+ process.stderr.write(
33221
+ "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"
33222
+ );
33223
+ process.exitCode = 1;
33224
+ return;
33225
+ }
33226
+ mkdirSync5(manifestDir, { recursive: true });
33227
+ const launcher = writeLauncher(manifestDir, hostScript, nodePath, process.platform);
33228
+ const manifestPath = join18(manifestDir, `${NATIVE_HOST_NAME}.json`);
33229
+ const tmp = `${manifestPath}.tmp`;
33230
+ writeFileSync7(tmp, `${JSON.stringify(nativeHostManifest(launcher), null, 2)}
33231
+ `, {
33232
+ mode: DATA_FILE_MODE
33233
+ });
33234
+ renameSync5(tmp, manifestPath);
33235
+ if (process.platform === "win32") {
33236
+ try {
33237
+ registerWindowsHost(manifestPath);
33238
+ } catch (err) {
33239
+ process.stderr.write(
33240
+ `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.
33241
+ `
33242
+ );
33243
+ process.exitCode = 1;
33244
+ return;
33245
+ }
33246
+ }
33247
+ process.stdout.write(
33248
+ `\u2713 Installed the AKA native-messaging host
33249
+ host script: ${hostScript}
33250
+ launcher: ${launcher}
33251
+ manifest: ${manifestPath}
33252
+ ` + (process.platform === "win32" ? ` registry: ${windowsRegistryKey()}
33253
+ ` : "") + `
33254
+ Next: load the extension in Chrome \u2014
33255
+ 1. Open chrome://extensions
33256
+ 2. Enable "Developer mode" (top right)
33257
+ 3. Click "Load unpacked" and select:
33258
+ ${extensionDist ?? "<run: pnpm --filter @akasecurity/plugin-browser-extension build>"}
33259
+ `
33260
+ );
33261
+ }
33262
+ function runStatus(manifestDir) {
33263
+ const manifestPath = join18(manifestDir, `${NATIVE_HOST_NAME}.json`);
33264
+ const installed = existsSync13(manifestPath);
33265
+ process.stdout.write(
33266
+ `native-messaging host: ${installed ? "installed" : "not installed"}
33267
+ manifest: ${manifestPath}
33268
+ ` + (installed ? "" : " run `aka extension install` to set it up\n")
33269
+ );
33270
+ }
33271
+
31510
33272
  // src/commands/init.ts
31511
- import { existsSync as existsSync12, statSync as statSync8 } from "fs";
31512
- import { join as join17 } from "path";
33273
+ import { existsSync as existsSync15, lstatSync as lstatSync2, readdirSync as readdirSync7, readlinkSync, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
33274
+ import { dirname as dirname8, join as join19, resolve as resolve5 } from "path";
31513
33275
  import * as readline from "readline/promises";
31514
33276
  import { parseArgs as parseArgs6 } from "util";
31515
33277
 
31516
33278
  // src/commands/plugins.ts
31517
- import { existsSync as existsSync11 } from "fs";
33279
+ import { existsSync as existsSync14 } from "fs";
31518
33280
  import { parseArgs as parseArgs5 } from "util";
31519
33281
  function runPlugins(argv) {
31520
33282
  const [sub, ...rest] = argv;
@@ -31531,7 +33293,7 @@ async function listPlugins(argv) {
31531
33293
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
31532
33294
  const home = homeBase(values.home);
31533
33295
  const active = /* @__PURE__ */ new Set();
31534
- if (existsSync11(dbPath(home))) {
33296
+ if (existsSync14(dbPath(home))) {
31535
33297
  const db = openLocalDatabase(dataDir(home));
31536
33298
  try {
31537
33299
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -31540,7 +33302,7 @@ async function listPlugins(argv) {
31540
33302
  db.close();
31541
33303
  }
31542
33304
  }
31543
- const installed = installedPluginVersions();
33305
+ const installed = installedAgentPluginVersions();
31544
33306
  const out = process.stdout;
31545
33307
  out.write("Agent plugins (the CLI is an optional hub \u2014 plugins also self-install):\n\n");
31546
33308
  for (const a of AGENT_PLUGINS) {
@@ -31571,42 +33333,44 @@ function installPlugin(argv) {
31571
33333
  return;
31572
33334
  }
31573
33335
  const ref = pluginRef(agent);
31574
- if (!ref) {
33336
+ const cliBin = agent.cliBin;
33337
+ if (!ref || !cliBin) {
31575
33338
  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\`.
33339
+ agent.installHint ? `${agent.installHint}
33340
+ ` : `${agent.name} has no automated install path yet \u2014 install it from the AKA marketplace in ${agent.name}, then run \`aka init\`.
31577
33341
  `
31578
33342
  );
31579
33343
  return;
31580
33344
  }
31581
- if (!claudeAvailable()) {
33345
+ if (!createCliPluginManager(cliBin).available()) {
31582
33346
  process.stdout.write(
31583
33347
  `Installing ${agent.name}\u2026
31584
33348
 
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}
33349
+ The \`${cliBin}\` CLI isn't on your PATH, so I can't install it automatically.
33350
+ Install it from inside ${agent.name}:
33351
+ ${cliBin} plugin marketplace add ${agent.marketplaceSource ?? ""}
33352
+ ${cliBin} plugin install ${ref}
31589
33353
 
31590
33354
  Then run \`aka init\` to set up the local store.
31591
33355
  `
31592
33356
  );
31593
33357
  return;
31594
33358
  }
31595
- process.stdout.write(`Installing ${agent.name} via Claude Code\u2026
33359
+ process.stdout.write(`Installing ${agent.name} via ${cliBin}\u2026
31596
33360
  `);
31597
33361
  const { ok } = installAgentPlugin(agent.id, "inherit");
31598
33362
  if (ok) {
31599
33363
  process.stdout.write(
31600
33364
  `
31601
33365
  \u2713 Installed ${agent.name}.
31602
- \u21BB Restart Claude Code to load it.
33366
+ \u21BB Restart ${agent.name} to load it.
31603
33367
  Run \`aka init\` to scaffold your local store (if you haven't already).
31604
33368
  `
31605
33369
  );
31606
33370
  } else {
31607
33371
  process.stderr.write(
31608
33372
  `
31609
- \u2717 Install failed \u2014 see the output above, or add it in Claude Code with \`/plugin install ${ref}\`.
33373
+ \u2717 Install failed \u2014 see the output above, or add it in ${agent.name} with \`${cliBin} plugin install ${ref}\`.
31610
33374
  `
31611
33375
  );
31612
33376
  process.exitCode = 1;
@@ -31615,40 +33379,159 @@ Then run \`aka init\` to set up the local store.
31615
33379
 
31616
33380
  // src/commands/init.ts
31617
33381
  var PLUGIN_OFFER_IDENTITY = `${PRODUCT_NAME} \u2014 ${PRODUCT_TAGLINE}`;
33382
+ var STORE_DB = "the store database (including the prompt corpus)";
33383
+ var STORE_SETTINGS = "your settings file";
33384
+ function storeContents(home) {
33385
+ return /* @__PURE__ */ new Map([
33386
+ [home, "the store (including the prompt corpus in aka.db)"],
33387
+ [settingsDir(home), STORE_SETTINGS],
33388
+ [dataDir(home), STORE_DB],
33389
+ [keysDir(home), "the vault key"],
33390
+ [join19(settingsDir(home), "settings.json"), STORE_SETTINGS],
33391
+ [dbPath(home), STORE_DB]
33392
+ ]);
33393
+ }
33394
+ function storeTargets(home) {
33395
+ return [...storeContents(home).keys()];
33396
+ }
33397
+ function storeModeTargets(home) {
33398
+ const targets = new Set(storeTargets(home));
33399
+ const data = dataDir(home);
33400
+ try {
33401
+ for (const name of readdirSync7(data)) {
33402
+ if (name.endsWith(".partial")) continue;
33403
+ targets.add(join19(data, name));
33404
+ }
33405
+ } catch {
33406
+ }
33407
+ return [...targets];
33408
+ }
31618
33409
  function looseStorePaths(home) {
31619
33410
  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) => {
33411
+ return storeModeTargets(home).filter((p) => {
31628
33412
  try {
31629
- return (statSync8(p).mode & 63) !== 0;
33413
+ if (lstatSync2(p).isSymbolicLink()) return false;
33414
+ return (statSync10(p).mode & 63) !== 0;
31630
33415
  } catch {
31631
33416
  return false;
31632
33417
  }
31633
33418
  });
31634
33419
  }
33420
+ function symlinkedStorePaths(home, platform2 = process.platform) {
33421
+ return [...storeContents(home)].flatMap(([path, holds]) => {
33422
+ try {
33423
+ if (!lstatSync2(path).isSymbolicLink()) return [];
33424
+ return [
33425
+ {
33426
+ path,
33427
+ target: linkTarget(path),
33428
+ holds,
33429
+ // existsSync follows the link, so a target that is gone reads as
33430
+ // absent here while lstat above still sees the link itself.
33431
+ missing: !existsSync15(path),
33432
+ mode: targetMode(path, platform2)
33433
+ }
33434
+ ];
33435
+ } catch {
33436
+ return [];
33437
+ }
33438
+ });
33439
+ }
33440
+ function linkTarget(path) {
33441
+ try {
33442
+ return realpathSync3(path);
33443
+ } catch {
33444
+ return resolve5(dirname8(path), readlinkSync(path));
33445
+ }
33446
+ }
33447
+ function targetMode(path, platform2) {
33448
+ if (platform2 === "win32") return void 0;
33449
+ try {
33450
+ return statSync10(path).mode & 511;
33451
+ } catch {
33452
+ return void 0;
33453
+ }
33454
+ }
33455
+ function symlinkWarnings(paths, platform2 = process.platform) {
33456
+ const seeAlso = '(see the "Data at rest" note in SECURITY.md)';
33457
+ return paths.map(({ path, target, holds, missing, mode }) => {
33458
+ if (missing) {
33459
+ 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}
33460
+ `;
33461
+ }
33462
+ const inherited = mode === void 0 ? "" : ` (currently ${formatMode(mode)}${(mode & 63) !== 0 ? ", NOT owner-only" : ""})`;
33463
+ const kept = platform2 === "win32" ? "" : "permissions are never changed through a symlink, so ";
33464
+ const under = platform2 === "win32" ? "" : " under that target's own permissions";
33465
+ return ` \u26A0 ${path} is a symlink to ${target}${inherited} \u2014 ${kept}${holds} is written there${under} ${seeAlso}
33466
+ `;
33467
+ }).join("");
33468
+ }
33469
+ function formatMode(mode) {
33470
+ return `0${mode.toString(8).padStart(3, "0")}`;
33471
+ }
33472
+ function assertStoreLinksResolve(home) {
33473
+ for (const dir of [home, settingsDir(home), dataDir(home)]) {
33474
+ if (!isBrokenLink(dir)) continue;
33475
+ throw new Error(
33476
+ `${dir} is a symlink to ${linkTarget(dir)}, which does not exist \u2014 create that target or remove the link, then re-run \`aka init\``
33477
+ );
33478
+ }
33479
+ }
33480
+ function isBrokenLink(path) {
33481
+ try {
33482
+ return lstatSync2(path).isSymbolicLink() && !existsSync15(path);
33483
+ } catch {
33484
+ return false;
33485
+ }
33486
+ }
33487
+ function assertStorePathsAreDirectories(home) {
33488
+ for (const dir of [home, settingsDir(home), dataDir(home), keysDir(home)]) {
33489
+ if (!existsAsNonDirectory(dir)) continue;
33490
+ 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";
33491
+ throw new Error(`${dir} ${occupant}. AKA keeps its store there; re-run \`aka init\` after.`);
33492
+ }
33493
+ }
33494
+ function isSymlink2(path) {
33495
+ try {
33496
+ return lstatSync2(path).isSymbolicLink();
33497
+ } catch {
33498
+ return false;
33499
+ }
33500
+ }
33501
+ function existsAsNonDirectory(path) {
33502
+ try {
33503
+ return !statSync10(path).isDirectory();
33504
+ } catch {
33505
+ return false;
33506
+ }
33507
+ }
31635
33508
  async function runInit(argv) {
31636
33509
  const { values } = parseArgs6({
31637
33510
  args: argv,
31638
33511
  options: { ...HOME_OPTION, yes: { type: "boolean", short: "y" } }
31639
33512
  });
31640
33513
  const home = homeBase(values.home);
33514
+ assertStoreLinksResolve(home);
33515
+ assertStorePathsAreDirectories(home);
31641
33516
  ensureLayoutDirSync(home);
31642
33517
  const settings = settingsDir(home);
31643
33518
  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)}
33519
+ const settingsFile = join19(settings, "settings.json");
33520
+ let settingsCreated = false;
33521
+ if (!existsSync15(settingsFile)) {
33522
+ try {
33523
+ settingsCreated = withFileLock(settingsFile, () => {
33524
+ if (existsSync15(settingsFile)) return false;
33525
+ writeOwnerOnlyFileSync(
33526
+ settingsFile,
33527
+ `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
31650
33528
  `
31651
- );
33529
+ );
33530
+ return true;
33531
+ });
33532
+ } catch (err) {
33533
+ if (!(err instanceof FileLockError) || err.reason !== "timeout") throw err;
33534
+ }
31652
33535
  }
31653
33536
  tightenFile(settingsFile);
31654
33537
  const db = openLocalDatabase(dataDir(home));
@@ -31664,14 +33547,15 @@ async function runInit(argv) {
31664
33547
  db.close();
31665
33548
  }
31666
33549
  const loose = looseStorePaths(home);
33550
+ const symlinked = symlinkedStorePaths(home);
31667
33551
  process.stdout.write(
31668
33552
  `\u2713 Initialized AKA at ${home}
31669
- settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
33553
+ settings: ${settingsFile}${settingsCreated ? "" : existsSync15(settingsFile) ? " (kept existing)" : " (not written \u2014 another process was writing it)"}
31670
33554
  database: ${dbPath(home)}
31671
33555
  seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
31672
33556
  ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
31673
33557
  ` : "") + (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
- ` : "")
33558
+ ` : "") + symlinkWarnings(symlinked)
31675
33559
  );
31676
33560
  await offerPluginInstall(values.yes === true);
31677
33561
  }
@@ -31716,7 +33600,7 @@ async function confirm(question) {
31716
33600
  }
31717
33601
 
31718
33602
  // src/commands/scan.ts
31719
- import { statSync as statSync9 } from "fs";
33603
+ import { statSync as statSync11 } from "fs";
31720
33604
  import { parseArgs as parseArgs7 } from "util";
31721
33605
  function severityRank(s) {
31722
33606
  return Severity.options.indexOf(s);
@@ -31730,7 +33614,7 @@ function renderEgressLine(egress) {
31730
33614
  const cap = egress.truncated ? ` (capped at ${String(MAX_EGRESS_CALL_SITES_PER_PROJECT)} call sites)` : "";
31731
33615
  return `Data shares: ${String(egress.destinations)} destination(s) \xB7 ${String(egress.endpoints)} endpoint(s) \xB7 ${String(egress.callSites)} call site(s)${cap}`;
31732
33616
  }
31733
- function runScan(argv) {
33617
+ async function runScan(argv) {
31734
33618
  const { values, positionals } = parseArgs7({
31735
33619
  args: argv,
31736
33620
  options: {
@@ -31763,7 +33647,7 @@ function runScan(argv) {
31763
33647
  failOn = parsed.data;
31764
33648
  }
31765
33649
  try {
31766
- statSync9(target);
33650
+ statSync11(target);
31767
33651
  } catch {
31768
33652
  process.stderr.write(`aka scan: no such file or directory: ${target}
31769
33653
  `);
@@ -31778,7 +33662,11 @@ function runScan(argv) {
31778
33662
  let egress;
31779
33663
  try {
31780
33664
  const { ruleActions } = db.installedPacks.installedRuleset();
31781
- result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli", dataDir: storeDir });
33665
+ result = await scanPathIntoStore(db, target, {
33666
+ ruleActions,
33667
+ sourceTool: "cli",
33668
+ dataDir: storeDir
33669
+ });
31782
33670
  inventory = recordProjectInventory(db, target);
31783
33671
  egress = recordProjectEgress(db, target, result.egress, home);
31784
33672
  } finally {
@@ -32649,19 +34537,20 @@ function applyCliUpdate2() {
32649
34537
  function applyPluginUpdate2(status) {
32650
34538
  const agent = findAgent(status.id);
32651
34539
  const ref = agent ? pluginRef(agent) : void 0;
32652
- if (!agent || !ref) {
34540
+ const cliBin = agent?.cliBin;
34541
+ if (!agent || !ref || !cliBin) {
32653
34542
  process.stderr.write(`\u2717 ${status.name}: no update coordinates in the registry.
32654
34543
  `);
32655
34544
  return false;
32656
34545
  }
32657
- if (!claudeAvailable()) {
34546
+ if (!createCliPluginManager(cliBin).available()) {
32658
34547
  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}\`.
34548
+ `\u2717 ${status.name}: the \`${cliBin}\` CLI isn't on your PATH \u2014 install ${agent.name}, then run \`${cliBin} plugin update ${ref}\`.
32660
34549
  `
32661
34550
  );
32662
34551
  return false;
32663
34552
  }
32664
- process.stdout.write(`Updating ${status.name} (claude plugin update ${ref})\u2026
34553
+ process.stdout.write(`Updating ${status.name} (${cliBin} plugin update ${ref})\u2026
32665
34554
  `);
32666
34555
  const { ok } = applyPluginUpdate(status.id, "inherit");
32667
34556
  process.stdout.write(ok ? `\u2713 ${status.name} updated.
@@ -32719,7 +34608,6 @@ async function runShow2(argv, io) {
32719
34608
  const vault = new SecretVault({
32720
34609
  repo: db.secretVault,
32721
34610
  keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
32722
- fingerprintKey: loadOrCreateFingerprintKey(dir),
32723
34611
  // Read live so a consent revocation applies to the very next call.
32724
34612
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
32725
34613
  });
@@ -32807,6 +34695,7 @@ var COMMANDS = {
32807
34695
  detections: runDetections,
32808
34696
  plugins: runPlugins,
32809
34697
  dashboard: runDashboard,
34698
+ extension: runExtension,
32810
34699
  exception: (argv) => runException(argv),
32811
34700
  vault: (argv) => runVault(argv),
32812
34701
  tui: runTui,