@akasecurity/ai-tc-claude-code 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -492,13 +492,12 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync4 } from "fs";
496
- import { join as join7 } from "path";
495
+ import { existsSync as existsSync5 } from "fs";
496
+ import { join as join8 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/database.ts
499
- import { randomUUID as randomUUID9 } from "crypto";
500
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
501
- import { join, sep } from "path";
499
+ import { randomUUID as randomUUID10 } from "crypto";
500
+ import { join as join2, sep } from "path";
502
501
  import { DatabaseSync } from "node:sqlite";
503
502
 
504
503
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -578,6 +577,14 @@ var SQLITE_MIGRATIONS = [
578
577
  {
579
578
  tag: "0018_serious_tana_nile",
580
579
  sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
580
+ },
581
+ {
582
+ tag: "0019_audit_started_at_index",
583
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
584
+ },
585
+ {
586
+ tag: "0020_secret_vault_pagination_indexes",
587
+ 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');"
581
588
  }
582
589
  ];
583
590
 
@@ -15315,7 +15322,17 @@ var Finding = external_exports.object({
15315
15322
  }).meta({ id: "Finding" });
15316
15323
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15317
15324
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15318
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15325
+ var FindingProvider = external_exports.enum([
15326
+ "claudecode",
15327
+ "claudedesktop",
15328
+ "cursor",
15329
+ "copilot",
15330
+ "chatgpt",
15331
+ "claudeai",
15332
+ "codex",
15333
+ "antigravity",
15334
+ "api"
15335
+ ]).meta({ id: "FindingProvider" });
15319
15336
  var FindingCategory = external_exports.enum([
15320
15337
  "secret",
15321
15338
  "pii",
@@ -15369,7 +15386,16 @@ var FindingInstance = external_exports.object({
15369
15386
  confidence: external_exports.number().min(0).max(1),
15370
15387
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15371
15388
  // that predate the resolution feature stay valid.
15372
- status: FindingStatus.optional()
15389
+ status: FindingStatus.optional(),
15390
+ // The audit event this finding was captured from. Optional so callers that
15391
+ // do not project it stay valid. An at-rest finding is content-addressed by
15392
+ // finding_key and its row is upserted on re-detection, so this names the
15393
+ // MOST RECENT detection event, not the first.
15394
+ eventId: external_exports.string().optional(),
15395
+ // The session that event belongs to, when it has one — the seam a
15396
+ // per-instance "view session" link needs. Absent for events captured
15397
+ // outside a session.
15398
+ sessionId: external_exports.string().optional()
15373
15399
  }).meta({ id: "FindingInstance" });
15374
15400
  var FindingGroup = external_exports.object({
15375
15401
  id: external_exports.string(),
@@ -15413,7 +15439,11 @@ var FindingFacets = external_exports.object({
15413
15439
  // for every instance, so every group lands in a bucket; a status-less
15414
15440
  // group (possible only for callers whose rows carry no statuses) is
15415
15441
  // counted under no value.
15416
- status: external_exports.array(FindingFacetItem)
15442
+ status: external_exports.array(FindingFacetItem),
15443
+ // Host tool (attributes.tool_name). Present only on the instance-level
15444
+ // reads, which can filter by it; the grouped read omits the dimension
15445
+ // because a group spans tools.
15446
+ tool: external_exports.array(FindingFacetItem).optional()
15417
15447
  }).meta({ id: "FindingFacets" });
15418
15448
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15419
15449
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15431,6 +15461,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15431
15461
  // Scope to findings whose event carries this session id (the Activity page's
15432
15462
  // session → findings drilldown). Findings without a session never match.
15433
15463
  sessionId: external_exports.string().optional(),
15464
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15465
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15466
+ // means all time — this list has no default window.
15467
+ from: external_exports.iso.datetime().optional(),
15468
+ // A group or instance id that must appear in the page even when the cursor
15469
+ // has already advanced past its sort position. This is what keeps the
15470
+ // Findings page's one-shot ?finding= deep link resolving once the list
15471
+ // paginates: the target group is appended out of sort order rather than
15472
+ // scanning forward for it. Never affects totals, facets or the cursor.
15473
+ includeId: external_exports.string().optional(),
15434
15474
  groupBy: external_exports.literal("type").optional(),
15435
15475
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15436
15476
  cursor: external_exports.string().optional()
@@ -15475,15 +15515,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15475
15515
  detection: FindingDetectionRef,
15476
15516
  policy: FindingPolicyRef
15477
15517
  }).meta({ id: "FindingInstanceDetail" });
15518
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15519
+ var ListFindingInstancesQuery = external_exports.object({
15520
+ severity: external_exports.array(Severity).optional(),
15521
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15522
+ subtype: external_exports.array(external_exports.string()).optional(),
15523
+ provider: external_exports.array(FindingProvider).optional(),
15524
+ action: external_exports.array(FindingAction).optional(),
15525
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15526
+ // the grouped query's group-level fold.
15527
+ status: external_exports.array(FindingStatus).optional(),
15528
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15529
+ // where the free-text `q` can only match the rendered "via Bash" label.
15530
+ tool: external_exports.array(external_exports.string()).optional(),
15531
+ // Exact repository / file-path matches, for the drill-down out of the
15532
+ // locations view. A row whose event carries no repo/file matches neither.
15533
+ repo: external_exports.string().optional(),
15534
+ file: external_exports.string().optional(),
15535
+ q: external_exports.string().optional(),
15536
+ sessionId: external_exports.string().optional(),
15537
+ from: external_exports.iso.datetime().optional(),
15538
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15539
+ cursor: external_exports.string().optional()
15540
+ });
15541
+ var ListFindingInstancesResponse = external_exports.object({
15542
+ // Instances matching the filters across the whole scope, not just this
15543
+ // page — cursor-independent, like the grouped list's totals.
15544
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15545
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15546
+ // dimension still excludes its own filter.
15547
+ facets: FindingFacets,
15548
+ items: external_exports.array(FindingInstanceDetail),
15549
+ nextCursor: external_exports.string().nullable()
15550
+ }).meta({ id: "ListFindingInstancesResponse" });
15551
+ var FindingLocationFile = external_exports.object({
15552
+ // Empty when the instances carried no file path (a prompt or a tool call
15553
+ // with no file attribution).
15554
+ file: external_exports.string(),
15555
+ instanceCount: external_exports.number().int().nonnegative(),
15556
+ maxSeverity: Severity,
15557
+ latestDetectedAt: external_exports.iso.datetime(),
15558
+ // Folded from the instances' derived statuses with the same
15559
+ // open-dominates precedence a group uses.
15560
+ status: FindingStatus.optional(),
15561
+ // Distinct rules seen at this location, capped — the row shows them as
15562
+ // chips, and the count is what conveys scale.
15563
+ ruleIds: external_exports.array(external_exports.string())
15564
+ }).meta({ id: "FindingLocationFile" });
15565
+ var FindingLocationRepo = external_exports.object({
15566
+ /** Empty when the instances carried no repo attribute. */
15567
+ repo: external_exports.string(),
15568
+ instanceCount: external_exports.number().int().nonnegative(),
15569
+ maxSeverity: Severity,
15570
+ latestDetectedAt: external_exports.iso.datetime(),
15571
+ status: FindingStatus.optional(),
15572
+ files: external_exports.array(FindingLocationFile)
15573
+ }).meta({ id: "FindingLocationRepo" });
15574
+ var ListFindingLocationsQuery = external_exports.object({
15575
+ severity: external_exports.array(Severity).optional(),
15576
+ subtype: external_exports.array(external_exports.string()).optional(),
15577
+ provider: external_exports.array(FindingProvider).optional(),
15578
+ action: external_exports.array(FindingAction).optional(),
15579
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15580
+ // instances that match, and folds its status from those.
15581
+ status: external_exports.array(FindingStatus).optional(),
15582
+ tool: external_exports.array(external_exports.string()).optional(),
15583
+ q: external_exports.string().optional(),
15584
+ sessionId: external_exports.string().optional(),
15585
+ from: external_exports.iso.datetime().optional(),
15586
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15587
+ });
15588
+ var ListFindingLocationsResponse = external_exports.object({
15589
+ totals: external_exports.object({
15590
+ findings: external_exports.number().int().nonnegative(),
15591
+ repos: external_exports.number().int().nonnegative(),
15592
+ files: external_exports.number().int().nonnegative()
15593
+ }),
15594
+ /** Sorted by max severity, then most recent. */
15595
+ items: external_exports.array(FindingLocationRepo),
15596
+ /** Whether `limit` truncated the repo list. */
15597
+ hasMore: external_exports.boolean()
15598
+ }).meta({ id: "ListFindingLocationsResponse" });
15478
15599
 
15479
15600
  // ../../packages/schema/src/zod/harness-map.ts
15480
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15601
+ var Harness = external_exports.enum([
15602
+ "claudecode",
15603
+ "cursor",
15604
+ "copilot",
15605
+ "codex",
15606
+ "antigravity",
15607
+ "windsurf",
15608
+ "claudedesktop",
15609
+ "chatgpt",
15610
+ "claudeai",
15611
+ "api"
15612
+ ]).meta({ id: "Harness" });
15481
15613
  var TOOL_TO_HARNESS = {
15482
15614
  "claude-code": "claudecode",
15483
15615
  "claude-desktop": "claudedesktop",
15484
15616
  "github-copilot": "copilot",
15485
15617
  cursor: "cursor",
15486
- chatgpt: "chatgpt"
15618
+ chatgpt: "chatgpt",
15619
+ codex: "codex",
15620
+ antigravity: "antigravity",
15621
+ "claude-ai": "claudeai"
15487
15622
  };
15488
15623
 
15489
15624
  // ../../packages/schema/src/zod/meta.ts
@@ -15941,7 +16076,18 @@ var ActivityOverviewResponse = external_exports.object({
15941
16076
  // ../../packages/schema/src/zod/event.ts
15942
16077
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15943
16078
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15944
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16079
+ var SourceTool = external_exports.enum([
16080
+ "claude-code",
16081
+ "claude-desktop",
16082
+ "cursor",
16083
+ "chatgpt",
16084
+ "claude-ai",
16085
+ "github-copilot",
16086
+ "codex",
16087
+ "antigravity",
16088
+ "cli",
16089
+ "unknown"
16090
+ ]).meta({ id: "SourceTool" });
15945
16091
  var EventMetadata = external_exports.object({
15946
16092
  sessionId: external_exports.string().optional(),
15947
16093
  repo: external_exports.string().optional(),
@@ -16012,7 +16158,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
16012
16158
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
16013
16159
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
16014
16160
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
16015
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16161
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
16016
16162
  var AccessCounts = external_exports.object({
16017
16163
  open: external_exports.number().int().nonnegative(),
16018
16164
  approved: external_exports.number().int().nonnegative(),
@@ -16281,6 +16427,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16281
16427
  useCount: true,
16282
16428
  conditions: true
16283
16429
  });
16430
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16284
16431
 
16285
16432
  // ../../packages/schema/src/zod/rule.ts
16286
16433
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17091,6 +17238,35 @@ var EgressWriteSummary = external_exports.object({
17091
17238
  droppedFiles: external_exports.array(external_exports.string()).default([])
17092
17239
  }).meta({ id: "EgressWriteSummary" });
17093
17240
 
17241
+ // ../../packages/schema/src/zod/exception-action.ts
17242
+ var confirmation = external_exports.string().optional();
17243
+ var ApproveBlockedInput = external_exports.object({
17244
+ reference: external_exports.string(),
17245
+ scope: external_exports.string(),
17246
+ reason: external_exports.string(),
17247
+ confirmation
17248
+ });
17249
+ var AddExceptionInput = external_exports.object({
17250
+ ruleId: external_exports.string(),
17251
+ value: external_exports.string(),
17252
+ scope: external_exports.string(),
17253
+ reason: external_exports.string(),
17254
+ confirmation
17255
+ });
17256
+ var GrantRevealInput = external_exports.object({
17257
+ pointer: external_exports.string(),
17258
+ scope: external_exports.string(),
17259
+ justification: external_exports.string(),
17260
+ confirmation
17261
+ });
17262
+ var RevokeExceptionInput = external_exports.object({
17263
+ id: external_exports.string(),
17264
+ reason: external_exports.string()
17265
+ });
17266
+ var RotateKeyInput = external_exports.object({
17267
+ confirmation: external_exports.string()
17268
+ });
17269
+
17094
17270
  // ../../packages/schema/src/zod/findings-group-build.ts
17095
17271
  function toApiAction(dbVal) {
17096
17272
  const map2 = {
@@ -17146,6 +17322,8 @@ function buildFindingGroups(rows, opts = {}) {
17146
17322
  repo: r.repo,
17147
17323
  file: r.file,
17148
17324
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17325
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17326
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17149
17327
  action: toApiAction(effectiveDbAction),
17150
17328
  detectedAt: r.occurredAt,
17151
17329
  confidence: r.confidence,
@@ -17277,14 +17455,17 @@ function applyFindingFilters(groups, opts) {
17277
17455
  }
17278
17456
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17279
17457
  var SEVERITY_RANK = SEVERITY_ORDER;
17458
+ function compareFindingGroupOrder(a, b) {
17459
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17460
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17461
+ const severityDiff = rankA - rankB;
17462
+ if (severityDiff !== 0) return severityDiff;
17463
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17464
+ if (recencyDiff !== 0) return recencyDiff;
17465
+ return a.id.localeCompare(b.id);
17466
+ }
17280
17467
  function sortFindingGroups(groups) {
17281
- return [...groups].sort((a, b) => {
17282
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17283
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17284
- const severityDiff = rankA - rankB;
17285
- if (severityDiff !== 0) return severityDiff;
17286
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17287
- });
17468
+ return [...groups].sort(compareFindingGroupOrder);
17288
17469
  }
17289
17470
  function computeFindingFacets(allGroups, opts) {
17290
17471
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17340,15 +17521,158 @@ function computeFindingFacets(allGroups, opts) {
17340
17521
  for (const g of forStatus) {
17341
17522
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17342
17523
  }
17343
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17524
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17525
+ return {
17526
+ severity: toItems2(severityMap),
17527
+ provider: toItems2(providerMap),
17528
+ action: toItems2(actionMap),
17529
+ subtype: toItems2(subtypeMap),
17530
+ status: toItems2(statusMap)
17531
+ };
17532
+ }
17533
+
17534
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17535
+ function rowHaystack(row) {
17536
+ return [
17537
+ row.ruleId,
17538
+ row.category,
17539
+ row.maskedMatch,
17540
+ row.repo,
17541
+ row.file,
17542
+ row.toolName ? `via ${row.toolName}` : "",
17543
+ row.id
17544
+ ].join(" ").toLowerCase();
17545
+ }
17546
+ function matchesDimension(row, opts, dimension) {
17547
+ switch (dimension) {
17548
+ case "severity":
17549
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17550
+ case "subtype":
17551
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17552
+ case "providers":
17553
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17554
+ case "actions":
17555
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17556
+ case "statuses":
17557
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17558
+ case "tools":
17559
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17560
+ case "repo":
17561
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17562
+ case "file":
17563
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17564
+ case "q":
17565
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17566
+ }
17567
+ }
17568
+ var DIMENSIONS = [
17569
+ "severity",
17570
+ "subtype",
17571
+ "providers",
17572
+ "actions",
17573
+ "statuses",
17574
+ "tools",
17575
+ "repo",
17576
+ "file",
17577
+ "q"
17578
+ ];
17579
+ function matchesInstanceFilters(row, opts, except) {
17580
+ for (const dimension of DIMENSIONS) {
17581
+ if (dimension === except) continue;
17582
+ if (!matchesDimension(row, opts, dimension)) return false;
17583
+ }
17584
+ return true;
17585
+ }
17586
+ function toItems(counts) {
17587
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17588
+ }
17589
+ function bump(counts, value) {
17590
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17591
+ }
17592
+ function createInstanceFacetAccumulator(opts) {
17593
+ const severity = /* @__PURE__ */ new Map();
17594
+ const subtype = /* @__PURE__ */ new Map();
17595
+ const provider = /* @__PURE__ */ new Map();
17596
+ const action = /* @__PURE__ */ new Map();
17597
+ const status = /* @__PURE__ */ new Map();
17598
+ const tool = /* @__PURE__ */ new Map();
17344
17599
  return {
17345
- severity: toItems(severityMap),
17346
- provider: toItems(providerMap),
17347
- action: toItems(actionMap),
17348
- subtype: toItems(subtypeMap),
17349
- status: toItems(statusMap)
17600
+ add(row) {
17601
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17602
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17603
+ if (matchesInstanceFilters(row, opts, "providers")) {
17604
+ bump(provider, toApiProvider(row.sourceTool));
17605
+ }
17606
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17607
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17608
+ bump(status, row.status);
17609
+ }
17610
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17611
+ bump(tool, row.toolName);
17612
+ }
17613
+ },
17614
+ facets: () => ({
17615
+ severity: toItems(severity),
17616
+ subtype: toItems(subtype),
17617
+ provider: toItems(provider),
17618
+ action: toItems(action),
17619
+ status: toItems(status),
17620
+ tool: toItems(tool)
17621
+ })
17622
+ };
17623
+ }
17624
+ function toInstanceDetail(row) {
17625
+ const category = toApiCategory(row.category);
17626
+ return {
17627
+ id: row.id,
17628
+ provider: toApiProvider(row.sourceTool),
17629
+ repo: row.repo,
17630
+ file: row.file,
17631
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17632
+ eventId: row.eventId,
17633
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17634
+ action: toApiAction(row.actionTaken),
17635
+ detectedAt: row.occurredAt,
17636
+ confidence: row.confidence,
17637
+ ...row.status === void 0 ? {} : { status: row.status },
17638
+ groupId: row.ruleId,
17639
+ category,
17640
+ subtype: row.ruleId,
17641
+ severity: row.severity,
17642
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17643
+ detection: { id: row.ruleId, name: null },
17644
+ policy: { id: `category:${category}`, name: category }
17645
+ };
17646
+ }
17647
+ var SEVERITY_ORDER2 = {
17648
+ critical: 0,
17649
+ high: 1,
17650
+ medium: 2,
17651
+ low: 3
17652
+ };
17653
+ function newLocationAccumulator() {
17654
+ return {
17655
+ instanceCount: 0,
17656
+ // Sorts after every known severity, so the first row always wins the
17657
+ // comparison below rather than an unknown value pinning the location.
17658
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17659
+ maxSeverity: "low",
17660
+ latestDetectedAt: "",
17661
+ statuses: [],
17662
+ ruleIds: /* @__PURE__ */ new Set()
17350
17663
  };
17351
17664
  }
17665
+ function addToLocation(acc, row) {
17666
+ acc.instanceCount += 1;
17667
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17668
+ if (rank < acc.maxSeverityRank) {
17669
+ acc.maxSeverityRank = rank;
17670
+ acc.maxSeverity = row.severity;
17671
+ }
17672
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17673
+ acc.statuses.push(row.status);
17674
+ acc.ruleIds.add(row.ruleId);
17675
+ }
17352
17676
 
17353
17677
  // ../../packages/schema/src/zod/installed-pack.ts
17354
17678
  var InstalledPack = external_exports.object({
@@ -17489,6 +17813,50 @@ var VaultInventoryEntry = external_exports.object({
17489
17813
  revealGrantId: external_exports.string().nullable(),
17490
17814
  sightings: external_exports.array(VaultSighting)
17491
17815
  });
17816
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17817
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17818
+ var MAX_VAULT_PAGE_LIMIT = 200;
17819
+ var ListVaultInventoryQuery = external_exports.object({
17820
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17821
+ // Opaque; names the last row of the page just served.
17822
+ cursor: external_exports.string().optional()
17823
+ });
17824
+ var ListVaultInventoryResponse = external_exports.object({
17825
+ // Vaulted values across the whole store, not just this page — cursor-
17826
+ // independent, so paging never changes what the count claims.
17827
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17828
+ items: external_exports.array(VaultInventoryEntry),
17829
+ // `null` once the last page is reached.
17830
+ nextCursor: external_exports.string().nullable()
17831
+ });
17832
+ var ListVaultReuseQuery = external_exports.object({
17833
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17834
+ cursor: external_exports.string().optional()
17835
+ });
17836
+ var ListVaultReuseResponse = external_exports.object({
17837
+ // Reused values across the whole store — the number the section's claim
17838
+ // ("values detected in more than one place") is about.
17839
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17840
+ items: external_exports.array(VaultInventoryEntry),
17841
+ nextCursor: external_exports.string().nullable()
17842
+ });
17843
+ var ListVaultDerefsQuery = external_exports.object({
17844
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17845
+ // hides them and counts them into `hiddenBatched` instead, so the model
17846
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17847
+ // over a Server Action, which preserves the type, never as a URL param.
17848
+ includeBatched: external_exports.boolean().optional(),
17849
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17850
+ cursor: external_exports.string().optional()
17851
+ });
17852
+ var ListVaultDerefsResponse = external_exports.object({
17853
+ items: external_exports.array(VaultDeref),
17854
+ nextCursor: external_exports.string().nullable(),
17855
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17856
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17857
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17858
+ hiddenBatched: external_exports.number().int().nonnegative()
17859
+ });
17492
17860
  var VaultKeyCustody = external_exports.string();
17493
17861
  var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17494
17862
  var VAULT_CONSENT_VERSION = 1;
@@ -17865,7 +18233,7 @@ var TopSourcesQuery = external_exports.object({
17865
18233
  // Omit for both kinds.
17866
18234
  kind: external_exports.enum(SOURCE_KINDS).optional()
17867
18235
  });
17868
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18236
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17869
18237
  var ScanCoverageProvider = external_exports.object({
17870
18238
  provider: Provider,
17871
18239
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -18118,6 +18486,195 @@ function captureId(sessionId, contentHash, filePath = null) {
18118
18486
  );
18119
18487
  }
18120
18488
 
18489
+ // ../../packages/persistence/src/internal/snapshot.ts
18490
+ import { randomUUID } from "crypto";
18491
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18492
+ import { basename, dirname, join } from "path";
18493
+
18494
+ // ../../packages/persistence/src/paths.ts
18495
+ import {
18496
+ chmodSync,
18497
+ linkSync,
18498
+ lstatSync,
18499
+ mkdirSync,
18500
+ renameSync,
18501
+ rmSync,
18502
+ writeFileSync
18503
+ } from "fs";
18504
+ import { threadId } from "worker_threads";
18505
+ var DATA_DIR_MODE = 448;
18506
+ var DATA_FILE_MODE = 384;
18507
+ var DB_FILENAME = "aka.db";
18508
+ function isSymlink(path) {
18509
+ try {
18510
+ return lstatSync(path).isSymbolicLink();
18511
+ } catch {
18512
+ return false;
18513
+ }
18514
+ }
18515
+ function chmodBestEffort(path, mode) {
18516
+ if (isSymlink(path)) return;
18517
+ try {
18518
+ chmodSync(path, mode);
18519
+ } catch {
18520
+ }
18521
+ }
18522
+ function tightenDir(dir) {
18523
+ chmodBestEffort(dir, DATA_DIR_MODE);
18524
+ }
18525
+ function ensureDataDirSync(dir) {
18526
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18527
+ tightenDir(dir);
18528
+ }
18529
+ function dbSidecars(file2) {
18530
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18531
+ }
18532
+ function tightenFile(file2) {
18533
+ chmodBestEffort(file2, DATA_FILE_MODE);
18534
+ }
18535
+ function tightenPerms(file2) {
18536
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18537
+ }
18538
+ function classifyOccupant(file2) {
18539
+ try {
18540
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
18541
+ return { kind: "gone" };
18542
+ } catch (err) {
18543
+ if (err.code === "ENOENT") return { kind: "gone" };
18544
+ return { kind: "unknown", cause: err };
18545
+ }
18546
+ }
18547
+ var KeyUnclaimableError = class extends Error {
18548
+ code = "key-unclaimable";
18549
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
18550
+ // defines the property anyway, so an error carrying nothing would still answer
18551
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
18552
+ // captured and then lost, which is worse than its plain absence.
18553
+ constructor(message, cause) {
18554
+ super(message, cause === void 0 ? void 0 : { cause });
18555
+ this.name = "KeyUnclaimableError";
18556
+ }
18557
+ };
18558
+ function createOwnerOnlyFileSync(file2, data) {
18559
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
18560
+ try {
18561
+ rmSync(tmp, { force: true });
18562
+ } catch {
18563
+ }
18564
+ let created;
18565
+ try {
18566
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18567
+ created = publishByLink(tmp, file2, data);
18568
+ } finally {
18569
+ try {
18570
+ rmSync(tmp, { force: true });
18571
+ } catch {
18572
+ }
18573
+ }
18574
+ if (created) tightenFile(file2);
18575
+ return created;
18576
+ }
18577
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
18578
+ function publishByLink(tmp, file2, data) {
18579
+ try {
18580
+ linkSync(tmp, file2);
18581
+ return true;
18582
+ } catch (err) {
18583
+ const code = err.code;
18584
+ if (code === "EEXIST") return false;
18585
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
18586
+ }
18587
+ try {
18588
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
18589
+ return true;
18590
+ } catch (err) {
18591
+ if (err.code === "EEXIST") return false;
18592
+ throw err;
18593
+ }
18594
+ }
18595
+
18596
+ // ../../packages/persistence/src/internal/snapshot.ts
18597
+ function backupPath(file2, tag) {
18598
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18599
+ }
18600
+ var STALE_PARTIAL_MS = 5 * 6e4;
18601
+ function reapStalePartials(file2) {
18602
+ const dir = dirname(file2);
18603
+ const prefix = `${basename(file2)}.`;
18604
+ let entries;
18605
+ try {
18606
+ entries = readdirSync(dir);
18607
+ } catch {
18608
+ return;
18609
+ }
18610
+ for (const name of entries) {
18611
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18612
+ const partial2 = join(dir, name);
18613
+ try {
18614
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18615
+ rmSync2(partial2, { force: true });
18616
+ }
18617
+ } catch {
18618
+ }
18619
+ }
18620
+ }
18621
+ function snapshotStore(db, backup) {
18622
+ const partial2 = `${backup}.partial`;
18623
+ try {
18624
+ rmSync2(partial2, { force: true });
18625
+ db.prepare("VACUUM INTO ?").run(partial2);
18626
+ tightenFile(partial2);
18627
+ renameSync2(partial2, backup);
18628
+ } catch (error51) {
18629
+ try {
18630
+ rmSync2(partial2, { force: true });
18631
+ } catch {
18632
+ }
18633
+ throw error51;
18634
+ }
18635
+ }
18636
+ function moveStoreAside(file2, backup) {
18637
+ const undo = [];
18638
+ renameSync2(file2, backup);
18639
+ undo.push([backup, file2]);
18640
+ try {
18641
+ for (const sidecar of dbSidecars(file2)) {
18642
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18643
+ try {
18644
+ renameSync2(sidecar, moved);
18645
+ undo.push([moved, sidecar]);
18646
+ } catch {
18647
+ rmSync2(sidecar, { force: true });
18648
+ }
18649
+ }
18650
+ } catch (error51) {
18651
+ for (const [from, to] of undo.reverse()) {
18652
+ try {
18653
+ renameSync2(from, to);
18654
+ } catch {
18655
+ }
18656
+ }
18657
+ throw error51;
18658
+ }
18659
+ tightenPerms(backup);
18660
+ }
18661
+ function discardStore(file2, backup) {
18662
+ try {
18663
+ rmSync2(file2, { force: true });
18664
+ for (const sidecar of dbSidecars(file2)) {
18665
+ rmSync2(sidecar, { force: true });
18666
+ }
18667
+ } catch (error51) {
18668
+ if (existsSync(file2)) {
18669
+ try {
18670
+ rmSync2(backup, { force: true });
18671
+ } catch {
18672
+ }
18673
+ }
18674
+ throw error51;
18675
+ }
18676
+ }
18677
+
18121
18678
  // ../../packages/persistence/src/internal/sql-text.ts
18122
18679
  function escapeLikePattern(s) {
18123
18680
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18259,55 +18816,6 @@ function mapRowsTolerant(rows, map2) {
18259
18816
  return out;
18260
18817
  }
18261
18818
 
18262
- // ../../packages/persistence/src/paths.ts
18263
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18264
- var DATA_DIR_MODE = 448;
18265
- var DATA_FILE_MODE = 384;
18266
- var DB_FILENAME = "aka.db";
18267
- function chmodBestEffort(path, mode) {
18268
- try {
18269
- chmodSync(path, mode);
18270
- } catch {
18271
- }
18272
- }
18273
- function tightenDir(dir) {
18274
- chmodBestEffort(dir, DATA_DIR_MODE);
18275
- }
18276
- function ensureDataDirSync(dir) {
18277
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18278
- tightenDir(dir);
18279
- }
18280
- function dbSidecars(file2) {
18281
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18282
- }
18283
- function tightenFile(file2) {
18284
- try {
18285
- if (lstatSync(file2).isSymbolicLink()) return;
18286
- } catch {
18287
- }
18288
- chmodBestEffort(file2, DATA_FILE_MODE);
18289
- }
18290
- function tightenPerms(file2) {
18291
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18292
- }
18293
- function writeOwnerOnlyFileSync(file2, data) {
18294
- const tmp = `${file2}.${String(process.pid)}.tmp`;
18295
- try {
18296
- rmSync(tmp, { force: true });
18297
- } catch {
18298
- }
18299
- try {
18300
- writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18301
- renameSync(tmp, file2);
18302
- } finally {
18303
- try {
18304
- rmSync(tmp, { force: true });
18305
- } catch {
18306
- }
18307
- }
18308
- tightenFile(file2);
18309
- }
18310
-
18311
18819
  // ../../packages/persistence/src/migrations.ts
18312
18820
  function describeObject(object2) {
18313
18821
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18423,9 +18931,9 @@ function applyLegacyDropMigration(db, file2) {
18423
18931
  }
18424
18932
  }
18425
18933
  function backupBeforeLegacyDrop(db, file2) {
18426
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18427
- db.prepare("VACUUM INTO ?").run(backup);
18428
- tightenFile(backup);
18934
+ reapStalePartials(file2);
18935
+ const backup = backupPath(file2, "pre-drop");
18936
+ snapshotStore(db, backup);
18429
18937
  return backup;
18430
18938
  }
18431
18939
  var TOKEN_USAGE_COLUMNS = [
@@ -18769,6 +19277,25 @@ function parseJsonObject(s) {
18769
19277
  return void 0;
18770
19278
  }
18771
19279
 
19280
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19281
+ function encodeKeysetCursor(payload) {
19282
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19283
+ }
19284
+ function decodeKeysetCursor(cursor) {
19285
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19286
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19287
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19288
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19289
+ // a null cursor, which a caller reads as "end of list". That is the one
19290
+ // outcome a cursor that does not decode must never produce, since the
19291
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19292
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19293
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19294
+ return parsed;
19295
+ }
19296
+ return null;
19297
+ }
19298
+
18772
19299
  // ../../packages/persistence/src/repositories/activity.ts
18773
19300
  var DAY_MS = 864e5;
18774
19301
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18814,16 +19341,6 @@ function utcWindow(nowMs) {
18814
19341
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18815
19342
  return { startMs, endMs: startMs + DAY_MS };
18816
19343
  }
18817
- function encodeCursor(payload) {
18818
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18819
- }
18820
- function decodeCursor(cursor) {
18821
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18822
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18823
- return parsed;
18824
- }
18825
- return null;
18826
- }
18827
19344
  var DB_EVENT_TYPE_TO_KIND = {
18828
19345
  session: "session",
18829
19346
  prompt: "prompt",
@@ -18968,7 +19485,7 @@ var SqliteActivityRepository = class {
18968
19485
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18969
19486
  }
18970
19487
  listSessions(query) {
18971
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19488
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18972
19489
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18973
19490
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18974
19491
  const conditions = [SESSION_ROOT];
@@ -19042,7 +19559,7 @@ var SqliteActivityRepository = class {
19042
19559
  )
19043
19560
  );
19044
19561
  const last = page[page.length - 1];
19045
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19562
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19046
19563
  return Promise.resolve({ items, nextCursor, emptyCount });
19047
19564
  }
19048
19565
  getSession(sessionId) {
@@ -19915,7 +20432,7 @@ var SqliteEventsRepository = class {
19915
20432
  };
19916
20433
 
19917
20434
  // ../../packages/persistence/src/repositories/exceptions.ts
19918
- import { randomUUID } from "crypto";
20435
+ import { randomUUID as randomUUID2 } from "crypto";
19919
20436
 
19920
20437
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19921
20438
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19951,8 +20468,9 @@ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19951
20468
  AND conditions IS NULL
19952
20469
  AND ${ACTIVE_PREDICATE}`;
19953
20470
  var SqliteExceptionsRepository = class {
19954
- constructor(db) {
20471
+ constructor(db, now = () => Date.now()) {
19955
20472
  this.db = db;
20473
+ this.now = now;
19956
20474
  this.consumeStmt = db.prepare(
19957
20475
  `UPDATE exceptions
19958
20476
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19970,6 +20488,7 @@ var SqliteExceptionsRepository = class {
19970
20488
  );
19971
20489
  }
19972
20490
  db;
20491
+ now;
19973
20492
  consumeStmt;
19974
20493
  insertBlockedStmt;
19975
20494
  sweepBlockedStmt;
@@ -19996,8 +20515,8 @@ var SqliteExceptionsRepository = class {
19996
20515
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19997
20516
  );
19998
20517
  }
19999
- const id = randomUUID();
20000
- const now = Date.now();
20518
+ const id = randomUUID2();
20519
+ const now = this.now();
20001
20520
  try {
20002
20521
  this.insertExceptionRow(id, input, now);
20003
20522
  } catch (err) {
@@ -20075,7 +20594,7 @@ var SqliteExceptionsRepository = class {
20075
20594
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
20076
20595
  const rows = allRows(
20077
20596
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
20078
- opts?.includeTerminal ? {} : { now: Date.now() }
20597
+ opts?.includeTerminal ? {} : { now: this.now() }
20079
20598
  );
20080
20599
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
20081
20600
  return Promise.resolve(exceptions);
@@ -20110,7 +20629,7 @@ var SqliteExceptionsRepository = class {
20110
20629
  * already revoked.
20111
20630
  */
20112
20631
  revoke(id, revokedBy, reason) {
20113
- const now = Date.now();
20632
+ const now = this.now();
20114
20633
  const result = this.db.prepare(
20115
20634
  `UPDATE exceptions
20116
20635
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -20124,7 +20643,7 @@ var SqliteExceptionsRepository = class {
20124
20643
  * callers must treat identically — means it does not and the detection is
20125
20644
  * enforced as usual. Deliberately NOT wrapped in try/catch.
20126
20645
  */
20127
- consume(id, now = Date.now()) {
20646
+ consume(id, now = this.now()) {
20128
20647
  const result = this.consumeStmt.run({ id, now });
20129
20648
  return Promise.resolve(Number(result.changes) === 1);
20130
20649
  }
@@ -20133,7 +20652,7 @@ var SqliteExceptionsRepository = class {
20133
20652
  * version — what rides the policy bundle to the hook. Grants written under
20134
20653
  * a different (rotated-away) key never match, so they are excluded at read.
20135
20654
  */
20136
- activeBundleEntries(keyVersion, now = Date.now()) {
20655
+ activeBundleEntries(keyVersion, now = this.now()) {
20137
20656
  const rows = allRows(
20138
20657
  this.db.prepare(
20139
20658
  `SELECT * FROM exceptions
@@ -20165,7 +20684,7 @@ var SqliteExceptionsRepository = class {
20165
20684
  * than the retention window on every write, so the ledger self-limits.
20166
20685
  */
20167
20686
  recordBlocked(entry) {
20168
- const now = Date.now();
20687
+ const now = this.now();
20169
20688
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
20170
20689
  this.insertBlockedStmt.run({
20171
20690
  reference: entry.reference,
@@ -20188,7 +20707,7 @@ var SqliteExceptionsRepository = class {
20188
20707
  WHERE blocked_at > :cutoff
20189
20708
  ORDER BY blocked_at DESC, rowid DESC`
20190
20709
  ),
20191
- { cutoff: Date.now() - windowMs }
20710
+ { cutoff: this.now() - windowMs }
20192
20711
  );
20193
20712
  return Promise.resolve(
20194
20713
  rows.map((row) => ({
@@ -20216,8 +20735,9 @@ var SqliteExceptionsRepository = class {
20216
20735
  * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20217
20736
  * grant instead. Fail closed until reveal-side condition evaluation exists.
20218
20737
  */
20219
- activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20738
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20220
20739
  try {
20740
+ const at = now ?? this.now();
20221
20741
  const row = getRow(
20222
20742
  this.db.prepare(
20223
20743
  `SELECT id FROM exceptions
@@ -20226,7 +20746,7 @@ var SqliteExceptionsRepository = class {
20226
20746
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20227
20747
  LIMIT 1`
20228
20748
  ),
20229
- { ruleId, valueFingerprint, keyVersion, now }
20749
+ { ruleId, valueFingerprint, keyVersion, now: at }
20230
20750
  );
20231
20751
  return Promise.resolve(row ?? null);
20232
20752
  } catch (err) {
@@ -20240,7 +20760,7 @@ var SqliteExceptionsRepository = class {
20240
20760
  * predicate, so correctness never depends on this sweep; it only bounds how
20241
20761
  * long the audit evidence is kept locally. Returns the deleted count.
20242
20762
  */
20243
- sweepTerminal(retentionMs, now = Date.now()) {
20763
+ sweepTerminal(retentionMs, now = this.now()) {
20244
20764
  const result = this.db.prepare(
20245
20765
  `DELETE FROM exceptions
20246
20766
  WHERE updated_at < :cutoff
@@ -20303,6 +20823,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20303
20823
 
20304
20824
  // ../../packages/persistence/src/repositories/findings.ts
20305
20825
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20826
+ var SCAN_BATCH_ROWS = 1e3;
20827
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20828
+ var LOCATION_RULE_IDS_CAP = 20;
20829
+ function compareLocationOrder(a, b) {
20830
+ return compareFindingGroupOrder(
20831
+ {
20832
+ severity: a.maxSeverity,
20833
+ latestDetectedAt: a.latestDetectedAt,
20834
+ id: ""
20835
+ },
20836
+ {
20837
+ severity: b.maxSeverity,
20838
+ latestDetectedAt: b.latestDetectedAt,
20839
+ id: ""
20840
+ }
20841
+ );
20842
+ }
20306
20843
  var CONCAT_SEP = ",";
20307
20844
  var TUPLE_SEP = "|";
20308
20845
  function splitConcat(value) {
@@ -20315,6 +20852,33 @@ function deriveInstanceStatus(row) {
20315
20852
  latestResolutionStatus: row.latest_status
20316
20853
  });
20317
20854
  }
20855
+ function encodeGroupCursor(group) {
20856
+ const payload = {
20857
+ sev: group.severity,
20858
+ t: group.latestDetectedAt,
20859
+ id: group.id
20860
+ };
20861
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20862
+ }
20863
+ function decodeGroupCursor(cursor) {
20864
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20865
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20866
+ return {
20867
+ severity: parsed.sev,
20868
+ latestDetectedAt: parsed.t,
20869
+ id: parsed.id
20870
+ };
20871
+ }
20872
+ return null;
20873
+ }
20874
+ function firstAfter(sorted, cursor) {
20875
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20876
+ return index === -1 ? sorted.length : index;
20877
+ }
20878
+ function findDeepLinked(sorted, page, id) {
20879
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20880
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20881
+ }
20318
20882
  var DAY_MS3 = 864e5;
20319
20883
  var SqliteFindingsRepository = class {
20320
20884
  constructor(db) {
@@ -20424,8 +20988,13 @@ var SqliteFindingsRepository = class {
20424
20988
  */
20425
20989
  listGroupedFindings(query) {
20426
20990
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20427
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20428
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20991
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20992
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20993
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20994
+ const sessionParams = {
20995
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20996
+ ...fromMs === void 0 ? {} : { fromMs }
20997
+ };
20429
20998
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20430
20999
  predicate,
20431
21000
  params: sessionParams
@@ -20433,7 +21002,8 @@ var SqliteFindingsRepository = class {
20433
21002
  const rows = allRows(
20434
21003
  this.db.prepare(
20435
21004
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20436
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
21005
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
21006
+ kind, finding_key, latest_status
20437
21007
  FROM (
20438
21008
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20439
21009
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20443,6 +21013,7 @@ var SqliteFindingsRepository = class {
20443
21013
  json_extract(e.attributes, '$.repo') AS repo,
20444
21014
  json_extract(e.attributes, '$.file_path') AS file,
20445
21015
  json_extract(e.attributes, '$.tool_name') AS tool_name,
21016
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20446
21017
  e.event_type AS kind, f.finding_key AS finding_key,
20447
21018
  latest.status AS latest_status,
20448
21019
  ROW_NUMBER() OVER (
@@ -20474,6 +21045,8 @@ var SqliteFindingsRepository = class {
20474
21045
  repo: r.repo ?? "",
20475
21046
  file: r.file ?? "",
20476
21047
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
21048
+ eventId: r.event_id,
21049
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20477
21050
  status: deriveInstanceStatus(r)
20478
21051
  }));
20479
21052
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20497,18 +21070,23 @@ var SqliteFindingsRepository = class {
20497
21070
  groups: sorted.length
20498
21071
  };
20499
21072
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21073
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21074
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21075
+ const page = sorted.slice(start, start + limit);
21076
+ const lastOnPage = page.at(-1);
21077
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21078
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20500
21079
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20501
- const items = sorted.slice(0, limit).map(
20502
- (g) => statusSet ? {
20503
- ...g,
20504
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20505
- } : g
20506
- );
21080
+ const narrow = (g) => statusSet ? {
21081
+ ...g,
21082
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21083
+ } : g;
21084
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20507
21085
  return Promise.resolve({
20508
21086
  totals,
20509
21087
  facets,
20510
21088
  items,
20511
- nextCursor: null,
21089
+ nextCursor,
20512
21090
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20513
21091
  });
20514
21092
  }
@@ -20540,6 +21118,266 @@ var SqliteFindingsRepository = class {
20540
21118
  * request actually carries a `q`. (Substring matching is unaffected by a
20541
21119
  * path repeating across tuples.)
20542
21120
  */
21121
+ /**
21122
+ * The instance-level (flat) findings list: one row per finding, newest first,
21123
+ * paged by keyset.
21124
+ *
21125
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21126
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21127
+ * them changes no reported number. Severity, subtype, provider, action,
21128
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21129
+ * facet excludes its own filter, so a row the filter rejects still has to be
21130
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21131
+ * Several could not be expressed there anyway: status comes from the one
21132
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21133
+ * none of the mappers names", which no IN-list can say.
21134
+ *
21135
+ * The scan runs from the top of the scope on every request, not from the
21136
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21137
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21138
+ * while the counting runs, and only the page itself is retained.
21139
+ */
21140
+ listFindingInstances(query) {
21141
+ const opts = {
21142
+ severity: query.severity,
21143
+ subtype: query.subtype,
21144
+ providers: query.provider,
21145
+ actions: query.action,
21146
+ statuses: query.status,
21147
+ tools: query.tool,
21148
+ repo: query.repo,
21149
+ file: query.file,
21150
+ q: query.q
21151
+ };
21152
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21153
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21154
+ const accumulator = createInstanceFacetAccumulator(opts);
21155
+ const items = [];
21156
+ let total = 0;
21157
+ let last;
21158
+ let hasMore = false;
21159
+ for (const row of this.scanFindingRows({
21160
+ sessionId: query.sessionId,
21161
+ from: query.from
21162
+ })) {
21163
+ accumulator.add(row);
21164
+ if (!matchesInstanceFilters(row, opts)) continue;
21165
+ total += 1;
21166
+ if (items.length < limit) {
21167
+ items.push(toInstanceDetail(row));
21168
+ last = row;
21169
+ } else {
21170
+ hasMore = true;
21171
+ }
21172
+ }
21173
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21174
+ if (cursor !== null) {
21175
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21176
+ return Promise.resolve({
21177
+ totals: { findings: total },
21178
+ facets: accumulator.facets(),
21179
+ items: resumed.items,
21180
+ nextCursor: resumed.nextCursor
21181
+ });
21182
+ }
21183
+ return Promise.resolve({
21184
+ totals: { findings: total },
21185
+ facets: accumulator.facets(),
21186
+ items,
21187
+ nextCursor
21188
+ });
21189
+ }
21190
+ /**
21191
+ * The page of matching rows strictly after `cursor`. Separate from the
21192
+ * counting pass because that one starts at the top of the scope by design;
21193
+ * this one narrows the scan with the same keyset predicate the activity list
21194
+ * uses, so a later page costs less than the first rather than more.
21195
+ */
21196
+ pageAfter(cursor, opts, limit, query) {
21197
+ const items = [];
21198
+ let last;
21199
+ let hasMore = false;
21200
+ for (const row of this.scanFindingRows({
21201
+ sessionId: query.sessionId,
21202
+ from: query.from,
21203
+ after: cursor
21204
+ })) {
21205
+ if (!matchesInstanceFilters(row, opts)) continue;
21206
+ if (items.length < limit) {
21207
+ items.push(toInstanceDetail(row));
21208
+ last = row;
21209
+ } else {
21210
+ hasMore = true;
21211
+ break;
21212
+ }
21213
+ }
21214
+ return {
21215
+ items,
21216
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21217
+ };
21218
+ }
21219
+ /**
21220
+ * The same findings folded by location: repository, then file within it.
21221
+ *
21222
+ * The grouping keys come from the capturing event's attributes, which is what
21223
+ * the local store relates a finding to — there is no finding↔asset row to
21224
+ * group by instead. A repo or file the event did not record folds into the
21225
+ * empty-string bucket, which the view renders but does not link, since no
21226
+ * filter can name it.
21227
+ */
21228
+ listFindingLocations(query) {
21229
+ const opts = {
21230
+ severity: query.severity,
21231
+ subtype: query.subtype,
21232
+ providers: query.provider,
21233
+ actions: query.action,
21234
+ statuses: query.status,
21235
+ tools: query.tool,
21236
+ q: query.q
21237
+ };
21238
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21239
+ const byRepo = /* @__PURE__ */ new Map();
21240
+ let total = 0;
21241
+ for (const row of this.scanFindingRows({
21242
+ sessionId: query.sessionId,
21243
+ from: query.from
21244
+ })) {
21245
+ if (!matchesInstanceFilters(row, opts)) continue;
21246
+ total += 1;
21247
+ let files = byRepo.get(row.repo);
21248
+ if (files === void 0) {
21249
+ files = /* @__PURE__ */ new Map();
21250
+ byRepo.set(row.repo, files);
21251
+ }
21252
+ let acc = files.get(row.file);
21253
+ if (acc === void 0) {
21254
+ acc = newLocationAccumulator();
21255
+ files.set(row.file, acc);
21256
+ }
21257
+ addToLocation(acc, row);
21258
+ }
21259
+ let fileCount = 0;
21260
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21261
+ fileCount += files.size;
21262
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21263
+ file: file2,
21264
+ instanceCount: acc.instanceCount,
21265
+ maxSeverity: acc.maxSeverity,
21266
+ latestDetectedAt: acc.latestDetectedAt,
21267
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21268
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21269
+ })).sort(compareLocationOrder);
21270
+ const rollup = fileRows.reduce(
21271
+ (a, f) => ({
21272
+ instanceCount: a.instanceCount + f.instanceCount,
21273
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21274
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21275
+ }),
21276
+ {
21277
+ instanceCount: 0,
21278
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21279
+ latestDetectedAt: ""
21280
+ }
21281
+ );
21282
+ const statuses = fileRows.map((f) => f.status);
21283
+ const folded = foldGroupStatus(statuses);
21284
+ return {
21285
+ repo,
21286
+ instanceCount: rollup.instanceCount,
21287
+ maxSeverity: rollup.maxSeverity,
21288
+ latestDetectedAt: rollup.latestDetectedAt,
21289
+ ...folded === void 0 ? {} : { status: folded },
21290
+ files: fileRows
21291
+ };
21292
+ });
21293
+ repos.sort(compareLocationOrder);
21294
+ return Promise.resolve({
21295
+ totals: { findings: total, repos: repos.length, files: fileCount },
21296
+ items: repos.slice(0, limit),
21297
+ hasMore: repos.length > limit
21298
+ });
21299
+ }
21300
+ /**
21301
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21302
+ *
21303
+ * A generator so a caller streams the scope without it ever being an array:
21304
+ * the flat list counts and facets the whole filtered scope, which on a large
21305
+ * store is far more rows than any page. Each batch advances the same keyset
21306
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21307
+ * rather than one unbounded result set.
21308
+ *
21309
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21310
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21311
+ * makes it a point lookup per row, and the derived table would re-materialize
21312
+ * a window over the whole resolution table once per batch.
21313
+ *
21314
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21315
+ * would be missing from its own facet, which is computed by excluding that
21316
+ * dimension — see listFindingInstances.
21317
+ */
21318
+ *scanFindingRows(scope) {
21319
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21320
+ const params = [];
21321
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21322
+ conditions.push("e.root_session_id = ?");
21323
+ params.push(scope.sessionId);
21324
+ }
21325
+ if (scope.from !== void 0) {
21326
+ conditions.push("e.started_at >= ?");
21327
+ params.push(isoToEpochMillis(scope.from));
21328
+ }
21329
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21330
+ d.severity AS severity, f.masked_match AS masked_match,
21331
+ f.action_taken AS action_taken, f.confidence AS confidence,
21332
+ e.started_at AS occurred_at,
21333
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21334
+ json_extract(e.attributes, '$.repo') AS repo,
21335
+ json_extract(e.attributes, '$.file_path') AS file,
21336
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21337
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21338
+ e.event_type AS kind, f.finding_key AS finding_key,
21339
+ ${latestResolutionStatusSql("f")} AS latest_status
21340
+ FROM inspection_findings f
21341
+ JOIN audit_events e ON e.id = f.audit_event_id
21342
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21343
+ WHERE ${conditions.join(" AND ")}
21344
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21345
+ ORDER BY e.started_at DESC, f.id DESC
21346
+ LIMIT ?`;
21347
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21348
+ for (; ; ) {
21349
+ const rows = allRows(this.db.prepare(sql), [
21350
+ ...params,
21351
+ after.startedAtMs,
21352
+ after.startedAtMs,
21353
+ after.id,
21354
+ SCAN_BATCH_ROWS
21355
+ ]);
21356
+ for (const r of rows) {
21357
+ yield {
21358
+ id: r.id,
21359
+ ruleId: r.rule_id,
21360
+ category: r.category,
21361
+ severity: r.severity,
21362
+ maskedMatch: r.masked_match,
21363
+ actionTaken: r.action_taken,
21364
+ confidence: r.confidence,
21365
+ occurredAt: epochMillisToIso(r.occurred_at),
21366
+ sourceTool: r.source_tool,
21367
+ repo: r.repo ?? "",
21368
+ file: r.file ?? "",
21369
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21370
+ eventId: r.event_id,
21371
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21372
+ status: deriveInstanceStatus(r)
21373
+ };
21374
+ }
21375
+ if (rows.length < SCAN_BATCH_ROWS) return;
21376
+ const lastRow = rows[rows.length - 1];
21377
+ if (lastRow === void 0) return;
21378
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21379
+ }
21380
+ }
20543
21381
  groupAggregates(withSearchText, scope) {
20544
21382
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20545
21383
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20800,7 +21638,7 @@ var SqliteInspectionFindingsRepository = class {
20800
21638
  };
20801
21639
 
20802
21640
  // ../../packages/persistence/src/repositories/installed-packs.ts
20803
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21641
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20804
21642
 
20805
21643
  // ../../packages/persistence/src/semver.ts
20806
21644
  function parse3(version2) {
@@ -20951,7 +21789,7 @@ var SqliteInstalledPacksRepository = class {
20951
21789
  let behind = false;
20952
21790
  for (const row of rows) {
20953
21791
  const params = {
20954
- id: randomUUID2(),
21792
+ id: randomUUID3(),
20955
21793
  namespace: row.namespace,
20956
21794
  packId: row.packId,
20957
21795
  version: row.version,
@@ -20963,7 +21801,7 @@ var SqliteInstalledPacksRepository = class {
20963
21801
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20964
21802
  this.upsertAvailableStmt.run({
20965
21803
  ...params,
20966
- id: randomUUID2(),
21804
+ id: randomUUID3(),
20967
21805
  recordedBy: meta3?.recordedBy ?? null
20968
21806
  });
20969
21807
  } else {
@@ -21286,14 +22124,15 @@ var SqliteInventoryRepository = class {
21286
22124
  };
21287
22125
 
21288
22126
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21289
- import { randomUUID as randomUUID3 } from "crypto";
22127
+ import { randomUUID as randomUUID4 } from "crypto";
21290
22128
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21291
22129
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21292
22130
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21293
22131
  var HARNESS_LABELS = {
21294
22132
  claudecode: "Claude Code",
21295
22133
  cursor: "Cursor",
21296
- codex: "Codex"
22134
+ codex: "Codex",
22135
+ antigravity: "Antigravity"
21297
22136
  };
21298
22137
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21299
22138
  var EMPTY_PROJECT_AGG = {
@@ -21308,6 +22147,7 @@ function resolveHarnessId(attrs, row) {
21308
22147
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21309
22148
  if (t.includes("cursor")) return "cursor";
21310
22149
  if (t.includes("codex")) return "codex";
22150
+ if (t.includes("antigravity")) return "antigravity";
21311
22151
  return null;
21312
22152
  }
21313
22153
  function isLiveRealClaudeCode(rows) {
@@ -21766,7 +22606,7 @@ var SqliteInventoryAssetsRepository = class {
21766
22606
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21767
22607
  VALUES (:id, :projectId, :path, :access, :now, :now)
21768
22608
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21769
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22609
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21770
22610
  }
21771
22611
  return true;
21772
22612
  }
@@ -21787,7 +22627,7 @@ var SqliteInventoryAssetsRepository = class {
21787
22627
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21788
22628
  VALUES (:id, :assetId, :trust, :now, :now)
21789
22629
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21790
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22630
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21791
22631
  }
21792
22632
  this.configRowsCache = void 0;
21793
22633
  return "ok";
@@ -22084,7 +22924,7 @@ var SqliteInventoryAssetsRepository = class {
22084
22924
  };
22085
22925
 
22086
22926
  // ../../packages/persistence/src/repositories/policies.ts
22087
- import { randomUUID as randomUUID4 } from "crypto";
22927
+ import { randomUUID as randomUUID5 } from "crypto";
22088
22928
  var SqlitePoliciesRepository = class {
22089
22929
  constructor(db) {
22090
22930
  this.db = db;
@@ -22119,7 +22959,7 @@ var SqlitePoliciesRepository = class {
22119
22959
  failOpenTransaction(this.db, () => {
22120
22960
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
22121
22961
  stmt.run({
22122
- id: randomUUID4(),
22962
+ id: randomUUID5(),
22123
22963
  target: JSON.stringify({ category }),
22124
22964
  action,
22125
22965
  now: Date.now()
@@ -22139,7 +22979,7 @@ var SqlitePoliciesRepository = class {
22139
22979
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
22140
22980
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
22141
22981
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
22142
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22982
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
22143
22983
  }
22144
22984
  // Caps every global per-category policy currently set to block/redact down
22145
22985
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22207,7 +23047,7 @@ var SqlitePolicyCatalogRepository = class {
22207
23047
  };
22208
23048
 
22209
23049
  // ../../packages/persistence/src/repositories/project-files.ts
22210
- import { randomUUID as randomUUID5 } from "crypto";
23050
+ import { randomUUID as randomUUID6 } from "crypto";
22211
23051
  var SqliteProjectFilesRepository = class {
22212
23052
  constructor(db) {
22213
23053
  this.db = db;
@@ -22239,7 +23079,7 @@ var SqliteProjectFilesRepository = class {
22239
23079
  const stamp = Math.max(now, maxStamp + 1);
22240
23080
  for (const file2 of scan2.files) {
22241
23081
  this.upsertStmt.run({
22242
- id: randomUUID5(),
23082
+ id: randomUUID6(),
22243
23083
  projectId,
22244
23084
  path: file2.path,
22245
23085
  name: file2.name,
@@ -22253,7 +23093,7 @@ var SqliteProjectFilesRepository = class {
22253
23093
  };
22254
23094
 
22255
23095
  // ../../packages/persistence/src/repositories/resolutions.ts
22256
- import { randomUUID as randomUUID6 } from "crypto";
23096
+ import { randomUUID as randomUUID7 } from "crypto";
22257
23097
  var SqliteResolutionsRepository = class {
22258
23098
  constructor(db, now = () => Date.now()) {
22259
23099
  this.db = db;
@@ -22307,7 +23147,7 @@ var SqliteResolutionsRepository = class {
22307
23147
  */
22308
23148
  insertResolution(r) {
22309
23149
  this.insertStmt.run({
22310
- id: randomUUID6(),
23150
+ id: randomUUID7(),
22311
23151
  findingKey: r.findingKey,
22312
23152
  status: FindingStatus.parse(r.status),
22313
23153
  method: ResolutionMethod.parse(r.method),
@@ -22366,13 +23206,51 @@ var SqliteRuleProbeCacheRepository = class {
22366
23206
  this.readStmt = db.prepare(
22367
23207
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22368
23208
  );
23209
+ this.countQuarantinedStmt = db.prepare(
23210
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23211
+ );
23212
+ this.clearQuarantinedStmt = db.prepare(
23213
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23214
+ );
22369
23215
  }
22370
23216
  db;
22371
23217
  upsertStmt;
22372
23218
  readStmt;
23219
+ countQuarantinedStmt;
23220
+ clearQuarantinedStmt;
22373
23221
  getVerdict(ruleKey) {
22374
23222
  return getRow(this.readStmt, { ruleKey });
22375
23223
  }
23224
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23225
+ countQuarantined() {
23226
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23227
+ }
23228
+ /**
23229
+ * Forgets every quarantine verdict, so the rules behind them are measured
23230
+ * again on the next load. This is the undo for a verdict the machine reached
23231
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23232
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23233
+ * loaded or slow machine can reach about a rule that is in fact fine.
23234
+ *
23235
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23236
+ * keeping, and dropping it would make every rule pay the battery again.
23237
+ *
23238
+ * Reports `refused` from the write's own result rather than inferring it from
23239
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23240
+ * swallows a contended DELETE (another writer holding the lock past
23241
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23242
+ * leaves the count unchanged, which is indistinguishable from "there was
23243
+ * nothing to clear". An undo that reports success while the quarantines are
23244
+ * still in place is worse than one that fails, because the rules it claimed
23245
+ * to restore are silently still disabled.
23246
+ */
23247
+ clearQuarantined() {
23248
+ const before = this.countQuarantined();
23249
+ const committed = failOpenTransaction(this.db, () => {
23250
+ this.clearQuarantinedStmt.run();
23251
+ });
23252
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23253
+ }
22376
23254
  setVerdict(ruleKey, verdict, worstProbeMs2) {
22377
23255
  failOpenTransaction(this.db, () => {
22378
23256
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
@@ -22427,7 +23305,39 @@ var SqliteScanLedgerRepository = class {
22427
23305
  };
22428
23306
 
22429
23307
  // ../../packages/persistence/src/repositories/secret-vault.ts
22430
- import { randomUUID as randomUUID7 } from "crypto";
23308
+ import { randomUUID as randomUUID8 } from "crypto";
23309
+ function pageLimit(requested, fallback) {
23310
+ if (requested === void 0) return fallback;
23311
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23312
+ }
23313
+ function encodeReuseCursor(payload) {
23314
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23315
+ }
23316
+ function decodeReuseCursor(cursor) {
23317
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23318
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23319
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23320
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23321
+ // malformed cursor must never produce, since restarting from the top is the
23322
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23323
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23324
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23325
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23326
+ }
23327
+ return null;
23328
+ }
23329
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23330
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23331
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23332
+ v.occurrence_count, v.first_seen, v.last_seen`;
23333
+ function toSighting(row) {
23334
+ return {
23335
+ location: row.location,
23336
+ kind: row.kind,
23337
+ firstSeen: new Date(row.first_seen).toISOString(),
23338
+ lastSeen: new Date(row.last_seen).toISOString()
23339
+ };
23340
+ }
22431
23341
  var SELECT_COLUMNS = `
22432
23342
  pointer_id AS pointerId,
22433
23343
  value_fingerprint AS valueFingerprint,
@@ -22611,39 +23521,67 @@ var SqliteSecretVaultRepository = class {
22611
23521
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
22612
23522
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22613
23523
  ).run({
22614
- id: randomUUID7(),
23524
+ id: randomUUID8(),
22615
23525
  pointerId: entry.pointerId,
22616
23526
  location: entry.location,
22617
23527
  kind: entry.kind,
22618
23528
  now
22619
23529
  });
22620
23530
  }
22621
- listSightings(pointerId) {
23531
+ /**
23532
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23533
+ * than one query per row. A pointer with no sightings still gets an entry, so
23534
+ * the caller never has to distinguish "none" from "missing".
23535
+ *
23536
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23537
+ * the instance the way the fixed-shape ones in the constructor are.
23538
+ */
23539
+ sightingsFor(pointerIds) {
23540
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23541
+ if (pointerIds.length === 0) return byPointer;
22622
23542
  const rows = allRows(
22623
23543
  this.db.prepare(
22624
- `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22625
- WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
23544
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23545
+ FROM secret_vault_sighting
23546
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23547
+ ORDER BY last_seen DESC`
22626
23548
  ),
22627
- { pointerId }
23549
+ pointerIds
22628
23550
  );
23551
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23552
+ return byPointer;
23553
+ }
23554
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23555
+ toInventoryEntries(rows) {
23556
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
22629
23557
  return rows.map((r) => ({
22630
- location: r.location,
22631
- kind: r.kind,
23558
+ pointerId: r.pointer_id,
23559
+ category: r.category,
23560
+ ...r.provider === null ? {} : { provider: r.provider },
23561
+ maskedMatch: r.masked_match,
23562
+ occurrences: r.occurrence_count,
22632
23563
  firstSeen: new Date(r.first_seen).toISOString(),
22633
- lastSeen: new Date(r.last_seen).toISOString()
23564
+ lastSeen: new Date(r.last_seen).toISOString(),
23565
+ revealGrantId: r.grant_id,
23566
+ sightings: sightings.get(r.pointer_id) ?? []
22634
23567
  }));
22635
23568
  }
22636
23569
  /**
22637
- * The dashboard inventory: every vaulted value's descriptor data joined with
22638
- * its sightings and the active reveal-to-model grant when one exists.
22639
- * Raw-free by construction — neither the fingerprint nor the ciphertext
22640
- * columns are selected.
23570
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23571
+ * value's descriptor data joined with its sightings and the active
23572
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23573
+ * the fingerprint nor the ciphertext columns are selected.
23574
+ *
23575
+ * `totals.values` counts the whole store, not the page, so the count a reader
23576
+ * sees never depends on how far they have paged.
22641
23577
  */
22642
- listInventory(now = Date.now()) {
23578
+ listInventory(query = {}, now = Date.now()) {
23579
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23580
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23581
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
22643
23582
  const rows = allRows(
22644
23583
  this.db.prepare(
22645
- `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22646
- v.occurrence_count, v.first_seen, v.last_seen,
23584
+ `SELECT ${INVENTORY_COLUMNS},
22647
23585
  (SELECT e.id FROM exceptions e
22648
23586
  WHERE e.rule_id = v.rule_id
22649
23587
  AND e.value_fingerprint = v.value_fingerprint
@@ -22651,45 +23589,109 @@ var SqliteSecretVaultRepository = class {
22651
23589
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22652
23590
  LIMIT 1) AS grant_id
22653
23591
  FROM secret_vault v
22654
- ORDER BY v.last_seen DESC`
23592
+ ${where}
23593
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23594
+ LIMIT :limit`
22655
23595
  ),
22656
- { now }
23596
+ bindParams({
23597
+ now,
23598
+ limit: limit + 1,
23599
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23600
+ })
22657
23601
  );
22658
- return rows.map((r) => ({
22659
- pointerId: r.pointer_id,
22660
- category: r.category,
22661
- ...r.provider === null ? {} : { provider: r.provider },
22662
- maskedMatch: r.masked_match,
22663
- occurrences: r.occurrence_count,
22664
- firstSeen: new Date(r.first_seen).toISOString(),
22665
- lastSeen: new Date(r.last_seen).toISOString(),
22666
- revealGrantId: r.grant_id,
22667
- sightings: this.listSightings(r.pointer_id)
22668
- }));
23602
+ const hasMore = rows.length > limit;
23603
+ const page = hasMore ? rows.slice(0, limit) : rows;
23604
+ const last = page[page.length - 1];
23605
+ return {
23606
+ totals: { values: this.countEntries() },
23607
+ items: this.toInventoryEntries(page),
23608
+ // Minted from the last row of the PAGE, never the extra probe row.
23609
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23610
+ };
22669
23611
  }
22670
23612
  /**
22671
- * The de-reference trail, newest first. By default the batched, high-volume
22672
- * reasons (display, view-render) are hidden and counted instead the rows
22673
- * that matter as a signal are the model crossings, and burying them under
22674
- * render noise would defeat the audit's purpose.
23613
+ * Values reused on this machine detected more than once, or written to more
23614
+ * than one location most-reused first, one page at a time.
23615
+ *
23616
+ * Its own read rather than a filter over an inventory page: reuse is a
23617
+ * property of the whole store, and deriving it from 50 newest rows would
23618
+ * under-report exactly the values a reader most needs to see.
22675
23619
  */
22676
- listDerefs(opts) {
22677
- const limit = opts?.limit ?? 200;
22678
- const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
23620
+ listReuse(query = {}, now = Date.now()) {
23621
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23622
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23623
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23624
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23625
+ const rows = allRows(
23626
+ this.db.prepare(
23627
+ `SELECT ${INVENTORY_COLUMNS},
23628
+ (SELECT e.id FROM exceptions e
23629
+ WHERE e.rule_id = v.rule_id
23630
+ AND e.value_fingerprint = v.value_fingerprint
23631
+ AND e.key_version = v.fingerprint_key_version
23632
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23633
+ LIMIT 1) AS grant_id
23634
+ FROM secret_vault v
23635
+ WHERE ${REUSED_PREDICATE} ${after}
23636
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23637
+ LIMIT :limit`
23638
+ ),
23639
+ bindParams({
23640
+ now,
23641
+ limit: limit + 1,
23642
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23643
+ })
23644
+ );
23645
+ const hasMore = rows.length > limit;
23646
+ const page = hasMore ? rows.slice(0, limit) : rows;
23647
+ const last = page[page.length - 1];
23648
+ return {
23649
+ totals: { reused: this.countReused() },
23650
+ items: this.toInventoryEntries(page),
23651
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23652
+ };
23653
+ }
23654
+ /**
23655
+ * The de-reference trail, newest first, one page at a time. By default the
23656
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23657
+ * instead — the rows that matter as a signal are the model crossings, and
23658
+ * burying them under render noise would defeat the audit's purpose.
23659
+ *
23660
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23661
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23662
+ * the reader pages.
23663
+ */
23664
+ listDerefs(query = {}) {
23665
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23666
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23667
+ const conditions = [];
23668
+ if (query.includeBatched !== true) {
23669
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23670
+ }
23671
+ if (cursor !== null) {
23672
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23673
+ }
23674
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
22679
23675
  const rows = allRows(
22680
23676
  this.db.prepare(
22681
23677
  `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22682
23678
  FROM secret_vault_deref ${where}
22683
- ORDER BY at DESC, rowid DESC LIMIT :limit`
23679
+ ORDER BY at DESC, id DESC LIMIT :limit`
22684
23680
  ),
22685
- { limit }
23681
+ bindParams({
23682
+ limit: limit + 1,
23683
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23684
+ })
22686
23685
  );
22687
- const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
23686
+ const hasMore = rows.length > limit;
23687
+ const page = hasMore ? rows.slice(0, limit) : rows;
23688
+ const last = page[page.length - 1];
23689
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
22688
23690
  this.db,
22689
23691
  `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22690
23692
  );
22691
23693
  return {
22692
- rows: rows.map((r) => ({
23694
+ items: page.map((r) => ({
22693
23695
  id: r.id,
22694
23696
  pointerId: r.pointer_id,
22695
23697
  at: new Date(r.at).toISOString(),
@@ -22699,12 +23701,20 @@ var SqliteSecretVaultRepository = class {
22699
23701
  ...r.grant_id === null ? {} : { grantId: r.grant_id },
22700
23702
  pointerCount: r.pointer_count
22701
23703
  })),
23704
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
22702
23705
  hiddenBatched
22703
23706
  };
22704
23707
  }
22705
23708
  countEntries() {
22706
23709
  return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22707
23710
  }
23711
+ /** Values reused on this machine — the reuse list's page-independent total. */
23712
+ countReused() {
23713
+ return countScalar(
23714
+ this.db,
23715
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23716
+ );
23717
+ }
22708
23718
  };
22709
23719
 
22710
23720
  // ../../packages/persistence/src/repositories/security.ts
@@ -22719,7 +23729,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22719
23729
  var SCAN_COVERAGE = [
22720
23730
  { provider: "claudecode", coverage: 100, supported: true },
22721
23731
  { provider: "cursor", coverage: 0, supported: false },
22722
- { provider: "codex", coverage: 0, supported: false },
23732
+ { provider: "codex", coverage: 80, supported: true },
23733
+ { provider: "antigravity", coverage: 60, supported: true },
23734
+ { provider: "claudeai", coverage: 0, supported: false },
22723
23735
  { provider: "chatgpt", coverage: 0, supported: false },
22724
23736
  { provider: "copilot", coverage: 0, supported: false },
22725
23737
  { provider: "api", coverage: 0, supported: false }
@@ -23052,7 +24064,7 @@ var SqliteSecurityRepository = class {
23052
24064
  };
23053
24065
 
23054
24066
  // ../../packages/persistence/src/repositories/shares.ts
23055
- import { randomUUID as randomUUID8 } from "crypto";
24067
+ import { randomUUID as randomUUID9 } from "crypto";
23056
24068
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
23057
24069
  var IN_CHUNK = 500;
23058
24070
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -23308,7 +24320,7 @@ var SqliteSharesRepository = class {
23308
24320
  (id, destination_id, host, decision, created_at, updated_at)
23309
24321
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
23310
24322
  ).run({
23311
- id: randomUUID8(),
24323
+ id: randomUUID9(),
23312
24324
  destinationId,
23313
24325
  host: dest.host,
23314
24326
  decision,
@@ -23457,7 +24469,7 @@ var SqliteSharesRepository = class {
23457
24469
  let destinationId = destIds.get(hit.host);
23458
24470
  if (destinationId === void 0) {
23459
24471
  destStmt.run({
23460
- id: randomUUID8(),
24472
+ id: randomUUID9(),
23461
24473
  kind: hit.kind,
23462
24474
  name: hit.name,
23463
24475
  host: hit.host,
@@ -23473,7 +24485,7 @@ var SqliteSharesRepository = class {
23473
24485
  let endpointId = endpointIds.get(endpointKey);
23474
24486
  if (endpointId === void 0) {
23475
24487
  endpointStmt.run({
23476
- id: randomUUID8(),
24488
+ id: randomUUID9(),
23477
24489
  destinationId,
23478
24490
  method: hit.method,
23479
24491
  transport: hit.transport,
@@ -23486,7 +24498,7 @@ var SqliteSharesRepository = class {
23486
24498
  endpointIds.set(endpointKey, endpointId);
23487
24499
  }
23488
24500
  siteStmt.run({
23489
- id: randomUUID8(),
24501
+ id: randomUUID9(),
23490
24502
  endpointId,
23491
24503
  project: input.project,
23492
24504
  projectKey: input.projectKey,
@@ -23851,6 +24863,9 @@ function purgeSampleData(db) {
23851
24863
  }
23852
24864
 
23853
24865
  // ../../packages/persistence/src/database.ts
24866
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24867
+ "aka.persistence.unsafeTestOnlyRawHandle"
24868
+ );
23854
24869
  function linkHost(input, hostId) {
23855
24870
  return hostId ? { ...input, hostId } : input;
23856
24871
  }
@@ -23872,21 +24887,34 @@ function openWithPragmas(file2) {
23872
24887
  }
23873
24888
  return db;
23874
24889
  }
23875
- function backupLegacyStore(file2) {
23876
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23877
- renameSync2(file2, backup);
23878
- tightenFile(backup);
23879
- for (const sidecar of dbSidecars(file2)) {
23880
- if (existsSync(sidecar)) rmSync2(sidecar);
24890
+ function backupLegacyStore(db, file2) {
24891
+ reapStalePartials(file2);
24892
+ const backup = backupPath(file2, "legacy");
24893
+ let snapshotted = false;
24894
+ let snapshotError;
24895
+ try {
24896
+ snapshotStore(db, backup);
24897
+ snapshotted = true;
24898
+ } catch (error51) {
24899
+ snapshotError = error51;
24900
+ } finally {
24901
+ db.close();
23881
24902
  }
24903
+ if (!snapshotted) {
24904
+ akaWarn(
24905
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24906
+ );
24907
+ moveStoreAside(file2, backup);
24908
+ return backup;
24909
+ }
24910
+ discardStore(file2, backup);
23882
24911
  return backup;
23883
24912
  }
23884
24913
  function openAndInitialize(file2) {
23885
24914
  let db = openWithPragmas(file2);
23886
24915
  try {
23887
24916
  if (isForeignSqliteLineage(db)) {
23888
- db.close();
23889
- const backup = backupLegacyStore(file2);
24917
+ const backup = backupLegacyStore(db, file2);
23890
24918
  db = openWithPragmas(file2);
23891
24919
  akaWarn(
23892
24920
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23930,7 +24958,7 @@ function openAndInitialize(file2) {
23930
24958
  }
23931
24959
  function openLocalDatabase(dir) {
23932
24960
  ensureDataDirSync(dir);
23933
- const file2 = join(dir, DB_FILENAME);
24961
+ const file2 = join2(dir, DB_FILENAME);
23934
24962
  const {
23935
24963
  db,
23936
24964
  events,
@@ -24047,7 +25075,7 @@ function openLocalDatabase(dir) {
24047
25075
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
24048
25076
  if (!definitionId) continue;
24049
25077
  inspectionFindings.insertFinding({
24050
- id: randomUUID9(),
25078
+ id: randomUUID10(),
24051
25079
  auditEventId: record2.scanEvent.id,
24052
25080
  inspectionDefinitionId: definitionId,
24053
25081
  span: finding.span,
@@ -24153,7 +25181,9 @@ function openLocalDatabase(dir) {
24153
25181
  transaction,
24154
25182
  close: () => {
24155
25183
  db.close();
24156
- }
25184
+ },
25185
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25186
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
24157
25187
  };
24158
25188
  }
24159
25189
 
@@ -24177,6 +25207,20 @@ var UserGrantPolicyProvider = class {
24177
25207
  }
24178
25208
  };
24179
25209
 
25210
+ // ../../packages/persistence/src/file-lock.ts
25211
+ import { randomUUID as randomUUID11 } from "crypto";
25212
+ import {
25213
+ closeSync,
25214
+ existsSync as existsSync2,
25215
+ openSync,
25216
+ readFileSync,
25217
+ rmSync as rmSync3,
25218
+ statSync as statSync2,
25219
+ writeFileSync as writeFileSync2
25220
+ } from "fs";
25221
+ import { hostname as hostname3 } from "os";
25222
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25223
+
24180
25224
  // ../../packages/persistence/src/finding-key.ts
24181
25225
  import { createHash as createHash3 } from "crypto";
24182
25226
  function normalizeFilePath(filePath) {
@@ -24189,13 +25233,13 @@ function computeFindingKey(input) {
24189
25233
 
24190
25234
  // ../../packages/persistence/src/fingerprint.ts
24191
25235
  import { createHmac, randomBytes } from "crypto";
24192
- import { existsSync as existsSync2, readFileSync } from "fs";
24193
- import { join as join2 } from "path";
25236
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25237
+ import { join as join3 } from "path";
24194
25238
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
24195
- var KEY_FILENAME = "exception.key";
25239
+ var EXCEPTION_KEY_FILENAME = "exception.key";
24196
25240
  var KEY_MATERIAL_BYTES = 32;
24197
25241
  function keyFilePath(dataDir2) {
24198
- return join2(dataDir2, KEY_FILENAME);
25242
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
24199
25243
  }
24200
25244
  function parseKeyFile(raw) {
24201
25245
  const parsed = JSON.parse(raw);
@@ -24233,8 +25277,8 @@ var FloorUnreadableError = class extends Error {
24233
25277
  }
24234
25278
  };
24235
25279
  function storedKeyVersionFloor(dataDir2) {
24236
- const file2 = join2(dataDir2, DB_FILENAME);
24237
- if (!existsSync2(file2)) return 0;
25280
+ const file2 = join3(dataDir2, DB_FILENAME);
25281
+ if (!existsSync3(file2)) return 0;
24238
25282
  let db;
24239
25283
  try {
24240
25284
  db = new DatabaseSync2(file2, { readOnly: true });
@@ -24259,18 +25303,36 @@ function storedKeyVersionFloor(dataDir2) {
24259
25303
  db?.close();
24260
25304
  }
24261
25305
  }
24262
- function writeKeyFile(dataDir2, key) {
25306
+ function serializeKey(key) {
25307
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
25308
+ }
25309
+ function createKeyFile(dataDir2, key) {
24263
25310
  ensureDataDirSync(dataDir2);
24264
25311
  const file2 = keyFilePath(dataDir2);
24265
- const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
24266
- writeOwnerOnlyFileSync(file2, `${body}
24267
- `);
24268
- return key;
25312
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
25313
+ `)) return key;
25314
+ const winner = readFingerprintKey(dataDir2);
25315
+ if (winner) {
25316
+ tightenFile(file2);
25317
+ return winner;
25318
+ }
25319
+ const occupant = classifyOccupant(file2);
25320
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
25321
+ }
25322
+ function occupantMessage(file2, kind) {
25323
+ switch (kind) {
25324
+ case "symlink":
25325
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
25326
+ case "gone":
25327
+ return "exception key file was removed while it was being created";
25328
+ case "unknown":
25329
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
25330
+ }
24269
25331
  }
24270
25332
  function readFingerprintKey(dataDir2) {
24271
25333
  let raw;
24272
25334
  try {
24273
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25335
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
24274
25336
  } catch (err) {
24275
25337
  if (err.code === "ENOENT") return null;
24276
25338
  throw err instanceof Error ? err : new Error(String(err));
@@ -24283,7 +25345,7 @@ function loadOrCreateFingerprintKey(dataDir2) {
24283
25345
  tightenFile(keyFilePath(dataDir2));
24284
25346
  return existing;
24285
25347
  }
24286
- return writeKeyFile(dataDir2, {
25348
+ return createKeyFile(dataDir2, {
24287
25349
  version: storedKeyVersionFloor(dataDir2) + 1,
24288
25350
  material: randomBytes(KEY_MATERIAL_BYTES)
24289
25351
  });
@@ -24296,21 +25358,21 @@ function fingerprintValue(key, raw) {
24296
25358
  import { renameSync as renameSync3 } from "fs";
24297
25359
  import { mkdir } from "fs/promises";
24298
25360
  import { homedir } from "os";
24299
- import { join as join3 } from "path";
25361
+ import { join as join4 } from "path";
24300
25362
  function defaultDataDir() {
24301
- return join3(homedir(), ".aka");
25363
+ return join4(homedir(), ".aka");
24302
25364
  }
24303
25365
  function settingsDir(base = defaultDataDir()) {
24304
- return join3(base, "settings");
25366
+ return join4(base, "settings");
24305
25367
  }
24306
25368
  function dataDir(base = defaultDataDir()) {
24307
- return join3(base, "data");
25369
+ return join4(base, "data");
24308
25370
  }
24309
25371
  function dbPath(base = defaultDataDir()) {
24310
- return join3(dataDir(base), "aka.db");
25372
+ return join4(dataDir(base), "aka.db");
24311
25373
  }
24312
25374
  function keysDir(base = defaultDataDir()) {
24313
- return join3(base, "keys");
25375
+ return join4(base, "keys");
24314
25376
  }
24315
25377
  function ensureLayoutDirSync(dir = defaultDataDir()) {
24316
25378
  ensureDataDirSync(dir);
@@ -24323,8 +25385,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24323
25385
  for (const { name, dest } of moves) {
24324
25386
  try {
24325
25387
  ensureDataDirSync(dest);
24326
- const moved = join3(dest, name);
24327
- renameSync3(join3(base, name), moved);
25388
+ const moved = join4(dest, name);
25389
+ renameSync3(join4(base, name), moved);
24328
25390
  tightenFile(moved);
24329
25391
  } catch {
24330
25392
  }
@@ -24332,10 +25394,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24332
25394
  }
24333
25395
 
24334
25396
  // ../../packages/persistence/src/settings.ts
24335
- import { readFileSync as readFileSync2 } from "fs";
24336
- import { join as join4 } from "path";
25397
+ import { readFileSync as readFileSync3 } from "fs";
25398
+ import { join as join5 } from "path";
25399
+ var SETTINGS_FILENAME = "settings.json";
24337
25400
  function readWorkspaceSettings(base = defaultDataDir()) {
24338
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25401
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
24339
25402
  if (!record2) return defaultWorkspaceSettings();
24340
25403
  try {
24341
25404
  return WorkspaceSettings.parse(record2);
@@ -24346,7 +25409,7 @@ function readWorkspaceSettings(base = defaultDataDir()) {
24346
25409
  function readJson(file2) {
24347
25410
  let text;
24348
25411
  try {
24349
- text = readFileSync2(file2, "utf8");
25412
+ text = readFileSync3(file2, "utf8");
24350
25413
  } catch {
24351
25414
  return null;
24352
25415
  }
@@ -24467,13 +25530,18 @@ import { randomBytes as randomBytes2 } from "crypto";
24467
25530
  import {
24468
25531
  chmodSync as chmodSync2,
24469
25532
  mkdirSync as mkdirSync2,
24470
- readFileSync as readFileSync3,
25533
+ readFileSync as readFileSync4,
24471
25534
  renameSync as renameSync4,
24472
- rmSync as rmSync3,
24473
- statSync,
24474
- writeFileSync as writeFileSync2
25535
+ rmSync as rmSync4,
25536
+ statSync as statSync3,
25537
+ writeFileSync as writeFileSync3
24475
25538
  } from "fs";
24476
- import { join as join5 } from "path";
25539
+ import { join as join6 } from "path";
25540
+ var VAULT_OCCUPANT_REASON = {
25541
+ symlink: "the path is a symlink; remove it so a keyring can be created",
25542
+ gone: "the path was occupied but holds no keyring (removed while it was being created)",
25543
+ unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
25544
+ };
24477
25545
  var VaultKeyEpochMissingError = class extends Error {
24478
25546
  version;
24479
25547
  constructor(version2) {
@@ -24566,28 +25634,28 @@ function claimRotationLock(lock, owner) {
24566
25634
  throw asError(err);
24567
25635
  }
24568
25636
  try {
24569
- writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
25637
+ writeFileSync3(join6(lock, LOCK_OWNER_FILE), `${owner}
24570
25638
  `, { mode: DATA_FILE_MODE });
24571
25639
  return true;
24572
25640
  } catch (err) {
24573
- rmSync3(lock, { recursive: true, force: true });
25641
+ rmSync4(lock, { recursive: true, force: true });
24574
25642
  throw asError(err);
24575
25643
  }
24576
25644
  }
24577
25645
  function acquireRotationLock(keysDir2) {
24578
- const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
25646
+ const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
24579
25647
  const owner = randomBytes2(16).toString("hex");
24580
25648
  if (claimRotationLock(lock, owner)) return { lock, owner };
24581
25649
  let held;
24582
25650
  try {
24583
- held = statSync(lock);
25651
+ held = statSync3(lock);
24584
25652
  } catch {
24585
25653
  throw new Error(ROTATION_IN_PROGRESS);
24586
25654
  }
24587
25655
  if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
24588
25656
  const aside = `${lock}.stale.${owner}`;
24589
25657
  try {
24590
- const now = statSync(lock);
25658
+ const now = statSync3(lock);
24591
25659
  if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
24592
25660
  throw new Error(ROTATION_IN_PROGRESS);
24593
25661
  }
@@ -24596,17 +25664,17 @@ function acquireRotationLock(keysDir2) {
24596
25664
  if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
24597
25665
  throw new Error(ROTATION_IN_PROGRESS, { cause: err });
24598
25666
  }
24599
- rmSync3(aside, { recursive: true, force: true });
25667
+ rmSync4(aside, { recursive: true, force: true });
24600
25668
  if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
24601
25669
  return { lock, owner };
24602
25670
  }
24603
25671
  function releaseRotationLock(lease) {
24604
25672
  try {
24605
- if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
25673
+ if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
24606
25674
  } catch {
24607
25675
  return;
24608
25676
  }
24609
- rmSync3(lease.lock, { recursive: true, force: true });
25677
+ rmSync4(lease.lock, { recursive: true, force: true });
24610
25678
  }
24611
25679
  function withRotationLock(keysDir2, work) {
24612
25680
  ensureDataDirSync(keysDir2);
@@ -24623,7 +25691,7 @@ var FileKeyProvider = class {
24623
25691
  this.#keysDir = keysDir2;
24624
25692
  }
24625
25693
  get filePath() {
24626
- return join5(this.#keysDir, VAULT_KEY_FILENAME);
25694
+ return join6(this.#keysDir, VAULT_KEY_FILENAME);
24627
25695
  }
24628
25696
  loadOrCreate() {
24629
25697
  return asAsync(() => {
@@ -24653,7 +25721,7 @@ var FileKeyProvider = class {
24653
25721
  #read() {
24654
25722
  let raw;
24655
25723
  try {
24656
- raw = readFileSync3(this.filePath, "utf8");
25724
+ raw = readFileSync4(this.filePath, "utf8");
24657
25725
  } catch (err) {
24658
25726
  if (err.code === "ENOENT") return null;
24659
25727
  throw err instanceof Error ? err : new Error(String(err));
@@ -24661,34 +25729,32 @@ var FileKeyProvider = class {
24661
25729
  return parseKeyring(raw);
24662
25730
  }
24663
25731
  /**
24664
- * First mint: the keyring is created at its FINAL path with a
24665
- * creation-exclusive write, so two processes racing a fresh machine cannot
24666
- * each mint a different epoch 1 with tmp + rename the loser's replace
24667
- * would orphan everything the winner had already sealed. On EEXIST the
24668
- * loser re-reads and adopts the winner's keyring; it minted nothing.
24669
- * Atomic replace is unnecessary here: nothing can be mid-read of a file
24670
- * that did not exist, and a torn exclusive write parses as corrupt on the
24671
- * next read and fails secure rather than being re-minted over.
25732
+ * First mint: the keyring is CREATED, never replaced, so two processes racing
25733
+ * a fresh machine cannot each mint a different epoch 1 — with tmp + rename
25734
+ * the loser's replace would orphan everything the winner had already sealed.
25735
+ * The loser re-reads and adopts the winner's keyring; it minted nothing.
25736
+ *
25737
+ * `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
25738
+ * at the final path, so the keyring never exists at zero length: a reader —
25739
+ * including the loser, re-reading in order to adopt sees the file absent or
25740
+ * whole, and never mistakes a live keyring for a corrupt one. A corrupt file
25741
+ * still throws from the parse and is never re-minted over.
24672
25742
  */
24673
25743
  #createExclusive() {
24674
25744
  ensureDataDirSync(this.#keysDir);
24675
25745
  const keyring = mintKeyring();
24676
- try {
24677
- writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
24678
- `, {
24679
- flag: "wx",
24680
- mode: DATA_FILE_MODE
24681
- });
24682
- } catch (err) {
24683
- if (err.code !== "EEXIST") throw asError(err);
24684
- const winner = this.#read();
24685
- if (!winner) {
24686
- throw new Error("vault: key file vanished during first mint", { cause: err });
24687
- }
24688
- return winner;
25746
+ if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
25747
+ `)) return keyring;
25748
+ const winner = this.#read();
25749
+ if (!winner) {
25750
+ const occupant = classifyOccupant(this.filePath);
25751
+ throw new KeyUnclaimableError(
25752
+ `vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
25753
+ occupant.cause
25754
+ );
24689
25755
  }
24690
25756
  tightenFileMode(this.filePath);
24691
- return keyring;
25757
+ return winner;
24692
25758
  }
24693
25759
  /**
24694
25760
  * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
@@ -24699,7 +25765,7 @@ var FileKeyProvider = class {
24699
25765
  ensureDataDirSync(this.#keysDir);
24700
25766
  const file2 = this.filePath;
24701
25767
  const tmp = `${file2}.tmp`;
24702
- writeFileSync2(tmp, `${serializeKeyring(keyring)}
25768
+ writeFileSync3(tmp, `${serializeKeyring(keyring)}
24703
25769
  `, { mode: DATA_FILE_MODE });
24704
25770
  renameSync4(tmp, file2);
24705
25771
  tightenFileMode(file2);
@@ -24825,7 +25891,7 @@ function createKeyProvider(custody, keysDir2) {
24825
25891
  }
24826
25892
 
24827
25893
  // ../../packages/persistence/src/vault/vault.ts
24828
- import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
25894
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
24829
25895
  var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
24830
25896
  var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
24831
25897
  var VAULT_PURGE_POINTER_ID = "*";
@@ -24852,14 +25918,12 @@ function parsePointer(token) {
24852
25918
  var SecretVault = class {
24853
25919
  #repo;
24854
25920
  #keys;
24855
- #fingerprintKey;
24856
25921
  #isConsented;
24857
25922
  #verifyGrant;
24858
25923
  #now;
24859
25924
  constructor(deps) {
24860
25925
  this.#repo = deps.repo;
24861
25926
  this.#keys = deps.keys;
24862
- this.#fingerprintKey = deps.fingerprintKey;
24863
25927
  this.#isConsented = deps.isConsented;
24864
25928
  this.#verifyGrant = deps.verifyGrant;
24865
25929
  this.#now = deps.now ?? (() => Date.now());
@@ -24868,10 +25932,23 @@ var SecretVault = class {
24868
25932
  * Store a value and return the pointer that stands for it. The same value
24869
25933
  * always yields the same pointer on this machine — one row, one pointer id,
24870
25934
  * one category — which is what makes dedup and reuse counting work.
25935
+ *
25936
+ * `fingerprintKey` is the exception-key epoch this value's fingerprint is
25937
+ * derived under — a different key from the vault's, with different rotation
25938
+ * semantics. It is a parameter of the WRITE rather than a constructor dep,
25939
+ * and a thunk rather than a value, so that the only way to reach a key is to
25940
+ * store something: a read-only caller never names it, and a caller whose
25941
+ * source mints on absence mints only once consent has actually opened the
25942
+ * write. `refreshFingerprints` takes its key the same way, for the same
25943
+ * reason.
25944
+ *
25945
+ * Resolved once per call, so the fingerprint and the version it is recorded
25946
+ * under can never come from two different epochs.
24871
25947
  */
24872
- async tokenize(raw, meta3) {
25948
+ async tokenize(raw, meta3, fingerprintKey) {
24873
25949
  if (!this.#isConsented()) return CONSENT_ABSENT;
24874
- const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
25950
+ const fpKey = fingerprintKey();
25951
+ const valueFingerprint = fingerprintValue(fpKey, raw);
24875
25952
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
24876
25953
  const now = this.#now();
24877
25954
  if (existing) {
@@ -24887,7 +25964,7 @@ var SecretVault = class {
24887
25964
  {
24888
25965
  pointerId: base32Encode(pointerId),
24889
25966
  valueFingerprint,
24890
- fingerprintKeyVersion: this.#fingerprintKey.version,
25967
+ fingerprintKeyVersion: fpKey.version,
24891
25968
  keyVersion: version2,
24892
25969
  // Recorded so the row stays OPENABLE if the wire-format constant ever
24893
25970
  // moves: it is part of this row's AEAD AAD. It is not a tag input —
@@ -25131,7 +26208,7 @@ var SecretVault = class {
25131
26208
  purgeVault() {
25132
26209
  const destroyed = this.#repo.purgeAll();
25133
26210
  this.#repo.recordDeref({
25134
- id: randomUUID10(),
26211
+ id: randomUUID12(),
25135
26212
  pointerId: VAULT_PURGE_POINTER_ID,
25136
26213
  at: this.#now(),
25137
26214
  target: "human",
@@ -25200,7 +26277,7 @@ var SecretVault = class {
25200
26277
  }
25201
26278
  #audit(pointerId, opts, outcome) {
25202
26279
  this.#repo.recordDeref({
25203
- id: randomUUID10(),
26280
+ id: randomUUID12(),
25204
26281
  pointerId,
25205
26282
  at: this.#now(),
25206
26283
  target: opts.target,
@@ -25215,15 +26292,15 @@ var SecretVault = class {
25215
26292
  };
25216
26293
 
25217
26294
  // ../../packages/persistence/src/warn-era-cap.ts
25218
- import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25219
- import { join as join6 } from "path";
26295
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
26296
+ import { join as join7 } from "path";
25220
26297
  var MARKER = "warn-era-capped";
25221
26298
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25222
26299
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25223
- const marker = join6(dataDir2, MARKER);
25224
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
26300
+ const marker = join7(dataDir2, MARKER);
26301
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
25225
26302
  const capped = db.policies.capCategoryActions();
25226
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
26303
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
25227
26304
  `, { mode: DATA_FILE_MODE });
25228
26305
  return { capped };
25229
26306
  }
@@ -25277,11 +26354,11 @@ function resolveProvider() {
25277
26354
  }
25278
26355
 
25279
26356
  // ../../packages/plugin-sdk/src/config.ts
25280
- function loadConfig(base = defaultDataDir()) {
26357
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
25281
26358
  try {
25282
26359
  ensureLayoutDirSync(base);
25283
- const settingsFile = join7(settingsDir(base), "settings.json");
25284
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
26360
+ const settingsFile = join8(settingsDir(base), "settings.json");
26361
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
25285
26362
  } catch {
25286
26363
  }
25287
26364
  migrateLegacyLayout(base);
@@ -25292,21 +26369,21 @@ function loadConfig(base = defaultDataDir()) {
25292
26369
  dbPath: dbPath(base),
25293
26370
  settingsDir: settingsDir(base),
25294
26371
  onboarded: settings.onboardedAt != null,
25295
- provider: resolveProviderSafe()
26372
+ provider: resolveProviderSafe(resolveProviderFn)
25296
26373
  };
25297
26374
  }
25298
- function resolveProviderSafe() {
26375
+ function resolveProviderSafe(resolveProviderFn) {
25299
26376
  try {
25300
- return resolveProvider();
26377
+ return resolveProviderFn();
25301
26378
  } catch {
25302
26379
  return { provider: "anthropic" };
25303
26380
  }
25304
26381
  }
25305
26382
 
25306
26383
  // ../../packages/plugin-sdk/src/config-inventory.ts
25307
- import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
26384
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
25308
26385
  import { homedir as homedir2 } from "os";
25309
- import { basename as basename2, join as join9 } from "path";
26386
+ import { basename as basename3, join as join10 } from "path";
25310
26387
 
25311
26388
  // ../../packages/detections/src/egress/registry.ts
25312
26389
  var EXTRACTOR_VERSION = "1";
@@ -27963,7 +29040,7 @@ var gcp_service_account_default = {
27963
29040
  severity: "critical",
27964
29041
  matcher: {
27965
29042
  type: "regex",
27966
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
29043
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
27967
29044
  flags: "g"
27968
29045
  },
27969
29046
  examples: [
@@ -28345,15 +29422,15 @@ function uniqueRuleIds(findings) {
28345
29422
  }
28346
29423
 
28347
29424
  // ../../packages/plugin-sdk/src/repo.ts
28348
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28349
- import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
29425
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
29426
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
28350
29427
  function resolveRepo(cwd) {
28351
29428
  try {
28352
29429
  const root = findGitRoot(cwd);
28353
29430
  if (!root) return void 0;
28354
29431
  const ctx = resolveGitContext(root);
28355
29432
  const url2 = ctx ? remoteUrl(ctx) : void 0;
28356
- return (url2 ? slugFromUrl(url2) : void 0) ?? basename(ctx?.headRoot ?? root);
29433
+ return (url2 ? slugFromUrl(url2) : void 0) ?? basename2(ctx?.headRoot ?? root);
28357
29434
  } catch {
28358
29435
  return void 0;
28359
29436
  }
@@ -28361,36 +29438,36 @@ function resolveRepo(cwd) {
28361
29438
  function findGitRoot(start) {
28362
29439
  let dir = start;
28363
29440
  for (; ; ) {
28364
- if (existsSync5(join8(dir, ".git"))) return dir;
28365
- const parent = dirname(dir);
29441
+ if (existsSync6(join9(dir, ".git"))) return dir;
29442
+ const parent = dirname2(dir);
28366
29443
  if (parent === dir) return void 0;
28367
29444
  dir = parent;
28368
29445
  }
28369
29446
  }
28370
29447
  function resolveGitContext(root) {
28371
- const dotGit = join8(root, ".git");
29448
+ const dotGit = join9(root, ".git");
28372
29449
  try {
28373
- if (statSync2(dotGit).isDirectory()) {
28374
- return { configPath: join8(dotGit, "config"), headRoot: root };
29450
+ if (statSync4(dotGit).isDirectory()) {
29451
+ return { configPath: join9(dotGit, "config"), headRoot: root };
28375
29452
  }
28376
29453
  } catch {
28377
29454
  return void 0;
28378
29455
  }
28379
29456
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
28380
29457
  if (!target) return void 0;
28381
- const gitdir = isAbsolute(target) ? target : join8(root, target);
28382
- if (existsSync5(join8(gitdir, "config"))) {
28383
- return { configPath: join8(gitdir, "config"), headRoot: root };
29458
+ const gitdir = isAbsolute(target) ? target : join9(root, target);
29459
+ if (existsSync6(join9(gitdir, "config"))) {
29460
+ return { configPath: join9(gitdir, "config"), headRoot: root };
28384
29461
  }
28385
- const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
29462
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
28386
29463
  if (!commonRaw) return void 0;
28387
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
28388
- const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
28389
- return { configPath: join8(commonGitDir, "config"), headRoot };
29464
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
29465
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
29466
+ return { configPath: join9(commonGitDir, "config"), headRoot };
28390
29467
  }
28391
29468
  function safeRead(path) {
28392
29469
  try {
28393
- return readFileSync4(path, "utf8");
29470
+ return readFileSync5(path, "utf8");
28394
29471
  } catch {
28395
29472
  return void 0;
28396
29473
  }
@@ -28428,13 +29505,13 @@ function slugFromUrl(url2) {
28428
29505
  }
28429
29506
 
28430
29507
  // ../../packages/plugin-sdk/src/events.ts
28431
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
29508
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28432
29509
  function contentHashOf(text) {
28433
29510
  return createHash4("sha256").update(text).digest("hex");
28434
29511
  }
28435
29512
  function buildIngestEvent(input) {
28436
29513
  return {
28437
- id: randomUUID11(),
29514
+ id: randomUUID13(),
28438
29515
  sourceTool: input.sourceTool,
28439
29516
  kind: input.kind,
28440
29517
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -28445,100 +29522,525 @@ function buildIngestEvent(input) {
28445
29522
  // SDK boot in the fail-open hook path). Preserve any id the caller already set.
28446
29523
  metadata: {
28447
29524
  ...input.metadata,
28448
- correlationId: input.metadata?.correlationId ?? randomUUID11()
29525
+ correlationId: input.metadata?.correlationId ?? randomUUID13()
28449
29526
  }
28450
29527
  };
28451
29528
  }
28452
29529
 
28453
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
28454
- import { arch, hostname as hostname3, platform, release } from "os";
28455
-
28456
- // ../../packages/plugin-sdk/src/nudge.ts
28457
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28458
- import { join as join10 } from "path";
28459
- var NUDGE_MARKER = "nudge-last-session";
28460
- function claimOnboardingNudge(dataDir2, sessionId) {
28461
- return claimOncePerSession(dataDir2, NUDGE_MARKER, sessionId);
29530
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
29531
+ import { existsSync as existsSync7 } from "fs";
29532
+ import { fileURLToPath } from "url";
29533
+ import { Worker } from "worker_threads";
29534
+ var ISOLATED_SCAN_BUDGET_MS = 2e3;
29535
+ var ISOLATED_PROBE_BUDGET_MS = 1e3;
29536
+ var ISOLATED_START_BUDGET_MS = 5e3;
29537
+ var ATTRIBUTION_MIN_RULE_MS = 500;
29538
+ var ATTRIBUTION_MIN_SHARE = 0.5;
29539
+ var resolvedWorkerUrl;
29540
+ function resolveWorkerUrl() {
29541
+ if (resolvedWorkerUrl !== void 0) return resolvedWorkerUrl ?? void 0;
29542
+ for (const name of ["scan-worker.js", "scan-worker.ts"]) {
29543
+ const candidate = new URL(name, import.meta.url);
29544
+ try {
29545
+ if (existsSync7(fileURLToPath(candidate))) {
29546
+ resolvedWorkerUrl = candidate;
29547
+ return candidate;
29548
+ }
29549
+ } catch {
29550
+ }
29551
+ }
29552
+ resolvedWorkerUrl = null;
29553
+ return void 0;
28462
29554
  }
28463
- function claimOncePerSession(dataDir2, marker, sessionId) {
28464
- if (!sessionId) return true;
28465
- const path = join10(dataDir2, marker);
28466
- try {
28467
- if (readFileSync6(path, "utf8") === sessionId) return false;
28468
- } catch {
29555
+ function messageOf(error51) {
29556
+ return error51 instanceof Error ? error51.message : String(error51);
29557
+ }
29558
+ function createIsolatedScanner(data, opts = {}) {
29559
+ const budgetMs = opts.budgetMs ?? ISOLATED_SCAN_BUDGET_MS;
29560
+ const probeBudgetMs = opts.probeBudgetMs ?? ISOLATED_PROBE_BUDGET_MS;
29561
+ const startBudgetMs = opts.startBudgetMs ?? ISOLATED_START_BUDGET_MS;
29562
+ const minAttributionMs = opts.minAttributionMs ?? ATTRIBUTION_MIN_RULE_MS;
29563
+ let worker;
29564
+ let readyWorker;
29565
+ let broken;
29566
+ let closed = false;
29567
+ let nextJobId = 1;
29568
+ let pending;
29569
+ const terminating = /* @__PURE__ */ new Set();
29570
+ let chain = Promise.resolve();
29571
+ function clearTimers(job) {
29572
+ if (job.startupTimer !== void 0) clearTimeout(job.startupTimer);
29573
+ if (job.timer !== void 0) clearTimeout(job.timer);
29574
+ }
29575
+ function take() {
29576
+ const job = pending;
29577
+ if (!job) return void 0;
29578
+ pending = void 0;
29579
+ clearTimers(job);
29580
+ worker?.unref();
29581
+ return job;
29582
+ }
29583
+ function failPending(outcome) {
29584
+ take()?.fail(outcome);
29585
+ }
29586
+ function kill(dead) {
29587
+ if (worker === dead) worker = void 0;
29588
+ if (readyWorker === dead) readyWorker = void 0;
29589
+ const done = dead.terminate().catch(() => void 0);
29590
+ terminating.add(done);
29591
+ void done.finally(() => terminating.delete(done));
29592
+ }
29593
+ function onDeadline(job) {
29594
+ if (pending !== job) return;
29595
+ const now = performance.now();
29596
+ const runningMs = now - job.progressAt;
29597
+ const elapsedMs = now - job.startedAt;
29598
+ const blamed = job.progressIndex >= 0 && runningMs >= minAttributionMs && runningMs >= elapsedMs * ATTRIBUTION_MIN_SHARE;
29599
+ const culpritIndex = blamed ? job.progressIndex : void 0;
29600
+ kill(job.worker);
29601
+ failPending({ status: "timeout", culpritIndex, elapsedMs });
29602
+ }
29603
+ function ensureWorker() {
29604
+ if (worker) return worker;
29605
+ const url2 = opts.workerUrl ?? resolveWorkerUrl();
29606
+ if (!url2) {
29607
+ return {
29608
+ error: "the scan worker script was not found next to this bundle"
29609
+ };
29610
+ }
29611
+ let started;
29612
+ try {
29613
+ started = new Worker(url2, { workerData: data });
29614
+ } catch (error51) {
29615
+ return { error: `could not start the scan worker: ${messageOf(error51)}` };
29616
+ }
29617
+ opts.onWorkerStart?.(started.threadId);
29618
+ started.on("message", (message) => {
29619
+ if (worker !== started) return;
29620
+ if (message.kind === "ready") {
29621
+ readyWorker = started;
29622
+ if (pending?.worker === started) beginDeadline(pending);
29623
+ return;
29624
+ }
29625
+ if (message.kind === "progress") {
29626
+ if (pending?.worker === started) {
29627
+ pending.progressIndex = message.index;
29628
+ pending.progressAt = performance.now();
29629
+ }
29630
+ return;
29631
+ }
29632
+ if (pending?.id !== message.id) return;
29633
+ if (message.kind === "failed") {
29634
+ failPending({
29635
+ status: "unavailable",
29636
+ reason: `the scan worker failed: ${message.message}`
29637
+ });
29638
+ return;
29639
+ }
29640
+ const job = take();
29641
+ if (job && !job.reply(message)) {
29642
+ job.fail({ status: "unavailable", reason: "the scan worker answered the wrong job" });
29643
+ }
29644
+ });
29645
+ started.on("error", (error51) => {
29646
+ if (worker !== started) return;
29647
+ broken = messageOf(error51);
29648
+ worker = void 0;
29649
+ if (readyWorker === started) readyWorker = void 0;
29650
+ failPending({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29651
+ });
29652
+ started.on("exit", () => {
29653
+ if (worker !== started) return;
29654
+ broken ??= "the scan worker exited before answering";
29655
+ worker = void 0;
29656
+ if (readyWorker === started) readyWorker = void 0;
29657
+ failPending({ status: "unavailable", reason: "the scan worker exited before answering" });
29658
+ });
29659
+ started.unref();
29660
+ worker = started;
29661
+ return started;
29662
+ }
29663
+ function beginDeadline(job) {
29664
+ if (job.startupTimer !== void 0) {
29665
+ clearTimeout(job.startupTimer);
29666
+ job.startupTimer = void 0;
29667
+ }
29668
+ if (job.timer !== void 0) return;
29669
+ job.startedAt = performance.now();
29670
+ job.progressAt = job.startedAt;
29671
+ job.timer = setTimeout(() => {
29672
+ onDeadline(job);
29673
+ }, job.budgetMs);
29674
+ }
29675
+ function runOne(spec, fail) {
29676
+ if (closed) {
29677
+ fail({ status: "unavailable", reason: "the scan worker is closed" });
29678
+ return;
29679
+ }
29680
+ if (broken !== void 0) {
29681
+ fail({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29682
+ return;
29683
+ }
29684
+ const started = ensureWorker();
29685
+ if (!(started instanceof Worker)) {
29686
+ broken = started.error;
29687
+ fail({ status: "unavailable", reason: started.error });
29688
+ return;
29689
+ }
29690
+ const id = nextJobId++;
29691
+ const now = performance.now();
29692
+ const job = {
29693
+ id,
29694
+ worker: started,
29695
+ budgetMs: spec.budgetMs,
29696
+ startedAt: now,
29697
+ progressIndex: -1,
29698
+ progressAt: now,
29699
+ startupTimer: void 0,
29700
+ timer: void 0,
29701
+ reply: spec.reply,
29702
+ fail
29703
+ };
29704
+ pending = job;
29705
+ started.ref();
29706
+ if (readyWorker === started) {
29707
+ beginDeadline(job);
29708
+ } else {
29709
+ job.startupTimer = setTimeout(() => {
29710
+ if (pending !== job) return;
29711
+ kill(job.worker);
29712
+ failPending({
29713
+ status: "unavailable",
29714
+ reason: `the scan worker did not start within ${String(startBudgetMs)}ms`
29715
+ });
29716
+ }, startBudgetMs);
29717
+ }
29718
+ try {
29719
+ started.postMessage(spec.build(id));
29720
+ } catch (error51) {
29721
+ failPending({
29722
+ // The thread went away between the ref and the post.
29723
+ status: "unavailable",
29724
+ reason: `could not reach the scan worker: ${messageOf(error51)}`
29725
+ });
29726
+ }
28469
29727
  }
28470
- try {
28471
- mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
28472
- writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
28473
- } catch {
29728
+ function enqueue(spec) {
29729
+ const next = chain.then(
29730
+ () => new Promise((resolve) => {
29731
+ spec(resolve);
29732
+ })
29733
+ );
29734
+ chain = next.then(
29735
+ () => void 0,
29736
+ () => void 0
29737
+ );
29738
+ return next;
28474
29739
  }
28475
- return true;
29740
+ return {
29741
+ scan(text, context, scanOpts) {
29742
+ return enqueue((resolve) => {
29743
+ runOne(
29744
+ {
29745
+ budgetMs,
29746
+ build: (id) => ({
29747
+ kind: "scan",
29748
+ id,
29749
+ text,
29750
+ filePath: context?.filePath,
29751
+ attribute: scanOpts?.attribute === true
29752
+ }),
29753
+ reply: (message) => {
29754
+ if (message.kind !== "result") return false;
29755
+ resolve({ status: "ok", findings: message.findings });
29756
+ return true;
29757
+ }
29758
+ },
29759
+ resolve
29760
+ );
29761
+ });
29762
+ },
29763
+ probe(rule) {
29764
+ return enqueue((resolve) => {
29765
+ runOne(
29766
+ {
29767
+ budgetMs: probeBudgetMs,
29768
+ build: (id) => ({ kind: "probe", id, rule }),
29769
+ reply: (message) => {
29770
+ if (message.kind !== "probed") return false;
29771
+ resolve({ status: "ok", safe: message.safe, worstMs: message.worstMs });
29772
+ return true;
29773
+ }
29774
+ },
29775
+ resolve
29776
+ );
29777
+ });
29778
+ },
29779
+ async close() {
29780
+ closed = true;
29781
+ const live = worker;
29782
+ worker = void 0;
29783
+ readyWorker = void 0;
29784
+ failPending({ status: "unavailable", reason: "the scan worker is closed" });
29785
+ if (live) kill(live);
29786
+ await Promise.all([...terminating]);
29787
+ }
29788
+ };
28476
29789
  }
28477
29790
 
28478
- // ../../packages/plugin-sdk/src/paths.ts
28479
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
28480
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
28481
-
28482
- // ../../packages/plugin-sdk/src/project-files.ts
28483
- var import_ignore = __toESM(require_ignore(), 1);
28484
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28485
- import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
28486
-
28487
29791
  // ../../packages/plugin-sdk/src/rule-quarantine.ts
28488
29792
  var PASS_BUDGET_MS = 2e3;
29793
+ var UNQUARANTINE_HINT = "clear it with `aka detections unquarantine`";
28489
29794
  function ruleProbeKey(rule) {
28490
29795
  if (rule.matcher.type !== "regex") return void 0;
28491
29796
  return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
28492
29797
  }
28493
- function warnQuarantined(rule, worstMs) {
28494
- const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
29798
+ function warn(rule, verb, detail, recoverable) {
29799
+ const hint = recoverable ? ` (${UNQUARANTINE_HINT})` : "";
29800
+ process.stderr.write(`[aka] ${verb} rule "${rule.id}": ${detail}${hint}
29801
+ `);
29802
+ }
29803
+ function warnQuarantined(rule, worstMs, cached2) {
29804
+ warn(
29805
+ rule,
29806
+ "quarantined",
29807
+ Number.isFinite(worstMs) ? `regex matcher exceeded the ReDoS timing budget (${worstMs.toFixed(1)}ms); excluded from this scan.` : "the timing battery failed while measuring its regex matcher; excluded from this scan.",
29808
+ cached2
29809
+ );
29810
+ }
29811
+ function warnUnmeasured(rule) {
29812
+ warn(
29813
+ rule,
29814
+ "skipped",
29815
+ "the timing pre-flight ran out of time before this rule could be measured; excluded for the rest of this run, and measured again next time.",
29816
+ false
29817
+ );
29818
+ }
29819
+ function warnUnmeasurable(reason, count) {
28495
29820
  process.stderr.write(
28496
- `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
29821
+ `[aka] ${String(count)} pulled/custom-pack rule(s) could not be time-checked: ${reason}. That is a problem with this install, not with the rules \u2014 until it is fixed they are excluded from every scan on this machine. Nothing was quarantined, so reinstalling AKA brings them straight back.
28497
29822
  `
28498
29823
  );
28499
29824
  }
29825
+ async function quarantineRule(gateway, rule, worstMs, detail) {
29826
+ const key = ruleProbeKey(rule);
29827
+ let cached2 = false;
29828
+ if (key !== void 0) {
29829
+ try {
29830
+ await gateway.setRuleProbeVerdict(key, "quarantined", worstMs);
29831
+ cached2 = true;
29832
+ } catch {
29833
+ }
29834
+ }
29835
+ warn(rule, "quarantined", detail, cached2);
29836
+ }
28500
29837
  async function filterUnsafeRules(rules, gateway, opts) {
28501
29838
  const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
29839
+ const prober = opts?.prober;
28502
29840
  const passStart = performance.now();
28503
29841
  const safe = [];
28504
- for (const rule of rules) {
28505
- const key = ruleProbeKey(rule);
28506
- if (key === void 0) {
28507
- safe.push(rule);
28508
- continue;
29842
+ const unmeasurable = /* @__PURE__ */ new Map();
29843
+ try {
29844
+ for (const rule of rules) {
29845
+ const key = ruleProbeKey(rule);
29846
+ if (key === void 0) {
29847
+ safe.push(rule);
29848
+ continue;
29849
+ }
29850
+ let cached2;
29851
+ try {
29852
+ cached2 = await gateway.getRuleProbeVerdict(key);
29853
+ } catch {
29854
+ cached2 = void 0;
29855
+ }
29856
+ if (cached2) {
29857
+ if (cached2.verdict === "safe") safe.push(rule);
29858
+ else warnQuarantined(rule, cached2.worstProbeMs, true);
29859
+ continue;
29860
+ }
29861
+ if (performance.now() - passStart >= passBudgetMs) {
29862
+ warnUnmeasured(rule);
29863
+ continue;
29864
+ }
29865
+ let isSafe;
29866
+ let worstMs;
29867
+ if (prober) {
29868
+ const outcome = await prober.probe(rule);
29869
+ if (outcome.status === "unavailable") {
29870
+ unmeasurable.set(outcome.reason, (unmeasurable.get(outcome.reason) ?? 0) + 1);
29871
+ continue;
29872
+ }
29873
+ isSafe = outcome.status === "ok" ? outcome.safe : false;
29874
+ worstMs = outcome.status === "ok" ? outcome.worstMs : outcome.elapsedMs;
29875
+ } else {
29876
+ try {
29877
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
29878
+ } catch {
29879
+ isSafe = false;
29880
+ worstMs = Number.POSITIVE_INFINITY;
29881
+ }
29882
+ }
29883
+ let persisted = false;
29884
+ try {
29885
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
29886
+ persisted = true;
29887
+ } catch {
29888
+ }
29889
+ if (isSafe) safe.push(rule);
29890
+ else warnQuarantined(rule, worstMs, persisted);
28509
29891
  }
28510
- let cached2;
29892
+ } finally {
29893
+ for (const [reason, count] of unmeasurable) warnUnmeasurable(reason, count);
29894
+ }
29895
+ return safe;
29896
+ }
29897
+
29898
+ // ../../packages/plugin-sdk/src/guarded-scan.ts
29899
+ var DEFAULT_DEGRADE_SCOPE = "the rest of this process";
29900
+ function warnDegraded(scope, dropped, detail) {
29901
+ process.stderr.write(
29902
+ `[aka] isolated scanning is off for ${scope}: ${detail}. ${String(dropped)} pulled/custom-pack rule(s) are excluded; the built-in packs still run.
29903
+ `
29904
+ );
29905
+ }
29906
+ function createGuardedScanner(partition, gateway, opts) {
29907
+ const degradeScope = opts?.degradeScope ?? DEFAULT_DEGRADE_SCOPE;
29908
+ const verified = partition.verified;
29909
+ let unverified = partition.unverified;
29910
+ let isolated = unverified.length > 0 ? createIsolatedScanner({ verified, unverified }, opts) : void 0;
29911
+ let retired = false;
29912
+ function inProcess(text, context) {
29913
+ return scan(text, verified, context);
29914
+ }
29915
+ async function retire() {
29916
+ const live = isolated;
29917
+ isolated = void 0;
29918
+ unverified = [];
29919
+ if (live) await live.close();
29920
+ }
29921
+ async function degrade() {
29922
+ retired = true;
29923
+ await retire();
29924
+ }
29925
+ async function attempt(active, text, context, attribute) {
28511
29926
  try {
28512
- cached2 = await gateway.getRuleProbeVerdict(key);
28513
- } catch {
28514
- cached2 = void 0;
28515
- }
28516
- if (cached2) {
28517
- if (cached2.verdict === "safe") safe.push(rule);
28518
- else warnQuarantined(rule, cached2.worstProbeMs);
28519
- continue;
29927
+ return await active.scan(text, context, { attribute });
29928
+ } catch (error51) {
29929
+ return {
29930
+ status: "unavailable",
29931
+ reason: error51 instanceof Error ? error51.message : "the scan worker failed unexpectedly"
29932
+ };
28520
29933
  }
28521
- if (performance.now() - passStart >= passBudgetMs) {
28522
- warnQuarantined(rule, void 0);
28523
- continue;
29934
+ }
29935
+ async function guardedScan(text, context) {
29936
+ const active = isolated;
29937
+ if (!active) return inProcess(text, context);
29938
+ let outcome = await attempt(active, text, context, false);
29939
+ if (outcome.status === "ok") return outcome.findings;
29940
+ if (outcome.status === "timeout") outcome = await attempt(active, text, context, true);
29941
+ const dropped = unverified.length;
29942
+ if (outcome.status === "ok") {
29943
+ warnDegraded(
29944
+ degradeScope,
29945
+ dropped,
29946
+ "a scan overran its bound once and no rule could be held responsible"
29947
+ );
29948
+ const findings = outcome.findings;
29949
+ await degrade();
29950
+ return findings;
29951
+ }
29952
+ if (outcome.status === "timeout") {
29953
+ const culprit = outcome.culpritIndex === void 0 ? void 0 : unverified[outcome.culpritIndex];
29954
+ if (culprit) {
29955
+ await quarantineRule(
29956
+ gateway,
29957
+ culprit,
29958
+ outcome.elapsedMs,
29959
+ `it did not finish within the ${outcome.elapsedMs.toFixed(0)}ms isolated-scan bound and was terminated; excluded from every later scan.`
29960
+ );
29961
+ }
29962
+ warnDegraded(
29963
+ degradeScope,
29964
+ dropped,
29965
+ culprit ? `rule "${culprit.id}" had to be terminated mid-scan` : `a scan was terminated at the ${outcome.elapsedMs.toFixed(0)}ms bound and no single rule could be held responsible, so nothing was quarantined and the next process will try these rules again`
29966
+ );
29967
+ } else {
29968
+ warnDegraded(degradeScope, dropped, outcome.reason);
28524
29969
  }
28525
- let isSafe;
28526
- let worstMs;
28527
- try {
28528
- ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
28529
- } catch {
28530
- isSafe = false;
28531
- worstMs = Number.POSITIVE_INFINITY;
29970
+ await degrade();
29971
+ return inProcess(text, context);
29972
+ }
29973
+ return {
29974
+ scan: guardedScan,
29975
+ degraded: () => retired,
29976
+ async close() {
29977
+ await retire();
28532
29978
  }
28533
- await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
28534
- if (isSafe) safe.push(rule);
28535
- else warnQuarantined(rule, worstMs);
29979
+ };
29980
+ }
29981
+
29982
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
29983
+ import { arch, hostname as hostname4, platform, release } from "os";
29984
+
29985
+ // ../../packages/plugin-sdk/src/nudge.ts
29986
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
29987
+ import { join as join11 } from "path";
29988
+ var NUDGE_MARKER = "nudge-last-session";
29989
+ function claimOnboardingNudge(dataDir2, sessionId) {
29990
+ return claimOncePerSession(dataDir2, NUDGE_MARKER, sessionId);
29991
+ }
29992
+ function claimOncePerSession(dataDir2, marker, sessionId) {
29993
+ if (!sessionId) return true;
29994
+ const path = join11(dataDir2, marker);
29995
+ try {
29996
+ if (readFileSync7(path, "utf8") === sessionId) return false;
29997
+ } catch {
28536
29998
  }
28537
- return safe;
29999
+ try {
30000
+ mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
30001
+ writeFileSync5(path, sessionId, { mode: DATA_FILE_MODE });
30002
+ } catch {
30003
+ }
30004
+ return true;
28538
30005
  }
28539
30006
 
30007
+ // ../../packages/plugin-sdk/src/paths.ts
30008
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
30009
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
30010
+
30011
+ // ../../packages/plugin-sdk/src/project-files.ts
30012
+ var import_ignore = __toESM(require_ignore(), 1);
30013
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
30014
+ import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
30015
+
30016
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
30017
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
30018
+ if (typeof v === "string" && v.trim() === "") return void 0;
30019
+ return v;
30020
+ }, external_exports.string().optional()).catch(void 0);
30021
+ var optionalFlag = external_exports.preprocess((v) => {
30022
+ if (typeof v !== "string") return false;
30023
+ const normalized = v.trim().toLowerCase();
30024
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
30025
+ }, external_exports.boolean()).catch(false);
30026
+ var antigravityProviderEnvShape = {
30027
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
30028
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
30029
+ };
30030
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
30031
+
30032
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
30033
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
30034
+ if (typeof v === "string" && v.trim() === "") return void 0;
30035
+ return v;
30036
+ }, external_exports.string().optional()).catch(void 0);
30037
+ var codexProviderEnvShape = {
30038
+ OPENAI_BASE_URL: optionalBaseUrl3
30039
+ };
30040
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
30041
+
28540
30042
  // ../../packages/plugin-sdk/src/runtime.ts
28541
- import { randomUUID as randomUUID12 } from "crypto";
30043
+ import { randomUUID as randomUUID14 } from "crypto";
28542
30044
  var ENFORCEMENT_CEILING_ENABLED = false;
28543
30045
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
28544
30046
  function entryIsActive(entry, now) {
@@ -28563,6 +30065,7 @@ function createPluginRuntime(gateway, settings, opts) {
28563
30065
  const dataDir2 = opts?.dataDir;
28564
30066
  let policies = [];
28565
30067
  let rules = [];
30068
+ let scanner;
28566
30069
  let bundleExceptions = [];
28567
30070
  let initialized = false;
28568
30071
  const ruleActionIndex = /* @__PURE__ */ new Map();
@@ -28588,8 +30091,24 @@ function createPluginRuntime(gateway, settings, opts) {
28588
30091
  return key !== void 0 && bundledProbeKeys.has(key);
28589
30092
  });
28590
30093
  const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
28591
- const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
28592
- rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
30094
+ let prober;
30095
+ const gated = await filterUnsafeRules(needsGate, gateway, {
30096
+ prober: {
30097
+ probe: (rule) => {
30098
+ prober ??= createIsolatedScanner({ verified: [], unverified: [] }, opts?.scanIsolation);
30099
+ return prober.probe(rule);
30100
+ }
30101
+ }
30102
+ });
30103
+ await prober?.close();
30104
+ const verified = bundle.rulesComplete ? [...ciVerified] : [...getLoadedRules(), ...ciVerified];
30105
+ const unverified = [];
30106
+ for (const rule of gated) {
30107
+ if (rule.matcher.type === "regex") unverified.push(rule);
30108
+ else verified.push(rule);
30109
+ }
30110
+ rules = [...verified, ...unverified];
30111
+ scanner = createGuardedScanner({ verified, unverified }, gateway, opts?.scanIsolation);
28593
30112
  bundleExceptions = bundle.exceptions ?? [];
28594
30113
  initialized = true;
28595
30114
  }
@@ -28729,7 +30248,7 @@ function createPluginRuntime(gateway, settings, opts) {
28729
30248
  const pair = `${finding.ruleId}:${fp}`;
28730
30249
  if (seen.has(pair)) continue;
28731
30250
  seen.add(pair);
28732
- const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
30251
+ const reference = randomUUID14().replaceAll("-", "").slice(0, 6);
28733
30252
  const maskedValue = maskMatch(finding.rawMatch);
28734
30253
  try {
28735
30254
  await gateway.recordBlockedDetection({
@@ -28753,8 +30272,10 @@ function createPluginRuntime(gateway, settings, opts) {
28753
30272
  async function evaluate(text, context, ctx) {
28754
30273
  try {
28755
30274
  await ensureInitialized();
30275
+ if (!scanner) throw new Error("the runtime initialized without a scanner");
28756
30276
  const shielded = shieldPointers(text);
28757
- const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
30277
+ const matched = await scanner.scan(shielded.text, context);
30278
+ const findings = dropShieldedFindings(matched, shielded.spans);
28758
30279
  const fpCache = /* @__PURE__ */ new Map();
28759
30280
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
28760
30281
  const decision = decide(findings, text, excepted);
@@ -28811,7 +30332,7 @@ function createPluginRuntime(gateway, settings, opts) {
28811
30332
  valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
28812
30333
  }) : void 0;
28813
30334
  return {
28814
- id: randomUUID12(),
30335
+ id: randomUUID14(),
28815
30336
  eventId: event.id,
28816
30337
  ruleId: match.ruleId,
28817
30338
  category: match.category,
@@ -28837,21 +30358,28 @@ function createPluginRuntime(gateway, settings, opts) {
28837
30358
  const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
28838
30359
  return contentHashOf(JSON.stringify(sorted));
28839
30360
  } catch {
28840
- return `unresolved-${randomUUID12()}`;
30361
+ return `unresolved-${randomUUID14()}`;
28841
30362
  }
28842
30363
  }
30364
+ function scanIsolationDegraded() {
30365
+ return scanner?.degraded() ?? false;
30366
+ }
28843
30367
  async function close() {
30368
+ try {
30369
+ await scanner?.close();
30370
+ } catch {
30371
+ }
28844
30372
  await gateway.close();
28845
30373
  }
28846
- return { processText, capture, rulesetFingerprint, close };
30374
+ return { processText, capture, rulesetFingerprint, scanIsolationDegraded, close };
28847
30375
  }
28848
30376
 
28849
30377
  // ../../packages/plugin-sdk/src/suppressions.ts
28850
30378
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28851
30379
 
28852
30380
  // ../../packages/plugin-sdk/src/throttle.ts
28853
- import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28854
- import { join as join12 } from "path";
30381
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
30382
+ import { join as join13 } from "path";
28855
30383
 
28856
30384
  // ../../packages/plugin-sdk/src/tokenize.ts
28857
30385
  function redactedPlaceholder(category) {
@@ -29093,7 +30621,6 @@ function createVaultGlue(options) {
29093
30621
  const vault = new SecretVault({
29094
30622
  repo: db.secretVault,
29095
30623
  keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
29096
- fingerprintKey: loadOrCreateFingerprintKey(dir),
29097
30624
  // Read live so a revocation applies to the very next call, not the next
29098
30625
  // process.
29099
30626
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
@@ -29114,8 +30641,10 @@ function createVaultGlue(options) {
29114
30641
  return decision.allow;
29115
30642
  }
29116
30643
  });
30644
+ let fingerprintKey;
30645
+ const fingerprintKeyForWrite = () => fingerprintKey ??= loadOrCreateFingerprintKey(dir);
29117
30646
  const vaultWithSightings = {
29118
- tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
30647
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3, fingerprintKeyForWrite),
29119
30648
  detokenize: (token, opts) => vault.detokenize(token, opts),
29120
30649
  describePointer: (token) => vault.describePointer(token),
29121
30650
  resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
@@ -29308,11 +30837,11 @@ function baseMetadata(input) {
29308
30837
  }
29309
30838
 
29310
30839
  // src/hooks/store-health.ts
29311
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
29312
- import { join as join13 } from "path";
30840
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
30841
+ import { join as join14 } from "path";
29313
30842
 
29314
30843
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
29315
- import { randomUUID as randomUUID13 } from "crypto";
30844
+ import { randomUUID as randomUUID15 } from "crypto";
29316
30845
 
29317
30846
  // ../../packages/plugin-runtime/src/recorder.ts
29318
30847
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -29474,7 +31003,7 @@ var StandaloneDataGateway = class {
29474
31003
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
29475
31004
  const installed = this.installedScanRules();
29476
31005
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
29477
- id: randomUUID13(),
31006
+ id: randomUUID15(),
29478
31007
  scope: "global",
29479
31008
  target: { ruleId },
29480
31009
  action,
@@ -29627,7 +31156,7 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
29627
31156
  }
29628
31157
 
29629
31158
  // ../../packages/plugin-runtime/src/handle-session-start.ts
29630
- import { randomUUID as randomUUID14 } from "crypto";
31159
+ import { randomUUID as randomUUID16 } from "crypto";
29631
31160
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
29632
31161
 
29633
31162
  // src/hooks/store-health.ts
@@ -29644,14 +31173,14 @@ function storeUnavailableMessage(dbPath2) {
29644
31173
  }
29645
31174
  function claimStoreUnavailableWarning(dataDir2, sessionId) {
29646
31175
  if (!sessionId) return true;
29647
- const path = join13(dataDir2, STORE_WARNING_MARKER);
31176
+ const path = join14(dataDir2, STORE_WARNING_MARKER);
29648
31177
  try {
29649
- if (readFileSync8(path, "utf8") === sessionId) return false;
31178
+ if (readFileSync9(path, "utf8") === sessionId) return false;
29650
31179
  } catch {
29651
31180
  }
29652
31181
  try {
29653
31182
  mkdirSync5(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
29654
- writeFileSync6(path, sessionId, { mode: DATA_FILE_MODE });
31183
+ writeFileSync7(path, sessionId, { mode: DATA_FILE_MODE });
29655
31184
  } catch {
29656
31185
  }
29657
31186
  return true;