@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({
@@ -17485,6 +17809,50 @@ var VaultInventoryEntry = external_exports.object({
17485
17809
  revealGrantId: external_exports.string().nullable(),
17486
17810
  sightings: external_exports.array(VaultSighting)
17487
17811
  });
17812
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17813
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17814
+ var MAX_VAULT_PAGE_LIMIT = 200;
17815
+ var ListVaultInventoryQuery = external_exports.object({
17816
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17817
+ // Opaque; names the last row of the page just served.
17818
+ cursor: external_exports.string().optional()
17819
+ });
17820
+ var ListVaultInventoryResponse = external_exports.object({
17821
+ // Vaulted values across the whole store, not just this page — cursor-
17822
+ // independent, so paging never changes what the count claims.
17823
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17824
+ items: external_exports.array(VaultInventoryEntry),
17825
+ // `null` once the last page is reached.
17826
+ nextCursor: external_exports.string().nullable()
17827
+ });
17828
+ var ListVaultReuseQuery = external_exports.object({
17829
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17830
+ cursor: external_exports.string().optional()
17831
+ });
17832
+ var ListVaultReuseResponse = external_exports.object({
17833
+ // Reused values across the whole store — the number the section's claim
17834
+ // ("values detected in more than one place") is about.
17835
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17836
+ items: external_exports.array(VaultInventoryEntry),
17837
+ nextCursor: external_exports.string().nullable()
17838
+ });
17839
+ var ListVaultDerefsQuery = external_exports.object({
17840
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17841
+ // hides them and counts them into `hiddenBatched` instead, so the model
17842
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17843
+ // over a Server Action, which preserves the type, never as a URL param.
17844
+ includeBatched: external_exports.boolean().optional(),
17845
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17846
+ cursor: external_exports.string().optional()
17847
+ });
17848
+ var ListVaultDerefsResponse = external_exports.object({
17849
+ items: external_exports.array(VaultDeref),
17850
+ nextCursor: external_exports.string().nullable(),
17851
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17852
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17853
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17854
+ hiddenBatched: external_exports.number().int().nonnegative()
17855
+ });
17488
17856
  var VaultKeyCustody = external_exports.string();
17489
17857
  var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17490
17858
  var VaultConsent = external_exports.object({
@@ -17857,7 +18225,7 @@ var TopSourcesQuery = external_exports.object({
17857
18225
  // Omit for both kinds.
17858
18226
  kind: external_exports.enum(SOURCE_KINDS).optional()
17859
18227
  });
17860
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18228
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17861
18229
  var ScanCoverageProvider = external_exports.object({
17862
18230
  provider: Provider,
17863
18231
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -18110,6 +18478,195 @@ function captureId(sessionId, contentHash, filePath = null) {
18110
18478
  );
18111
18479
  }
18112
18480
 
18481
+ // ../../packages/persistence/src/internal/snapshot.ts
18482
+ import { randomUUID } from "crypto";
18483
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18484
+ import { basename, dirname, join } from "path";
18485
+
18486
+ // ../../packages/persistence/src/paths.ts
18487
+ import {
18488
+ chmodSync,
18489
+ linkSync,
18490
+ lstatSync,
18491
+ mkdirSync,
18492
+ renameSync,
18493
+ rmSync,
18494
+ writeFileSync
18495
+ } from "fs";
18496
+ import { threadId } from "worker_threads";
18497
+ var DATA_DIR_MODE = 448;
18498
+ var DATA_FILE_MODE = 384;
18499
+ var DB_FILENAME = "aka.db";
18500
+ function isSymlink(path) {
18501
+ try {
18502
+ return lstatSync(path).isSymbolicLink();
18503
+ } catch {
18504
+ return false;
18505
+ }
18506
+ }
18507
+ function chmodBestEffort(path, mode) {
18508
+ if (isSymlink(path)) return;
18509
+ try {
18510
+ chmodSync(path, mode);
18511
+ } catch {
18512
+ }
18513
+ }
18514
+ function tightenDir(dir) {
18515
+ chmodBestEffort(dir, DATA_DIR_MODE);
18516
+ }
18517
+ function ensureDataDirSync(dir) {
18518
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18519
+ tightenDir(dir);
18520
+ }
18521
+ function dbSidecars(file2) {
18522
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18523
+ }
18524
+ function tightenFile(file2) {
18525
+ chmodBestEffort(file2, DATA_FILE_MODE);
18526
+ }
18527
+ function tightenPerms(file2) {
18528
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18529
+ }
18530
+ function classifyOccupant(file2) {
18531
+ try {
18532
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
18533
+ return { kind: "gone" };
18534
+ } catch (err) {
18535
+ if (err.code === "ENOENT") return { kind: "gone" };
18536
+ return { kind: "unknown", cause: err };
18537
+ }
18538
+ }
18539
+ var KeyUnclaimableError = class extends Error {
18540
+ code = "key-unclaimable";
18541
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
18542
+ // defines the property anyway, so an error carrying nothing would still answer
18543
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
18544
+ // captured and then lost, which is worse than its plain absence.
18545
+ constructor(message, cause) {
18546
+ super(message, cause === void 0 ? void 0 : { cause });
18547
+ this.name = "KeyUnclaimableError";
18548
+ }
18549
+ };
18550
+ function createOwnerOnlyFileSync(file2, data) {
18551
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
18552
+ try {
18553
+ rmSync(tmp, { force: true });
18554
+ } catch {
18555
+ }
18556
+ let created;
18557
+ try {
18558
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18559
+ created = publishByLink(tmp, file2, data);
18560
+ } finally {
18561
+ try {
18562
+ rmSync(tmp, { force: true });
18563
+ } catch {
18564
+ }
18565
+ }
18566
+ if (created) tightenFile(file2);
18567
+ return created;
18568
+ }
18569
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
18570
+ function publishByLink(tmp, file2, data) {
18571
+ try {
18572
+ linkSync(tmp, file2);
18573
+ return true;
18574
+ } catch (err) {
18575
+ const code = err.code;
18576
+ if (code === "EEXIST") return false;
18577
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
18578
+ }
18579
+ try {
18580
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
18581
+ return true;
18582
+ } catch (err) {
18583
+ if (err.code === "EEXIST") return false;
18584
+ throw err;
18585
+ }
18586
+ }
18587
+
18588
+ // ../../packages/persistence/src/internal/snapshot.ts
18589
+ function backupPath(file2, tag) {
18590
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18591
+ }
18592
+ var STALE_PARTIAL_MS = 5 * 6e4;
18593
+ function reapStalePartials(file2) {
18594
+ const dir = dirname(file2);
18595
+ const prefix = `${basename(file2)}.`;
18596
+ let entries;
18597
+ try {
18598
+ entries = readdirSync(dir);
18599
+ } catch {
18600
+ return;
18601
+ }
18602
+ for (const name of entries) {
18603
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18604
+ const partial2 = join(dir, name);
18605
+ try {
18606
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18607
+ rmSync2(partial2, { force: true });
18608
+ }
18609
+ } catch {
18610
+ }
18611
+ }
18612
+ }
18613
+ function snapshotStore(db, backup) {
18614
+ const partial2 = `${backup}.partial`;
18615
+ try {
18616
+ rmSync2(partial2, { force: true });
18617
+ db.prepare("VACUUM INTO ?").run(partial2);
18618
+ tightenFile(partial2);
18619
+ renameSync2(partial2, backup);
18620
+ } catch (error51) {
18621
+ try {
18622
+ rmSync2(partial2, { force: true });
18623
+ } catch {
18624
+ }
18625
+ throw error51;
18626
+ }
18627
+ }
18628
+ function moveStoreAside(file2, backup) {
18629
+ const undo = [];
18630
+ renameSync2(file2, backup);
18631
+ undo.push([backup, file2]);
18632
+ try {
18633
+ for (const sidecar of dbSidecars(file2)) {
18634
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18635
+ try {
18636
+ renameSync2(sidecar, moved);
18637
+ undo.push([moved, sidecar]);
18638
+ } catch {
18639
+ rmSync2(sidecar, { force: true });
18640
+ }
18641
+ }
18642
+ } catch (error51) {
18643
+ for (const [from, to] of undo.reverse()) {
18644
+ try {
18645
+ renameSync2(from, to);
18646
+ } catch {
18647
+ }
18648
+ }
18649
+ throw error51;
18650
+ }
18651
+ tightenPerms(backup);
18652
+ }
18653
+ function discardStore(file2, backup) {
18654
+ try {
18655
+ rmSync2(file2, { force: true });
18656
+ for (const sidecar of dbSidecars(file2)) {
18657
+ rmSync2(sidecar, { force: true });
18658
+ }
18659
+ } catch (error51) {
18660
+ if (existsSync(file2)) {
18661
+ try {
18662
+ rmSync2(backup, { force: true });
18663
+ } catch {
18664
+ }
18665
+ }
18666
+ throw error51;
18667
+ }
18668
+ }
18669
+
18113
18670
  // ../../packages/persistence/src/internal/sql-text.ts
18114
18671
  function escapeLikePattern(s) {
18115
18672
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18251,55 +18808,6 @@ function mapRowsTolerant(rows, map2) {
18251
18808
  return out;
18252
18809
  }
18253
18810
 
18254
- // ../../packages/persistence/src/paths.ts
18255
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18256
- var DATA_DIR_MODE = 448;
18257
- var DATA_FILE_MODE = 384;
18258
- var DB_FILENAME = "aka.db";
18259
- function chmodBestEffort(path, mode) {
18260
- try {
18261
- chmodSync(path, mode);
18262
- } catch {
18263
- }
18264
- }
18265
- function tightenDir(dir) {
18266
- chmodBestEffort(dir, DATA_DIR_MODE);
18267
- }
18268
- function ensureDataDirSync(dir) {
18269
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18270
- tightenDir(dir);
18271
- }
18272
- function dbSidecars(file2) {
18273
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18274
- }
18275
- function tightenFile(file2) {
18276
- try {
18277
- if (lstatSync(file2).isSymbolicLink()) return;
18278
- } catch {
18279
- }
18280
- chmodBestEffort(file2, DATA_FILE_MODE);
18281
- }
18282
- function tightenPerms(file2) {
18283
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18284
- }
18285
- function writeOwnerOnlyFileSync(file2, data) {
18286
- const tmp = `${file2}.${String(process.pid)}.tmp`;
18287
- try {
18288
- rmSync(tmp, { force: true });
18289
- } catch {
18290
- }
18291
- try {
18292
- writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18293
- renameSync(tmp, file2);
18294
- } finally {
18295
- try {
18296
- rmSync(tmp, { force: true });
18297
- } catch {
18298
- }
18299
- }
18300
- tightenFile(file2);
18301
- }
18302
-
18303
18811
  // ../../packages/persistence/src/migrations.ts
18304
18812
  function describeObject(object2) {
18305
18813
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18415,9 +18923,9 @@ function applyLegacyDropMigration(db, file2) {
18415
18923
  }
18416
18924
  }
18417
18925
  function backupBeforeLegacyDrop(db, file2) {
18418
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18419
- db.prepare("VACUUM INTO ?").run(backup);
18420
- tightenFile(backup);
18926
+ reapStalePartials(file2);
18927
+ const backup = backupPath(file2, "pre-drop");
18928
+ snapshotStore(db, backup);
18421
18929
  return backup;
18422
18930
  }
18423
18931
  var TOKEN_USAGE_COLUMNS = [
@@ -18761,6 +19269,25 @@ function parseJsonObject(s) {
18761
19269
  return void 0;
18762
19270
  }
18763
19271
 
19272
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19273
+ function encodeKeysetCursor(payload) {
19274
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19275
+ }
19276
+ function decodeKeysetCursor(cursor) {
19277
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19278
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19279
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19280
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19281
+ // a null cursor, which a caller reads as "end of list". That is the one
19282
+ // outcome a cursor that does not decode must never produce, since the
19283
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19284
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19285
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19286
+ return parsed;
19287
+ }
19288
+ return null;
19289
+ }
19290
+
18764
19291
  // ../../packages/persistence/src/repositories/activity.ts
18765
19292
  var DAY_MS = 864e5;
18766
19293
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18806,16 +19333,6 @@ function utcWindow(nowMs) {
18806
19333
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18807
19334
  return { startMs, endMs: startMs + DAY_MS };
18808
19335
  }
18809
- function encodeCursor(payload) {
18810
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18811
- }
18812
- function decodeCursor(cursor) {
18813
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18814
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18815
- return parsed;
18816
- }
18817
- return null;
18818
- }
18819
19336
  var DB_EVENT_TYPE_TO_KIND = {
18820
19337
  session: "session",
18821
19338
  prompt: "prompt",
@@ -18960,7 +19477,7 @@ var SqliteActivityRepository = class {
18960
19477
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18961
19478
  }
18962
19479
  listSessions(query) {
18963
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19480
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18964
19481
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18965
19482
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18966
19483
  const conditions = [SESSION_ROOT];
@@ -19034,7 +19551,7 @@ var SqliteActivityRepository = class {
19034
19551
  )
19035
19552
  );
19036
19553
  const last = page[page.length - 1];
19037
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19554
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19038
19555
  return Promise.resolve({ items, nextCursor, emptyCount });
19039
19556
  }
19040
19557
  getSession(sessionId) {
@@ -19907,7 +20424,7 @@ var SqliteEventsRepository = class {
19907
20424
  };
19908
20425
 
19909
20426
  // ../../packages/persistence/src/repositories/exceptions.ts
19910
- import { randomUUID } from "crypto";
20427
+ import { randomUUID as randomUUID2 } from "crypto";
19911
20428
 
19912
20429
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19913
20430
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19943,8 +20460,9 @@ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19943
20460
  AND conditions IS NULL
19944
20461
  AND ${ACTIVE_PREDICATE}`;
19945
20462
  var SqliteExceptionsRepository = class {
19946
- constructor(db) {
20463
+ constructor(db, now = () => Date.now()) {
19947
20464
  this.db = db;
20465
+ this.now = now;
19948
20466
  this.consumeStmt = db.prepare(
19949
20467
  `UPDATE exceptions
19950
20468
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19962,6 +20480,7 @@ var SqliteExceptionsRepository = class {
19962
20480
  );
19963
20481
  }
19964
20482
  db;
20483
+ now;
19965
20484
  consumeStmt;
19966
20485
  insertBlockedStmt;
19967
20486
  sweepBlockedStmt;
@@ -19988,8 +20507,8 @@ var SqliteExceptionsRepository = class {
19988
20507
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19989
20508
  );
19990
20509
  }
19991
- const id = randomUUID();
19992
- const now = Date.now();
20510
+ const id = randomUUID2();
20511
+ const now = this.now();
19993
20512
  try {
19994
20513
  this.insertExceptionRow(id, input, now);
19995
20514
  } catch (err) {
@@ -20067,7 +20586,7 @@ var SqliteExceptionsRepository = class {
20067
20586
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
20068
20587
  const rows = allRows(
20069
20588
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
20070
- opts?.includeTerminal ? {} : { now: Date.now() }
20589
+ opts?.includeTerminal ? {} : { now: this.now() }
20071
20590
  );
20072
20591
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
20073
20592
  return Promise.resolve(exceptions);
@@ -20102,7 +20621,7 @@ var SqliteExceptionsRepository = class {
20102
20621
  * already revoked.
20103
20622
  */
20104
20623
  revoke(id, revokedBy, reason) {
20105
- const now = Date.now();
20624
+ const now = this.now();
20106
20625
  const result = this.db.prepare(
20107
20626
  `UPDATE exceptions
20108
20627
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -20116,7 +20635,7 @@ var SqliteExceptionsRepository = class {
20116
20635
  * callers must treat identically — means it does not and the detection is
20117
20636
  * enforced as usual. Deliberately NOT wrapped in try/catch.
20118
20637
  */
20119
- consume(id, now = Date.now()) {
20638
+ consume(id, now = this.now()) {
20120
20639
  const result = this.consumeStmt.run({ id, now });
20121
20640
  return Promise.resolve(Number(result.changes) === 1);
20122
20641
  }
@@ -20125,7 +20644,7 @@ var SqliteExceptionsRepository = class {
20125
20644
  * version — what rides the policy bundle to the hook. Grants written under
20126
20645
  * a different (rotated-away) key never match, so they are excluded at read.
20127
20646
  */
20128
- activeBundleEntries(keyVersion, now = Date.now()) {
20647
+ activeBundleEntries(keyVersion, now = this.now()) {
20129
20648
  const rows = allRows(
20130
20649
  this.db.prepare(
20131
20650
  `SELECT * FROM exceptions
@@ -20157,7 +20676,7 @@ var SqliteExceptionsRepository = class {
20157
20676
  * than the retention window on every write, so the ledger self-limits.
20158
20677
  */
20159
20678
  recordBlocked(entry) {
20160
- const now = Date.now();
20679
+ const now = this.now();
20161
20680
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
20162
20681
  this.insertBlockedStmt.run({
20163
20682
  reference: entry.reference,
@@ -20180,7 +20699,7 @@ var SqliteExceptionsRepository = class {
20180
20699
  WHERE blocked_at > :cutoff
20181
20700
  ORDER BY blocked_at DESC, rowid DESC`
20182
20701
  ),
20183
- { cutoff: Date.now() - windowMs }
20702
+ { cutoff: this.now() - windowMs }
20184
20703
  );
20185
20704
  return Promise.resolve(
20186
20705
  rows.map((row) => ({
@@ -20208,8 +20727,9 @@ var SqliteExceptionsRepository = class {
20208
20727
  * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20209
20728
  * grant instead. Fail closed until reveal-side condition evaluation exists.
20210
20729
  */
20211
- activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20730
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20212
20731
  try {
20732
+ const at = now ?? this.now();
20213
20733
  const row = getRow(
20214
20734
  this.db.prepare(
20215
20735
  `SELECT id FROM exceptions
@@ -20218,7 +20738,7 @@ var SqliteExceptionsRepository = class {
20218
20738
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20219
20739
  LIMIT 1`
20220
20740
  ),
20221
- { ruleId, valueFingerprint, keyVersion, now }
20741
+ { ruleId, valueFingerprint, keyVersion, now: at }
20222
20742
  );
20223
20743
  return Promise.resolve(row ?? null);
20224
20744
  } catch (err) {
@@ -20232,7 +20752,7 @@ var SqliteExceptionsRepository = class {
20232
20752
  * predicate, so correctness never depends on this sweep; it only bounds how
20233
20753
  * long the audit evidence is kept locally. Returns the deleted count.
20234
20754
  */
20235
- sweepTerminal(retentionMs, now = Date.now()) {
20755
+ sweepTerminal(retentionMs, now = this.now()) {
20236
20756
  const result = this.db.prepare(
20237
20757
  `DELETE FROM exceptions
20238
20758
  WHERE updated_at < :cutoff
@@ -20295,6 +20815,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20295
20815
 
20296
20816
  // ../../packages/persistence/src/repositories/findings.ts
20297
20817
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20818
+ var SCAN_BATCH_ROWS = 1e3;
20819
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20820
+ var LOCATION_RULE_IDS_CAP = 20;
20821
+ function compareLocationOrder(a, b) {
20822
+ return compareFindingGroupOrder(
20823
+ {
20824
+ severity: a.maxSeverity,
20825
+ latestDetectedAt: a.latestDetectedAt,
20826
+ id: ""
20827
+ },
20828
+ {
20829
+ severity: b.maxSeverity,
20830
+ latestDetectedAt: b.latestDetectedAt,
20831
+ id: ""
20832
+ }
20833
+ );
20834
+ }
20298
20835
  var CONCAT_SEP = ",";
20299
20836
  var TUPLE_SEP = "|";
20300
20837
  function splitConcat(value) {
@@ -20307,6 +20844,33 @@ function deriveInstanceStatus(row) {
20307
20844
  latestResolutionStatus: row.latest_status
20308
20845
  });
20309
20846
  }
20847
+ function encodeGroupCursor(group) {
20848
+ const payload = {
20849
+ sev: group.severity,
20850
+ t: group.latestDetectedAt,
20851
+ id: group.id
20852
+ };
20853
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20854
+ }
20855
+ function decodeGroupCursor(cursor) {
20856
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20857
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20858
+ return {
20859
+ severity: parsed.sev,
20860
+ latestDetectedAt: parsed.t,
20861
+ id: parsed.id
20862
+ };
20863
+ }
20864
+ return null;
20865
+ }
20866
+ function firstAfter(sorted, cursor) {
20867
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20868
+ return index === -1 ? sorted.length : index;
20869
+ }
20870
+ function findDeepLinked(sorted, page, id) {
20871
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20872
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20873
+ }
20310
20874
  var DAY_MS3 = 864e5;
20311
20875
  var SqliteFindingsRepository = class {
20312
20876
  constructor(db) {
@@ -20416,8 +20980,13 @@ var SqliteFindingsRepository = class {
20416
20980
  */
20417
20981
  listGroupedFindings(query) {
20418
20982
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20419
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20420
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20983
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20984
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20985
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20986
+ const sessionParams = {
20987
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20988
+ ...fromMs === void 0 ? {} : { fromMs }
20989
+ };
20421
20990
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20422
20991
  predicate,
20423
20992
  params: sessionParams
@@ -20425,7 +20994,8 @@ var SqliteFindingsRepository = class {
20425
20994
  const rows = allRows(
20426
20995
  this.db.prepare(
20427
20996
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20428
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
20997
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
20998
+ kind, finding_key, latest_status
20429
20999
  FROM (
20430
21000
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20431
21001
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20435,6 +21005,7 @@ var SqliteFindingsRepository = class {
20435
21005
  json_extract(e.attributes, '$.repo') AS repo,
20436
21006
  json_extract(e.attributes, '$.file_path') AS file,
20437
21007
  json_extract(e.attributes, '$.tool_name') AS tool_name,
21008
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20438
21009
  e.event_type AS kind, f.finding_key AS finding_key,
20439
21010
  latest.status AS latest_status,
20440
21011
  ROW_NUMBER() OVER (
@@ -20466,6 +21037,8 @@ var SqliteFindingsRepository = class {
20466
21037
  repo: r.repo ?? "",
20467
21038
  file: r.file ?? "",
20468
21039
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
21040
+ eventId: r.event_id,
21041
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20469
21042
  status: deriveInstanceStatus(r)
20470
21043
  }));
20471
21044
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20489,18 +21062,23 @@ var SqliteFindingsRepository = class {
20489
21062
  groups: sorted.length
20490
21063
  };
20491
21064
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21065
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21066
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21067
+ const page = sorted.slice(start, start + limit);
21068
+ const lastOnPage = page.at(-1);
21069
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21070
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20492
21071
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20493
- const items = sorted.slice(0, limit).map(
20494
- (g) => statusSet ? {
20495
- ...g,
20496
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20497
- } : g
20498
- );
21072
+ const narrow = (g) => statusSet ? {
21073
+ ...g,
21074
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21075
+ } : g;
21076
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20499
21077
  return Promise.resolve({
20500
21078
  totals,
20501
21079
  facets,
20502
21080
  items,
20503
- nextCursor: null,
21081
+ nextCursor,
20504
21082
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20505
21083
  });
20506
21084
  }
@@ -20532,6 +21110,266 @@ var SqliteFindingsRepository = class {
20532
21110
  * request actually carries a `q`. (Substring matching is unaffected by a
20533
21111
  * path repeating across tuples.)
20534
21112
  */
21113
+ /**
21114
+ * The instance-level (flat) findings list: one row per finding, newest first,
21115
+ * paged by keyset.
21116
+ *
21117
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21118
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21119
+ * them changes no reported number. Severity, subtype, provider, action,
21120
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21121
+ * facet excludes its own filter, so a row the filter rejects still has to be
21122
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21123
+ * Several could not be expressed there anyway: status comes from the one
21124
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21125
+ * none of the mappers names", which no IN-list can say.
21126
+ *
21127
+ * The scan runs from the top of the scope on every request, not from the
21128
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21129
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21130
+ * while the counting runs, and only the page itself is retained.
21131
+ */
21132
+ listFindingInstances(query) {
21133
+ const opts = {
21134
+ severity: query.severity,
21135
+ subtype: query.subtype,
21136
+ providers: query.provider,
21137
+ actions: query.action,
21138
+ statuses: query.status,
21139
+ tools: query.tool,
21140
+ repo: query.repo,
21141
+ file: query.file,
21142
+ q: query.q
21143
+ };
21144
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21145
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21146
+ const accumulator = createInstanceFacetAccumulator(opts);
21147
+ const items = [];
21148
+ let total = 0;
21149
+ let last;
21150
+ let hasMore = false;
21151
+ for (const row of this.scanFindingRows({
21152
+ sessionId: query.sessionId,
21153
+ from: query.from
21154
+ })) {
21155
+ accumulator.add(row);
21156
+ if (!matchesInstanceFilters(row, opts)) continue;
21157
+ total += 1;
21158
+ if (items.length < limit) {
21159
+ items.push(toInstanceDetail(row));
21160
+ last = row;
21161
+ } else {
21162
+ hasMore = true;
21163
+ }
21164
+ }
21165
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21166
+ if (cursor !== null) {
21167
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21168
+ return Promise.resolve({
21169
+ totals: { findings: total },
21170
+ facets: accumulator.facets(),
21171
+ items: resumed.items,
21172
+ nextCursor: resumed.nextCursor
21173
+ });
21174
+ }
21175
+ return Promise.resolve({
21176
+ totals: { findings: total },
21177
+ facets: accumulator.facets(),
21178
+ items,
21179
+ nextCursor
21180
+ });
21181
+ }
21182
+ /**
21183
+ * The page of matching rows strictly after `cursor`. Separate from the
21184
+ * counting pass because that one starts at the top of the scope by design;
21185
+ * this one narrows the scan with the same keyset predicate the activity list
21186
+ * uses, so a later page costs less than the first rather than more.
21187
+ */
21188
+ pageAfter(cursor, opts, limit, query) {
21189
+ const items = [];
21190
+ let last;
21191
+ let hasMore = false;
21192
+ for (const row of this.scanFindingRows({
21193
+ sessionId: query.sessionId,
21194
+ from: query.from,
21195
+ after: cursor
21196
+ })) {
21197
+ if (!matchesInstanceFilters(row, opts)) continue;
21198
+ if (items.length < limit) {
21199
+ items.push(toInstanceDetail(row));
21200
+ last = row;
21201
+ } else {
21202
+ hasMore = true;
21203
+ break;
21204
+ }
21205
+ }
21206
+ return {
21207
+ items,
21208
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21209
+ };
21210
+ }
21211
+ /**
21212
+ * The same findings folded by location: repository, then file within it.
21213
+ *
21214
+ * The grouping keys come from the capturing event's attributes, which is what
21215
+ * the local store relates a finding to — there is no finding↔asset row to
21216
+ * group by instead. A repo or file the event did not record folds into the
21217
+ * empty-string bucket, which the view renders but does not link, since no
21218
+ * filter can name it.
21219
+ */
21220
+ listFindingLocations(query) {
21221
+ const opts = {
21222
+ severity: query.severity,
21223
+ subtype: query.subtype,
21224
+ providers: query.provider,
21225
+ actions: query.action,
21226
+ statuses: query.status,
21227
+ tools: query.tool,
21228
+ q: query.q
21229
+ };
21230
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21231
+ const byRepo = /* @__PURE__ */ new Map();
21232
+ let total = 0;
21233
+ for (const row of this.scanFindingRows({
21234
+ sessionId: query.sessionId,
21235
+ from: query.from
21236
+ })) {
21237
+ if (!matchesInstanceFilters(row, opts)) continue;
21238
+ total += 1;
21239
+ let files = byRepo.get(row.repo);
21240
+ if (files === void 0) {
21241
+ files = /* @__PURE__ */ new Map();
21242
+ byRepo.set(row.repo, files);
21243
+ }
21244
+ let acc = files.get(row.file);
21245
+ if (acc === void 0) {
21246
+ acc = newLocationAccumulator();
21247
+ files.set(row.file, acc);
21248
+ }
21249
+ addToLocation(acc, row);
21250
+ }
21251
+ let fileCount = 0;
21252
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21253
+ fileCount += files.size;
21254
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21255
+ file: file2,
21256
+ instanceCount: acc.instanceCount,
21257
+ maxSeverity: acc.maxSeverity,
21258
+ latestDetectedAt: acc.latestDetectedAt,
21259
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21260
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21261
+ })).sort(compareLocationOrder);
21262
+ const rollup = fileRows.reduce(
21263
+ (a, f) => ({
21264
+ instanceCount: a.instanceCount + f.instanceCount,
21265
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21266
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21267
+ }),
21268
+ {
21269
+ instanceCount: 0,
21270
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21271
+ latestDetectedAt: ""
21272
+ }
21273
+ );
21274
+ const statuses = fileRows.map((f) => f.status);
21275
+ const folded = foldGroupStatus(statuses);
21276
+ return {
21277
+ repo,
21278
+ instanceCount: rollup.instanceCount,
21279
+ maxSeverity: rollup.maxSeverity,
21280
+ latestDetectedAt: rollup.latestDetectedAt,
21281
+ ...folded === void 0 ? {} : { status: folded },
21282
+ files: fileRows
21283
+ };
21284
+ });
21285
+ repos.sort(compareLocationOrder);
21286
+ return Promise.resolve({
21287
+ totals: { findings: total, repos: repos.length, files: fileCount },
21288
+ items: repos.slice(0, limit),
21289
+ hasMore: repos.length > limit
21290
+ });
21291
+ }
21292
+ /**
21293
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21294
+ *
21295
+ * A generator so a caller streams the scope without it ever being an array:
21296
+ * the flat list counts and facets the whole filtered scope, which on a large
21297
+ * store is far more rows than any page. Each batch advances the same keyset
21298
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21299
+ * rather than one unbounded result set.
21300
+ *
21301
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21302
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21303
+ * makes it a point lookup per row, and the derived table would re-materialize
21304
+ * a window over the whole resolution table once per batch.
21305
+ *
21306
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21307
+ * would be missing from its own facet, which is computed by excluding that
21308
+ * dimension — see listFindingInstances.
21309
+ */
21310
+ *scanFindingRows(scope) {
21311
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21312
+ const params = [];
21313
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21314
+ conditions.push("e.root_session_id = ?");
21315
+ params.push(scope.sessionId);
21316
+ }
21317
+ if (scope.from !== void 0) {
21318
+ conditions.push("e.started_at >= ?");
21319
+ params.push(isoToEpochMillis(scope.from));
21320
+ }
21321
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21322
+ d.severity AS severity, f.masked_match AS masked_match,
21323
+ f.action_taken AS action_taken, f.confidence AS confidence,
21324
+ e.started_at AS occurred_at,
21325
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21326
+ json_extract(e.attributes, '$.repo') AS repo,
21327
+ json_extract(e.attributes, '$.file_path') AS file,
21328
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21329
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21330
+ e.event_type AS kind, f.finding_key AS finding_key,
21331
+ ${latestResolutionStatusSql("f")} AS latest_status
21332
+ FROM inspection_findings f
21333
+ JOIN audit_events e ON e.id = f.audit_event_id
21334
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21335
+ WHERE ${conditions.join(" AND ")}
21336
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21337
+ ORDER BY e.started_at DESC, f.id DESC
21338
+ LIMIT ?`;
21339
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21340
+ for (; ; ) {
21341
+ const rows = allRows(this.db.prepare(sql), [
21342
+ ...params,
21343
+ after.startedAtMs,
21344
+ after.startedAtMs,
21345
+ after.id,
21346
+ SCAN_BATCH_ROWS
21347
+ ]);
21348
+ for (const r of rows) {
21349
+ yield {
21350
+ id: r.id,
21351
+ ruleId: r.rule_id,
21352
+ category: r.category,
21353
+ severity: r.severity,
21354
+ maskedMatch: r.masked_match,
21355
+ actionTaken: r.action_taken,
21356
+ confidence: r.confidence,
21357
+ occurredAt: epochMillisToIso(r.occurred_at),
21358
+ sourceTool: r.source_tool,
21359
+ repo: r.repo ?? "",
21360
+ file: r.file ?? "",
21361
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21362
+ eventId: r.event_id,
21363
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21364
+ status: deriveInstanceStatus(r)
21365
+ };
21366
+ }
21367
+ if (rows.length < SCAN_BATCH_ROWS) return;
21368
+ const lastRow = rows[rows.length - 1];
21369
+ if (lastRow === void 0) return;
21370
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21371
+ }
21372
+ }
20535
21373
  groupAggregates(withSearchText, scope) {
20536
21374
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20537
21375
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20792,7 +21630,7 @@ var SqliteInspectionFindingsRepository = class {
20792
21630
  };
20793
21631
 
20794
21632
  // ../../packages/persistence/src/repositories/installed-packs.ts
20795
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21633
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20796
21634
 
20797
21635
  // ../../packages/persistence/src/semver.ts
20798
21636
  function parse3(version2) {
@@ -20943,7 +21781,7 @@ var SqliteInstalledPacksRepository = class {
20943
21781
  let behind = false;
20944
21782
  for (const row of rows) {
20945
21783
  const params = {
20946
- id: randomUUID2(),
21784
+ id: randomUUID3(),
20947
21785
  namespace: row.namespace,
20948
21786
  packId: row.packId,
20949
21787
  version: row.version,
@@ -20955,7 +21793,7 @@ var SqliteInstalledPacksRepository = class {
20955
21793
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20956
21794
  this.upsertAvailableStmt.run({
20957
21795
  ...params,
20958
- id: randomUUID2(),
21796
+ id: randomUUID3(),
20959
21797
  recordedBy: meta3?.recordedBy ?? null
20960
21798
  });
20961
21799
  } else {
@@ -21278,14 +22116,15 @@ var SqliteInventoryRepository = class {
21278
22116
  };
21279
22117
 
21280
22118
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21281
- import { randomUUID as randomUUID3 } from "crypto";
22119
+ import { randomUUID as randomUUID4 } from "crypto";
21282
22120
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21283
22121
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21284
22122
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21285
22123
  var HARNESS_LABELS = {
21286
22124
  claudecode: "Claude Code",
21287
22125
  cursor: "Cursor",
21288
- codex: "Codex"
22126
+ codex: "Codex",
22127
+ antigravity: "Antigravity"
21289
22128
  };
21290
22129
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21291
22130
  var EMPTY_PROJECT_AGG = {
@@ -21300,6 +22139,7 @@ function resolveHarnessId(attrs, row) {
21300
22139
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21301
22140
  if (t.includes("cursor")) return "cursor";
21302
22141
  if (t.includes("codex")) return "codex";
22142
+ if (t.includes("antigravity")) return "antigravity";
21303
22143
  return null;
21304
22144
  }
21305
22145
  function isLiveRealClaudeCode(rows) {
@@ -21758,7 +22598,7 @@ var SqliteInventoryAssetsRepository = class {
21758
22598
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21759
22599
  VALUES (:id, :projectId, :path, :access, :now, :now)
21760
22600
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21761
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22601
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21762
22602
  }
21763
22603
  return true;
21764
22604
  }
@@ -21779,7 +22619,7 @@ var SqliteInventoryAssetsRepository = class {
21779
22619
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21780
22620
  VALUES (:id, :assetId, :trust, :now, :now)
21781
22621
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21782
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22622
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21783
22623
  }
21784
22624
  this.configRowsCache = void 0;
21785
22625
  return "ok";
@@ -22076,7 +22916,7 @@ var SqliteInventoryAssetsRepository = class {
22076
22916
  };
22077
22917
 
22078
22918
  // ../../packages/persistence/src/repositories/policies.ts
22079
- import { randomUUID as randomUUID4 } from "crypto";
22919
+ import { randomUUID as randomUUID5 } from "crypto";
22080
22920
  var SqlitePoliciesRepository = class {
22081
22921
  constructor(db) {
22082
22922
  this.db = db;
@@ -22111,7 +22951,7 @@ var SqlitePoliciesRepository = class {
22111
22951
  failOpenTransaction(this.db, () => {
22112
22952
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
22113
22953
  stmt.run({
22114
- id: randomUUID4(),
22954
+ id: randomUUID5(),
22115
22955
  target: JSON.stringify({ category }),
22116
22956
  action,
22117
22957
  now: Date.now()
@@ -22131,7 +22971,7 @@ var SqlitePoliciesRepository = class {
22131
22971
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
22132
22972
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
22133
22973
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
22134
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22974
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
22135
22975
  }
22136
22976
  // Caps every global per-category policy currently set to block/redact down
22137
22977
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22199,7 +23039,7 @@ var SqlitePolicyCatalogRepository = class {
22199
23039
  };
22200
23040
 
22201
23041
  // ../../packages/persistence/src/repositories/project-files.ts
22202
- import { randomUUID as randomUUID5 } from "crypto";
23042
+ import { randomUUID as randomUUID6 } from "crypto";
22203
23043
  var SqliteProjectFilesRepository = class {
22204
23044
  constructor(db) {
22205
23045
  this.db = db;
@@ -22231,7 +23071,7 @@ var SqliteProjectFilesRepository = class {
22231
23071
  const stamp = Math.max(now, maxStamp + 1);
22232
23072
  for (const file2 of scan2.files) {
22233
23073
  this.upsertStmt.run({
22234
- id: randomUUID5(),
23074
+ id: randomUUID6(),
22235
23075
  projectId,
22236
23076
  path: file2.path,
22237
23077
  name: file2.name,
@@ -22245,7 +23085,7 @@ var SqliteProjectFilesRepository = class {
22245
23085
  };
22246
23086
 
22247
23087
  // ../../packages/persistence/src/repositories/resolutions.ts
22248
- import { randomUUID as randomUUID6 } from "crypto";
23088
+ import { randomUUID as randomUUID7 } from "crypto";
22249
23089
  var SqliteResolutionsRepository = class {
22250
23090
  constructor(db, now = () => Date.now()) {
22251
23091
  this.db = db;
@@ -22299,7 +23139,7 @@ var SqliteResolutionsRepository = class {
22299
23139
  */
22300
23140
  insertResolution(r) {
22301
23141
  this.insertStmt.run({
22302
- id: randomUUID6(),
23142
+ id: randomUUID7(),
22303
23143
  findingKey: r.findingKey,
22304
23144
  status: FindingStatus.parse(r.status),
22305
23145
  method: ResolutionMethod.parse(r.method),
@@ -22358,13 +23198,51 @@ var SqliteRuleProbeCacheRepository = class {
22358
23198
  this.readStmt = db.prepare(
22359
23199
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22360
23200
  );
23201
+ this.countQuarantinedStmt = db.prepare(
23202
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23203
+ );
23204
+ this.clearQuarantinedStmt = db.prepare(
23205
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23206
+ );
22361
23207
  }
22362
23208
  db;
22363
23209
  upsertStmt;
22364
23210
  readStmt;
23211
+ countQuarantinedStmt;
23212
+ clearQuarantinedStmt;
22365
23213
  getVerdict(ruleKey) {
22366
23214
  return getRow(this.readStmt, { ruleKey });
22367
23215
  }
23216
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23217
+ countQuarantined() {
23218
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23219
+ }
23220
+ /**
23221
+ * Forgets every quarantine verdict, so the rules behind them are measured
23222
+ * again on the next load. This is the undo for a verdict the machine reached
23223
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23224
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23225
+ * loaded or slow machine can reach about a rule that is in fact fine.
23226
+ *
23227
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23228
+ * keeping, and dropping it would make every rule pay the battery again.
23229
+ *
23230
+ * Reports `refused` from the write's own result rather than inferring it from
23231
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23232
+ * swallows a contended DELETE (another writer holding the lock past
23233
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23234
+ * leaves the count unchanged, which is indistinguishable from "there was
23235
+ * nothing to clear". An undo that reports success while the quarantines are
23236
+ * still in place is worse than one that fails, because the rules it claimed
23237
+ * to restore are silently still disabled.
23238
+ */
23239
+ clearQuarantined() {
23240
+ const before = this.countQuarantined();
23241
+ const committed = failOpenTransaction(this.db, () => {
23242
+ this.clearQuarantinedStmt.run();
23243
+ });
23244
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23245
+ }
22368
23246
  setVerdict(ruleKey, verdict, worstProbeMs2) {
22369
23247
  failOpenTransaction(this.db, () => {
22370
23248
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
@@ -22419,7 +23297,39 @@ var SqliteScanLedgerRepository = class {
22419
23297
  };
22420
23298
 
22421
23299
  // ../../packages/persistence/src/repositories/secret-vault.ts
22422
- import { randomUUID as randomUUID7 } from "crypto";
23300
+ import { randomUUID as randomUUID8 } from "crypto";
23301
+ function pageLimit(requested, fallback) {
23302
+ if (requested === void 0) return fallback;
23303
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23304
+ }
23305
+ function encodeReuseCursor(payload) {
23306
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23307
+ }
23308
+ function decodeReuseCursor(cursor) {
23309
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23310
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23311
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23312
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23313
+ // malformed cursor must never produce, since restarting from the top is the
23314
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23315
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23316
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23317
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23318
+ }
23319
+ return null;
23320
+ }
23321
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23322
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23323
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23324
+ v.occurrence_count, v.first_seen, v.last_seen`;
23325
+ function toSighting(row) {
23326
+ return {
23327
+ location: row.location,
23328
+ kind: row.kind,
23329
+ firstSeen: new Date(row.first_seen).toISOString(),
23330
+ lastSeen: new Date(row.last_seen).toISOString()
23331
+ };
23332
+ }
22423
23333
  var SELECT_COLUMNS = `
22424
23334
  pointer_id AS pointerId,
22425
23335
  value_fingerprint AS valueFingerprint,
@@ -22603,39 +23513,67 @@ var SqliteSecretVaultRepository = class {
22603
23513
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
22604
23514
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22605
23515
  ).run({
22606
- id: randomUUID7(),
23516
+ id: randomUUID8(),
22607
23517
  pointerId: entry.pointerId,
22608
23518
  location: entry.location,
22609
23519
  kind: entry.kind,
22610
23520
  now
22611
23521
  });
22612
23522
  }
22613
- listSightings(pointerId) {
23523
+ /**
23524
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23525
+ * than one query per row. A pointer with no sightings still gets an entry, so
23526
+ * the caller never has to distinguish "none" from "missing".
23527
+ *
23528
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23529
+ * the instance the way the fixed-shape ones in the constructor are.
23530
+ */
23531
+ sightingsFor(pointerIds) {
23532
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23533
+ if (pointerIds.length === 0) return byPointer;
22614
23534
  const rows = allRows(
22615
23535
  this.db.prepare(
22616
- `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22617
- WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
23536
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23537
+ FROM secret_vault_sighting
23538
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23539
+ ORDER BY last_seen DESC`
22618
23540
  ),
22619
- { pointerId }
23541
+ pointerIds
22620
23542
  );
23543
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23544
+ return byPointer;
23545
+ }
23546
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23547
+ toInventoryEntries(rows) {
23548
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
22621
23549
  return rows.map((r) => ({
22622
- location: r.location,
22623
- kind: r.kind,
23550
+ pointerId: r.pointer_id,
23551
+ category: r.category,
23552
+ ...r.provider === null ? {} : { provider: r.provider },
23553
+ maskedMatch: r.masked_match,
23554
+ occurrences: r.occurrence_count,
22624
23555
  firstSeen: new Date(r.first_seen).toISOString(),
22625
- lastSeen: new Date(r.last_seen).toISOString()
23556
+ lastSeen: new Date(r.last_seen).toISOString(),
23557
+ revealGrantId: r.grant_id,
23558
+ sightings: sightings.get(r.pointer_id) ?? []
22626
23559
  }));
22627
23560
  }
22628
23561
  /**
22629
- * The dashboard inventory: every vaulted value's descriptor data joined with
22630
- * its sightings and the active reveal-to-model grant when one exists.
22631
- * Raw-free by construction — neither the fingerprint nor the ciphertext
22632
- * columns are selected.
23562
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23563
+ * value's descriptor data joined with its sightings and the active
23564
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23565
+ * the fingerprint nor the ciphertext columns are selected.
23566
+ *
23567
+ * `totals.values` counts the whole store, not the page, so the count a reader
23568
+ * sees never depends on how far they have paged.
22633
23569
  */
22634
- listInventory(now = Date.now()) {
23570
+ listInventory(query = {}, now = Date.now()) {
23571
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23572
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23573
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
22635
23574
  const rows = allRows(
22636
23575
  this.db.prepare(
22637
- `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22638
- v.occurrence_count, v.first_seen, v.last_seen,
23576
+ `SELECT ${INVENTORY_COLUMNS},
22639
23577
  (SELECT e.id FROM exceptions e
22640
23578
  WHERE e.rule_id = v.rule_id
22641
23579
  AND e.value_fingerprint = v.value_fingerprint
@@ -22643,45 +23581,109 @@ var SqliteSecretVaultRepository = class {
22643
23581
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22644
23582
  LIMIT 1) AS grant_id
22645
23583
  FROM secret_vault v
22646
- ORDER BY v.last_seen DESC`
23584
+ ${where}
23585
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23586
+ LIMIT :limit`
22647
23587
  ),
22648
- { now }
23588
+ bindParams({
23589
+ now,
23590
+ limit: limit + 1,
23591
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23592
+ })
22649
23593
  );
22650
- return rows.map((r) => ({
22651
- pointerId: r.pointer_id,
22652
- category: r.category,
22653
- ...r.provider === null ? {} : { provider: r.provider },
22654
- maskedMatch: r.masked_match,
22655
- occurrences: r.occurrence_count,
22656
- firstSeen: new Date(r.first_seen).toISOString(),
22657
- lastSeen: new Date(r.last_seen).toISOString(),
22658
- revealGrantId: r.grant_id,
22659
- sightings: this.listSightings(r.pointer_id)
22660
- }));
23594
+ const hasMore = rows.length > limit;
23595
+ const page = hasMore ? rows.slice(0, limit) : rows;
23596
+ const last = page[page.length - 1];
23597
+ return {
23598
+ totals: { values: this.countEntries() },
23599
+ items: this.toInventoryEntries(page),
23600
+ // Minted from the last row of the PAGE, never the extra probe row.
23601
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23602
+ };
23603
+ }
23604
+ /**
23605
+ * Values reused on this machine — detected more than once, or written to more
23606
+ * than one location — most-reused first, one page at a time.
23607
+ *
23608
+ * Its own read rather than a filter over an inventory page: reuse is a
23609
+ * property of the whole store, and deriving it from 50 newest rows would
23610
+ * under-report exactly the values a reader most needs to see.
23611
+ */
23612
+ listReuse(query = {}, now = Date.now()) {
23613
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23614
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23615
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23616
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23617
+ const rows = allRows(
23618
+ this.db.prepare(
23619
+ `SELECT ${INVENTORY_COLUMNS},
23620
+ (SELECT e.id FROM exceptions e
23621
+ WHERE e.rule_id = v.rule_id
23622
+ AND e.value_fingerprint = v.value_fingerprint
23623
+ AND e.key_version = v.fingerprint_key_version
23624
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23625
+ LIMIT 1) AS grant_id
23626
+ FROM secret_vault v
23627
+ WHERE ${REUSED_PREDICATE} ${after}
23628
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23629
+ LIMIT :limit`
23630
+ ),
23631
+ bindParams({
23632
+ now,
23633
+ limit: limit + 1,
23634
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23635
+ })
23636
+ );
23637
+ const hasMore = rows.length > limit;
23638
+ const page = hasMore ? rows.slice(0, limit) : rows;
23639
+ const last = page[page.length - 1];
23640
+ return {
23641
+ totals: { reused: this.countReused() },
23642
+ items: this.toInventoryEntries(page),
23643
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23644
+ };
22661
23645
  }
22662
23646
  /**
22663
- * The de-reference trail, newest first. By default the batched, high-volume
22664
- * reasons (display, view-render) are hidden and counted instead — the rows
22665
- * that matter as a signal are the model crossings, and burying them under
22666
- * render noise would defeat the audit's purpose.
23647
+ * The de-reference trail, newest first, one page at a time. By default the
23648
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23649
+ * instead — the rows that matter as a signal are the model crossings, and
23650
+ * burying them under render noise would defeat the audit's purpose.
23651
+ *
23652
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23653
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23654
+ * the reader pages.
22667
23655
  */
22668
- listDerefs(opts) {
22669
- const limit = opts?.limit ?? 200;
22670
- const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
23656
+ listDerefs(query = {}) {
23657
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23658
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23659
+ const conditions = [];
23660
+ if (query.includeBatched !== true) {
23661
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23662
+ }
23663
+ if (cursor !== null) {
23664
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23665
+ }
23666
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
22671
23667
  const rows = allRows(
22672
23668
  this.db.prepare(
22673
23669
  `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22674
23670
  FROM secret_vault_deref ${where}
22675
- ORDER BY at DESC, rowid DESC LIMIT :limit`
23671
+ ORDER BY at DESC, id DESC LIMIT :limit`
22676
23672
  ),
22677
- { limit }
23673
+ bindParams({
23674
+ limit: limit + 1,
23675
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23676
+ })
22678
23677
  );
22679
- const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
23678
+ const hasMore = rows.length > limit;
23679
+ const page = hasMore ? rows.slice(0, limit) : rows;
23680
+ const last = page[page.length - 1];
23681
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
22680
23682
  this.db,
22681
23683
  `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22682
23684
  );
22683
23685
  return {
22684
- rows: rows.map((r) => ({
23686
+ items: page.map((r) => ({
22685
23687
  id: r.id,
22686
23688
  pointerId: r.pointer_id,
22687
23689
  at: new Date(r.at).toISOString(),
@@ -22691,12 +23693,20 @@ var SqliteSecretVaultRepository = class {
22691
23693
  ...r.grant_id === null ? {} : { grantId: r.grant_id },
22692
23694
  pointerCount: r.pointer_count
22693
23695
  })),
23696
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
22694
23697
  hiddenBatched
22695
23698
  };
22696
23699
  }
22697
23700
  countEntries() {
22698
23701
  return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22699
23702
  }
23703
+ /** Values reused on this machine — the reuse list's page-independent total. */
23704
+ countReused() {
23705
+ return countScalar(
23706
+ this.db,
23707
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23708
+ );
23709
+ }
22700
23710
  };
22701
23711
 
22702
23712
  // ../../packages/persistence/src/repositories/security.ts
@@ -22711,7 +23721,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22711
23721
  var SCAN_COVERAGE = [
22712
23722
  { provider: "claudecode", coverage: 100, supported: true },
22713
23723
  { provider: "cursor", coverage: 0, supported: false },
22714
- { provider: "codex", coverage: 0, supported: false },
23724
+ { provider: "codex", coverage: 80, supported: true },
23725
+ { provider: "antigravity", coverage: 60, supported: true },
23726
+ { provider: "claudeai", coverage: 0, supported: false },
22715
23727
  { provider: "chatgpt", coverage: 0, supported: false },
22716
23728
  { provider: "copilot", coverage: 0, supported: false },
22717
23729
  { provider: "api", coverage: 0, supported: false }
@@ -23044,7 +24056,7 @@ var SqliteSecurityRepository = class {
23044
24056
  };
23045
24057
 
23046
24058
  // ../../packages/persistence/src/repositories/shares.ts
23047
- import { randomUUID as randomUUID8 } from "crypto";
24059
+ import { randomUUID as randomUUID9 } from "crypto";
23048
24060
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
23049
24061
  var IN_CHUNK = 500;
23050
24062
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -23300,7 +24312,7 @@ var SqliteSharesRepository = class {
23300
24312
  (id, destination_id, host, decision, created_at, updated_at)
23301
24313
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
23302
24314
  ).run({
23303
- id: randomUUID8(),
24315
+ id: randomUUID9(),
23304
24316
  destinationId,
23305
24317
  host: dest.host,
23306
24318
  decision,
@@ -23449,7 +24461,7 @@ var SqliteSharesRepository = class {
23449
24461
  let destinationId = destIds.get(hit.host);
23450
24462
  if (destinationId === void 0) {
23451
24463
  destStmt.run({
23452
- id: randomUUID8(),
24464
+ id: randomUUID9(),
23453
24465
  kind: hit.kind,
23454
24466
  name: hit.name,
23455
24467
  host: hit.host,
@@ -23465,7 +24477,7 @@ var SqliteSharesRepository = class {
23465
24477
  let endpointId = endpointIds.get(endpointKey);
23466
24478
  if (endpointId === void 0) {
23467
24479
  endpointStmt.run({
23468
- id: randomUUID8(),
24480
+ id: randomUUID9(),
23469
24481
  destinationId,
23470
24482
  method: hit.method,
23471
24483
  transport: hit.transport,
@@ -23478,7 +24490,7 @@ var SqliteSharesRepository = class {
23478
24490
  endpointIds.set(endpointKey, endpointId);
23479
24491
  }
23480
24492
  siteStmt.run({
23481
- id: randomUUID8(),
24493
+ id: randomUUID9(),
23482
24494
  endpointId,
23483
24495
  project: input.project,
23484
24496
  projectKey: input.projectKey,
@@ -23843,6 +24855,9 @@ function purgeSampleData(db) {
23843
24855
  }
23844
24856
 
23845
24857
  // ../../packages/persistence/src/database.ts
24858
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24859
+ "aka.persistence.unsafeTestOnlyRawHandle"
24860
+ );
23846
24861
  function linkHost(input, hostId) {
23847
24862
  return hostId ? { ...input, hostId } : input;
23848
24863
  }
@@ -23864,21 +24879,34 @@ function openWithPragmas(file2) {
23864
24879
  }
23865
24880
  return db;
23866
24881
  }
23867
- function backupLegacyStore(file2) {
23868
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23869
- renameSync2(file2, backup);
23870
- tightenFile(backup);
23871
- for (const sidecar of dbSidecars(file2)) {
23872
- if (existsSync(sidecar)) rmSync2(sidecar);
24882
+ function backupLegacyStore(db, file2) {
24883
+ reapStalePartials(file2);
24884
+ const backup = backupPath(file2, "legacy");
24885
+ let snapshotted = false;
24886
+ let snapshotError;
24887
+ try {
24888
+ snapshotStore(db, backup);
24889
+ snapshotted = true;
24890
+ } catch (error51) {
24891
+ snapshotError = error51;
24892
+ } finally {
24893
+ db.close();
24894
+ }
24895
+ if (!snapshotted) {
24896
+ akaWarn(
24897
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24898
+ );
24899
+ moveStoreAside(file2, backup);
24900
+ return backup;
23873
24901
  }
24902
+ discardStore(file2, backup);
23874
24903
  return backup;
23875
24904
  }
23876
24905
  function openAndInitialize(file2) {
23877
24906
  let db = openWithPragmas(file2);
23878
24907
  try {
23879
24908
  if (isForeignSqliteLineage(db)) {
23880
- db.close();
23881
- const backup = backupLegacyStore(file2);
24909
+ const backup = backupLegacyStore(db, file2);
23882
24910
  db = openWithPragmas(file2);
23883
24911
  akaWarn(
23884
24912
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23922,7 +24950,7 @@ function openAndInitialize(file2) {
23922
24950
  }
23923
24951
  function openLocalDatabase(dir) {
23924
24952
  ensureDataDirSync(dir);
23925
- const file2 = join(dir, DB_FILENAME);
24953
+ const file2 = join2(dir, DB_FILENAME);
23926
24954
  const {
23927
24955
  db,
23928
24956
  events,
@@ -24039,7 +25067,7 @@ function openLocalDatabase(dir) {
24039
25067
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
24040
25068
  if (!definitionId) continue;
24041
25069
  inspectionFindings.insertFinding({
24042
- id: randomUUID9(),
25070
+ id: randomUUID10(),
24043
25071
  auditEventId: record2.scanEvent.id,
24044
25072
  inspectionDefinitionId: definitionId,
24045
25073
  span: finding.span,
@@ -24145,10 +25173,26 @@ function openLocalDatabase(dir) {
24145
25173
  transaction,
24146
25174
  close: () => {
24147
25175
  db.close();
24148
- }
25176
+ },
25177
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25178
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
24149
25179
  };
24150
25180
  }
24151
25181
 
25182
+ // ../../packages/persistence/src/file-lock.ts
25183
+ import { randomUUID as randomUUID11 } from "crypto";
25184
+ import {
25185
+ closeSync,
25186
+ existsSync as existsSync2,
25187
+ openSync,
25188
+ readFileSync,
25189
+ rmSync as rmSync3,
25190
+ statSync as statSync2,
25191
+ writeFileSync as writeFileSync2
25192
+ } from "fs";
25193
+ import { hostname as hostname3 } from "os";
25194
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25195
+
24152
25196
  // ../../packages/persistence/src/finding-key.ts
24153
25197
  import { createHash as createHash3 } from "crypto";
24154
25198
  function normalizeFilePath(filePath) {
@@ -24161,13 +25205,13 @@ function computeFindingKey(input) {
24161
25205
 
24162
25206
  // ../../packages/persistence/src/fingerprint.ts
24163
25207
  import { createHmac, randomBytes } from "crypto";
24164
- import { existsSync as existsSync2, readFileSync } from "fs";
24165
- import { join as join2 } from "path";
25208
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25209
+ import { join as join3 } from "path";
24166
25210
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
24167
- var KEY_FILENAME = "exception.key";
25211
+ var EXCEPTION_KEY_FILENAME = "exception.key";
24168
25212
  var KEY_MATERIAL_BYTES = 32;
24169
25213
  function keyFilePath(dataDir2) {
24170
- return join2(dataDir2, KEY_FILENAME);
25214
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
24171
25215
  }
24172
25216
  function parseKeyFile(raw) {
24173
25217
  const parsed = JSON.parse(raw);
@@ -24205,8 +25249,8 @@ var FloorUnreadableError = class extends Error {
24205
25249
  }
24206
25250
  };
24207
25251
  function storedKeyVersionFloor(dataDir2) {
24208
- const file2 = join2(dataDir2, DB_FILENAME);
24209
- if (!existsSync2(file2)) return 0;
25252
+ const file2 = join3(dataDir2, DB_FILENAME);
25253
+ if (!existsSync3(file2)) return 0;
24210
25254
  let db;
24211
25255
  try {
24212
25256
  db = new DatabaseSync2(file2, { readOnly: true });
@@ -24231,18 +25275,36 @@ function storedKeyVersionFloor(dataDir2) {
24231
25275
  db?.close();
24232
25276
  }
24233
25277
  }
24234
- function writeKeyFile(dataDir2, key) {
25278
+ function serializeKey(key) {
25279
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
25280
+ }
25281
+ function createKeyFile(dataDir2, key) {
24235
25282
  ensureDataDirSync(dataDir2);
24236
25283
  const file2 = keyFilePath(dataDir2);
24237
- const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
24238
- writeOwnerOnlyFileSync(file2, `${body}
24239
- `);
24240
- return key;
25284
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
25285
+ `)) return key;
25286
+ const winner = readFingerprintKey(dataDir2);
25287
+ if (winner) {
25288
+ tightenFile(file2);
25289
+ return winner;
25290
+ }
25291
+ const occupant = classifyOccupant(file2);
25292
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
25293
+ }
25294
+ function occupantMessage(file2, kind) {
25295
+ switch (kind) {
25296
+ case "symlink":
25297
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
25298
+ case "gone":
25299
+ return "exception key file was removed while it was being created";
25300
+ case "unknown":
25301
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
25302
+ }
24241
25303
  }
24242
25304
  function readFingerprintKey(dataDir2) {
24243
25305
  let raw;
24244
25306
  try {
24245
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25307
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
24246
25308
  } catch (err) {
24247
25309
  if (err.code === "ENOENT") return null;
24248
25310
  throw err instanceof Error ? err : new Error(String(err));
@@ -24255,7 +25317,7 @@ function loadOrCreateFingerprintKey(dataDir2) {
24255
25317
  tightenFile(keyFilePath(dataDir2));
24256
25318
  return existing;
24257
25319
  }
24258
- return writeKeyFile(dataDir2, {
25320
+ return createKeyFile(dataDir2, {
24259
25321
  version: storedKeyVersionFloor(dataDir2) + 1,
24260
25322
  material: randomBytes(KEY_MATERIAL_BYTES)
24261
25323
  });
@@ -24268,18 +25330,18 @@ function fingerprintValue(key, raw) {
24268
25330
  import { renameSync as renameSync3 } from "fs";
24269
25331
  import { mkdir } from "fs/promises";
24270
25332
  import { homedir } from "os";
24271
- import { join as join3 } from "path";
25333
+ import { join as join4 } from "path";
24272
25334
  function defaultDataDir() {
24273
- return join3(homedir(), ".aka");
25335
+ return join4(homedir(), ".aka");
24274
25336
  }
24275
25337
  function settingsDir(base = defaultDataDir()) {
24276
- return join3(base, "settings");
25338
+ return join4(base, "settings");
24277
25339
  }
24278
25340
  function dataDir(base = defaultDataDir()) {
24279
- return join3(base, "data");
25341
+ return join4(base, "data");
24280
25342
  }
24281
25343
  function dbPath(base = defaultDataDir()) {
24282
- return join3(dataDir(base), "aka.db");
25344
+ return join4(dataDir(base), "aka.db");
24283
25345
  }
24284
25346
  function ensureLayoutDirSync(dir = defaultDataDir()) {
24285
25347
  ensureDataDirSync(dir);
@@ -24292,8 +25354,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24292
25354
  for (const { name, dest } of moves) {
24293
25355
  try {
24294
25356
  ensureDataDirSync(dest);
24295
- const moved = join3(dest, name);
24296
- renameSync3(join3(base, name), moved);
25357
+ const moved = join4(dest, name);
25358
+ renameSync3(join4(base, name), moved);
24297
25359
  tightenFile(moved);
24298
25360
  } catch {
24299
25361
  }
@@ -24301,10 +25363,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24301
25363
  }
24302
25364
 
24303
25365
  // ../../packages/persistence/src/settings.ts
24304
- import { readFileSync as readFileSync2 } from "fs";
24305
- import { join as join4 } from "path";
25366
+ import { readFileSync as readFileSync3 } from "fs";
25367
+ import { join as join5 } from "path";
25368
+ var SETTINGS_FILENAME = "settings.json";
24306
25369
  function readWorkspaceSettings(base = defaultDataDir()) {
24307
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25370
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
24308
25371
  if (!record2) return defaultWorkspaceSettings();
24309
25372
  try {
24310
25373
  return WorkspaceSettings.parse(record2);
@@ -24315,7 +25378,7 @@ function readWorkspaceSettings(base = defaultDataDir()) {
24315
25378
  function readJson(file2) {
24316
25379
  let text;
24317
25380
  try {
24318
- text = readFileSync2(file2, "utf8");
25381
+ text = readFileSync3(file2, "utf8");
24319
25382
  } catch {
24320
25383
  return null;
24321
25384
  }
@@ -24337,27 +25400,27 @@ import { randomBytes as randomBytes2 } from "crypto";
24337
25400
  import {
24338
25401
  chmodSync as chmodSync2,
24339
25402
  mkdirSync as mkdirSync2,
24340
- readFileSync as readFileSync3,
25403
+ readFileSync as readFileSync4,
24341
25404
  renameSync as renameSync4,
24342
- rmSync as rmSync3,
24343
- statSync,
24344
- writeFileSync as writeFileSync2
25405
+ rmSync as rmSync4,
25406
+ statSync as statSync3,
25407
+ writeFileSync as writeFileSync3
24345
25408
  } from "fs";
24346
- import { join as join5 } from "path";
25409
+ import { join as join6 } from "path";
24347
25410
 
24348
25411
  // ../../packages/persistence/src/vault/vault.ts
24349
- import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
25412
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
24350
25413
 
24351
25414
  // ../../packages/persistence/src/warn-era-cap.ts
24352
- import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
24353
- import { join as join6 } from "path";
25415
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
25416
+ import { join as join7 } from "path";
24354
25417
  var MARKER = "warn-era-capped";
24355
25418
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
24356
25419
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
24357
- const marker = join6(dataDir2, MARKER);
24358
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25420
+ const marker = join7(dataDir2, MARKER);
25421
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
24359
25422
  const capped = db.policies.capCategoryActions();
24360
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
25423
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
24361
25424
  `, { mode: DATA_FILE_MODE });
24362
25425
  return { capped };
24363
25426
  }
@@ -24411,11 +25474,11 @@ function resolveProvider() {
24411
25474
  }
24412
25475
 
24413
25476
  // ../../packages/plugin-sdk/src/config.ts
24414
- function loadConfig(base = defaultDataDir()) {
25477
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
24415
25478
  try {
24416
25479
  ensureLayoutDirSync(base);
24417
- const settingsFile = join7(settingsDir(base), "settings.json");
24418
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
25480
+ const settingsFile = join8(settingsDir(base), "settings.json");
25481
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
24419
25482
  } catch {
24420
25483
  }
24421
25484
  migrateLegacyLayout(base);
@@ -24426,21 +25489,21 @@ function loadConfig(base = defaultDataDir()) {
24426
25489
  dbPath: dbPath(base),
24427
25490
  settingsDir: settingsDir(base),
24428
25491
  onboarded: settings.onboardedAt != null,
24429
- provider: resolveProviderSafe()
25492
+ provider: resolveProviderSafe(resolveProviderFn)
24430
25493
  };
24431
25494
  }
24432
- function resolveProviderSafe() {
25495
+ function resolveProviderSafe(resolveProviderFn) {
24433
25496
  try {
24434
- return resolveProvider();
25497
+ return resolveProviderFn();
24435
25498
  } catch {
24436
25499
  return { provider: "anthropic" };
24437
25500
  }
24438
25501
  }
24439
25502
 
24440
25503
  // ../../packages/plugin-sdk/src/config-inventory.ts
24441
- import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
25504
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
24442
25505
  import { homedir as homedir2 } from "os";
24443
- import { basename as basename2, join as join9 } from "path";
25506
+ import { basename as basename3, join as join10 } from "path";
24444
25507
 
24445
25508
  // ../../packages/detections/src/egress/registry.ts
24446
25509
  var EXTRACTOR_VERSION = "1";
@@ -25430,11 +26493,11 @@ var EXACT_KIND_BY_BASENAME = {
25430
26493
  "composer.json": "composer.json",
25431
26494
  "packages.config": "packages.config"
25432
26495
  };
25433
- function manifestKindOf(basename6) {
25434
- if (LOCKFILE_BASENAMES.has(basename6)) return null;
25435
- const exact = Object.hasOwn(EXACT_KIND_BY_BASENAME, basename6) ? EXACT_KIND_BY_BASENAME[basename6] : void 0;
26496
+ function manifestKindOf(basename7) {
26497
+ if (LOCKFILE_BASENAMES.has(basename7)) return null;
26498
+ const exact = Object.hasOwn(EXACT_KIND_BY_BASENAME, basename7) ? EXACT_KIND_BY_BASENAME[basename7] : void 0;
25436
26499
  if (exact !== void 0) return exact;
25437
- if (basename6.endsWith(".csproj")) return "csproj";
26500
+ if (basename7.endsWith(".csproj")) return "csproj";
25438
26501
  return null;
25439
26502
  }
25440
26503
  function extractManifestSdks(text, kind) {
@@ -27932,7 +28995,7 @@ var gcp_service_account_default = {
27932
28995
  severity: "critical",
27933
28996
  matcher: {
27934
28997
  type: "regex",
27935
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
28998
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
27936
28999
  flags: "g"
27937
29000
  },
27938
29001
  examples: [
@@ -28311,8 +29374,8 @@ function bundledDetections() {
28311
29374
  }
28312
29375
 
28313
29376
  // ../../packages/plugin-sdk/src/repo.ts
28314
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28315
- import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
29377
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
29378
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
28316
29379
  function resolveRepoIdentity(cwd) {
28317
29380
  try {
28318
29381
  const root = findGitRoot(cwd);
@@ -28325,7 +29388,7 @@ function resolveRepoIdentity(cwd) {
28325
29388
  // win32) so the persistence layer's `/`-separated checkout-path patterns
28326
29389
  // (the ghost sweep + the read-side worktree filter) match it as written.
28327
29390
  url: url2 ?? headRoot.split(sep2).join("/"),
28328
- name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
29391
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
28329
29392
  };
28330
29393
  } catch {
28331
29394
  return void 0;
@@ -28341,36 +29404,36 @@ function resolveWorktreeRoot(cwd) {
28341
29404
  function findGitRoot(start) {
28342
29405
  let dir = start;
28343
29406
  for (; ; ) {
28344
- if (existsSync5(join8(dir, ".git"))) return dir;
28345
- const parent = dirname(dir);
29407
+ if (existsSync6(join9(dir, ".git"))) return dir;
29408
+ const parent = dirname2(dir);
28346
29409
  if (parent === dir) return void 0;
28347
29410
  dir = parent;
28348
29411
  }
28349
29412
  }
28350
29413
  function resolveGitContext(root) {
28351
- const dotGit = join8(root, ".git");
29414
+ const dotGit = join9(root, ".git");
28352
29415
  try {
28353
- if (statSync2(dotGit).isDirectory()) {
28354
- return { configPath: join8(dotGit, "config"), headRoot: root };
29416
+ if (statSync4(dotGit).isDirectory()) {
29417
+ return { configPath: join9(dotGit, "config"), headRoot: root };
28355
29418
  }
28356
29419
  } catch {
28357
29420
  return void 0;
28358
29421
  }
28359
29422
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
28360
29423
  if (!target) return void 0;
28361
- const gitdir = isAbsolute(target) ? target : join8(root, target);
28362
- if (existsSync5(join8(gitdir, "config"))) {
28363
- return { configPath: join8(gitdir, "config"), headRoot: root };
29424
+ const gitdir = isAbsolute(target) ? target : join9(root, target);
29425
+ if (existsSync6(join9(gitdir, "config"))) {
29426
+ return { configPath: join9(gitdir, "config"), headRoot: root };
28364
29427
  }
28365
- const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
29428
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
28366
29429
  if (!commonRaw) return void 0;
28367
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
28368
- const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
28369
- return { configPath: join8(commonGitDir, "config"), headRoot };
29430
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
29431
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
29432
+ return { configPath: join9(commonGitDir, "config"), headRoot };
28370
29433
  }
28371
29434
  function safeRead(path) {
28372
29435
  try {
28373
- return readFileSync4(path, "utf8");
29436
+ return readFileSync5(path, "utf8");
28374
29437
  } catch {
28375
29438
  return void 0;
28376
29439
  }
@@ -28408,13 +29471,13 @@ function slugFromUrl(url2) {
28408
29471
  }
28409
29472
 
28410
29473
  // ../../packages/plugin-sdk/src/events.ts
28411
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
29474
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28412
29475
  function contentHashOf(text) {
28413
29476
  return createHash4("sha256").update(text).digest("hex");
28414
29477
  }
28415
29478
  function buildIngestEvent(input) {
28416
29479
  return {
28417
- id: randomUUID11(),
29480
+ id: randomUUID13(),
28418
29481
  sourceTool: input.sourceTool,
28419
29482
  kind: input.kind,
28420
29483
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -28425,21 +29488,473 @@ function buildIngestEvent(input) {
28425
29488
  // SDK boot in the fail-open hook path). Preserve any id the caller already set.
28426
29489
  metadata: {
28427
29490
  ...input.metadata,
28428
- correlationId: input.metadata?.correlationId ?? randomUUID11()
29491
+ correlationId: input.metadata?.correlationId ?? randomUUID13()
29492
+ }
29493
+ };
29494
+ }
29495
+
29496
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
29497
+ import { existsSync as existsSync7 } from "fs";
29498
+ import { fileURLToPath } from "url";
29499
+ import { Worker } from "worker_threads";
29500
+ var ISOLATED_SCAN_BUDGET_MS = 2e3;
29501
+ var ISOLATED_PROBE_BUDGET_MS = 1e3;
29502
+ var ISOLATED_START_BUDGET_MS = 5e3;
29503
+ var ATTRIBUTION_MIN_RULE_MS = 500;
29504
+ var ATTRIBUTION_MIN_SHARE = 0.5;
29505
+ var resolvedWorkerUrl;
29506
+ function resolveWorkerUrl() {
29507
+ if (resolvedWorkerUrl !== void 0) return resolvedWorkerUrl ?? void 0;
29508
+ for (const name of ["scan-worker.js", "scan-worker.ts"]) {
29509
+ const candidate = new URL(name, import.meta.url);
29510
+ try {
29511
+ if (existsSync7(fileURLToPath(candidate))) {
29512
+ resolvedWorkerUrl = candidate;
29513
+ return candidate;
29514
+ }
29515
+ } catch {
29516
+ }
29517
+ }
29518
+ resolvedWorkerUrl = null;
29519
+ return void 0;
29520
+ }
29521
+ function messageOf(error51) {
29522
+ return error51 instanceof Error ? error51.message : String(error51);
29523
+ }
29524
+ function createIsolatedScanner(data, opts = {}) {
29525
+ const budgetMs = opts.budgetMs ?? ISOLATED_SCAN_BUDGET_MS;
29526
+ const probeBudgetMs = opts.probeBudgetMs ?? ISOLATED_PROBE_BUDGET_MS;
29527
+ const startBudgetMs = opts.startBudgetMs ?? ISOLATED_START_BUDGET_MS;
29528
+ const minAttributionMs = opts.minAttributionMs ?? ATTRIBUTION_MIN_RULE_MS;
29529
+ let worker;
29530
+ let readyWorker;
29531
+ let broken;
29532
+ let closed = false;
29533
+ let nextJobId = 1;
29534
+ let pending;
29535
+ const terminating = /* @__PURE__ */ new Set();
29536
+ let chain = Promise.resolve();
29537
+ function clearTimers(job) {
29538
+ if (job.startupTimer !== void 0) clearTimeout(job.startupTimer);
29539
+ if (job.timer !== void 0) clearTimeout(job.timer);
29540
+ }
29541
+ function take() {
29542
+ const job = pending;
29543
+ if (!job) return void 0;
29544
+ pending = void 0;
29545
+ clearTimers(job);
29546
+ worker?.unref();
29547
+ return job;
29548
+ }
29549
+ function failPending(outcome) {
29550
+ take()?.fail(outcome);
29551
+ }
29552
+ function kill(dead) {
29553
+ if (worker === dead) worker = void 0;
29554
+ if (readyWorker === dead) readyWorker = void 0;
29555
+ const done = dead.terminate().catch(() => void 0);
29556
+ terminating.add(done);
29557
+ void done.finally(() => terminating.delete(done));
29558
+ }
29559
+ function onDeadline(job) {
29560
+ if (pending !== job) return;
29561
+ const now = performance.now();
29562
+ const runningMs = now - job.progressAt;
29563
+ const elapsedMs = now - job.startedAt;
29564
+ const blamed = job.progressIndex >= 0 && runningMs >= minAttributionMs && runningMs >= elapsedMs * ATTRIBUTION_MIN_SHARE;
29565
+ const culpritIndex = blamed ? job.progressIndex : void 0;
29566
+ kill(job.worker);
29567
+ failPending({ status: "timeout", culpritIndex, elapsedMs });
29568
+ }
29569
+ function ensureWorker() {
29570
+ if (worker) return worker;
29571
+ const url2 = opts.workerUrl ?? resolveWorkerUrl();
29572
+ if (!url2) {
29573
+ return {
29574
+ error: "the scan worker script was not found next to this bundle"
29575
+ };
29576
+ }
29577
+ let started;
29578
+ try {
29579
+ started = new Worker(url2, { workerData: data });
29580
+ } catch (error51) {
29581
+ return { error: `could not start the scan worker: ${messageOf(error51)}` };
29582
+ }
29583
+ opts.onWorkerStart?.(started.threadId);
29584
+ started.on("message", (message) => {
29585
+ if (worker !== started) return;
29586
+ if (message.kind === "ready") {
29587
+ readyWorker = started;
29588
+ if (pending?.worker === started) beginDeadline(pending);
29589
+ return;
29590
+ }
29591
+ if (message.kind === "progress") {
29592
+ if (pending?.worker === started) {
29593
+ pending.progressIndex = message.index;
29594
+ pending.progressAt = performance.now();
29595
+ }
29596
+ return;
29597
+ }
29598
+ if (pending?.id !== message.id) return;
29599
+ if (message.kind === "failed") {
29600
+ failPending({
29601
+ status: "unavailable",
29602
+ reason: `the scan worker failed: ${message.message}`
29603
+ });
29604
+ return;
29605
+ }
29606
+ const job = take();
29607
+ if (job && !job.reply(message)) {
29608
+ job.fail({ status: "unavailable", reason: "the scan worker answered the wrong job" });
29609
+ }
29610
+ });
29611
+ started.on("error", (error51) => {
29612
+ if (worker !== started) return;
29613
+ broken = messageOf(error51);
29614
+ worker = void 0;
29615
+ if (readyWorker === started) readyWorker = void 0;
29616
+ failPending({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29617
+ });
29618
+ started.on("exit", () => {
29619
+ if (worker !== started) return;
29620
+ broken ??= "the scan worker exited before answering";
29621
+ worker = void 0;
29622
+ if (readyWorker === started) readyWorker = void 0;
29623
+ failPending({ status: "unavailable", reason: "the scan worker exited before answering" });
29624
+ });
29625
+ started.unref();
29626
+ worker = started;
29627
+ return started;
29628
+ }
29629
+ function beginDeadline(job) {
29630
+ if (job.startupTimer !== void 0) {
29631
+ clearTimeout(job.startupTimer);
29632
+ job.startupTimer = void 0;
29633
+ }
29634
+ if (job.timer !== void 0) return;
29635
+ job.startedAt = performance.now();
29636
+ job.progressAt = job.startedAt;
29637
+ job.timer = setTimeout(() => {
29638
+ onDeadline(job);
29639
+ }, job.budgetMs);
29640
+ }
29641
+ function runOne(spec, fail) {
29642
+ if (closed) {
29643
+ fail({ status: "unavailable", reason: "the scan worker is closed" });
29644
+ return;
29645
+ }
29646
+ if (broken !== void 0) {
29647
+ fail({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29648
+ return;
29649
+ }
29650
+ const started = ensureWorker();
29651
+ if (!(started instanceof Worker)) {
29652
+ broken = started.error;
29653
+ fail({ status: "unavailable", reason: started.error });
29654
+ return;
29655
+ }
29656
+ const id = nextJobId++;
29657
+ const now = performance.now();
29658
+ const job = {
29659
+ id,
29660
+ worker: started,
29661
+ budgetMs: spec.budgetMs,
29662
+ startedAt: now,
29663
+ progressIndex: -1,
29664
+ progressAt: now,
29665
+ startupTimer: void 0,
29666
+ timer: void 0,
29667
+ reply: spec.reply,
29668
+ fail
29669
+ };
29670
+ pending = job;
29671
+ started.ref();
29672
+ if (readyWorker === started) {
29673
+ beginDeadline(job);
29674
+ } else {
29675
+ job.startupTimer = setTimeout(() => {
29676
+ if (pending !== job) return;
29677
+ kill(job.worker);
29678
+ failPending({
29679
+ status: "unavailable",
29680
+ reason: `the scan worker did not start within ${String(startBudgetMs)}ms`
29681
+ });
29682
+ }, startBudgetMs);
29683
+ }
29684
+ try {
29685
+ started.postMessage(spec.build(id));
29686
+ } catch (error51) {
29687
+ failPending({
29688
+ // The thread went away between the ref and the post.
29689
+ status: "unavailable",
29690
+ reason: `could not reach the scan worker: ${messageOf(error51)}`
29691
+ });
29692
+ }
29693
+ }
29694
+ function enqueue(spec) {
29695
+ const next = chain.then(
29696
+ () => new Promise((resolve) => {
29697
+ spec(resolve);
29698
+ })
29699
+ );
29700
+ chain = next.then(
29701
+ () => void 0,
29702
+ () => void 0
29703
+ );
29704
+ return next;
29705
+ }
29706
+ return {
29707
+ scan(text, context, scanOpts) {
29708
+ return enqueue((resolve) => {
29709
+ runOne(
29710
+ {
29711
+ budgetMs,
29712
+ build: (id) => ({
29713
+ kind: "scan",
29714
+ id,
29715
+ text,
29716
+ filePath: context?.filePath,
29717
+ attribute: scanOpts?.attribute === true
29718
+ }),
29719
+ reply: (message) => {
29720
+ if (message.kind !== "result") return false;
29721
+ resolve({ status: "ok", findings: message.findings });
29722
+ return true;
29723
+ }
29724
+ },
29725
+ resolve
29726
+ );
29727
+ });
29728
+ },
29729
+ probe(rule) {
29730
+ return enqueue((resolve) => {
29731
+ runOne(
29732
+ {
29733
+ budgetMs: probeBudgetMs,
29734
+ build: (id) => ({ kind: "probe", id, rule }),
29735
+ reply: (message) => {
29736
+ if (message.kind !== "probed") return false;
29737
+ resolve({ status: "ok", safe: message.safe, worstMs: message.worstMs });
29738
+ return true;
29739
+ }
29740
+ },
29741
+ resolve
29742
+ );
29743
+ });
29744
+ },
29745
+ async close() {
29746
+ closed = true;
29747
+ const live = worker;
29748
+ worker = void 0;
29749
+ readyWorker = void 0;
29750
+ failPending({ status: "unavailable", reason: "the scan worker is closed" });
29751
+ if (live) kill(live);
29752
+ await Promise.all([...terminating]);
29753
+ }
29754
+ };
29755
+ }
29756
+
29757
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
29758
+ var PASS_BUDGET_MS = 2e3;
29759
+ var UNQUARANTINE_HINT = "clear it with `aka detections unquarantine`";
29760
+ function ruleProbeKey(rule) {
29761
+ if (rule.matcher.type !== "regex") return void 0;
29762
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
29763
+ }
29764
+ function warn(rule, verb, detail, recoverable) {
29765
+ const hint = recoverable ? ` (${UNQUARANTINE_HINT})` : "";
29766
+ process.stderr.write(`[aka] ${verb} rule "${rule.id}": ${detail}${hint}
29767
+ `);
29768
+ }
29769
+ function warnQuarantined(rule, worstMs, cached2) {
29770
+ warn(
29771
+ rule,
29772
+ "quarantined",
29773
+ 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.",
29774
+ cached2
29775
+ );
29776
+ }
29777
+ function warnUnmeasured(rule) {
29778
+ warn(
29779
+ rule,
29780
+ "skipped",
29781
+ "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.",
29782
+ false
29783
+ );
29784
+ }
29785
+ function warnUnmeasurable(reason, count) {
29786
+ process.stderr.write(
29787
+ `[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.
29788
+ `
29789
+ );
29790
+ }
29791
+ async function quarantineRule(gateway, rule, worstMs, detail) {
29792
+ const key = ruleProbeKey(rule);
29793
+ let cached2 = false;
29794
+ if (key !== void 0) {
29795
+ try {
29796
+ await gateway.setRuleProbeVerdict(key, "quarantined", worstMs);
29797
+ cached2 = true;
29798
+ } catch {
29799
+ }
29800
+ }
29801
+ warn(rule, "quarantined", detail, cached2);
29802
+ }
29803
+ async function filterUnsafeRules(rules, gateway, opts) {
29804
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
29805
+ const prober = opts?.prober;
29806
+ const passStart = performance.now();
29807
+ const safe = [];
29808
+ const unmeasurable = /* @__PURE__ */ new Map();
29809
+ try {
29810
+ for (const rule of rules) {
29811
+ const key = ruleProbeKey(rule);
29812
+ if (key === void 0) {
29813
+ safe.push(rule);
29814
+ continue;
29815
+ }
29816
+ let cached2;
29817
+ try {
29818
+ cached2 = await gateway.getRuleProbeVerdict(key);
29819
+ } catch {
29820
+ cached2 = void 0;
29821
+ }
29822
+ if (cached2) {
29823
+ if (cached2.verdict === "safe") safe.push(rule);
29824
+ else warnQuarantined(rule, cached2.worstProbeMs, true);
29825
+ continue;
29826
+ }
29827
+ if (performance.now() - passStart >= passBudgetMs) {
29828
+ warnUnmeasured(rule);
29829
+ continue;
29830
+ }
29831
+ let isSafe;
29832
+ let worstMs;
29833
+ if (prober) {
29834
+ const outcome = await prober.probe(rule);
29835
+ if (outcome.status === "unavailable") {
29836
+ unmeasurable.set(outcome.reason, (unmeasurable.get(outcome.reason) ?? 0) + 1);
29837
+ continue;
29838
+ }
29839
+ isSafe = outcome.status === "ok" ? outcome.safe : false;
29840
+ worstMs = outcome.status === "ok" ? outcome.worstMs : outcome.elapsedMs;
29841
+ } else {
29842
+ try {
29843
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
29844
+ } catch {
29845
+ isSafe = false;
29846
+ worstMs = Number.POSITIVE_INFINITY;
29847
+ }
29848
+ }
29849
+ let persisted = false;
29850
+ try {
29851
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
29852
+ persisted = true;
29853
+ } catch {
29854
+ }
29855
+ if (isSafe) safe.push(rule);
29856
+ else warnQuarantined(rule, worstMs, persisted);
29857
+ }
29858
+ } finally {
29859
+ for (const [reason, count] of unmeasurable) warnUnmeasurable(reason, count);
29860
+ }
29861
+ return safe;
29862
+ }
29863
+
29864
+ // ../../packages/plugin-sdk/src/guarded-scan.ts
29865
+ var DEFAULT_DEGRADE_SCOPE = "the rest of this process";
29866
+ function warnDegraded(scope, dropped, detail) {
29867
+ process.stderr.write(
29868
+ `[aka] isolated scanning is off for ${scope}: ${detail}. ${String(dropped)} pulled/custom-pack rule(s) are excluded; the built-in packs still run.
29869
+ `
29870
+ );
29871
+ }
29872
+ function createGuardedScanner(partition, gateway, opts) {
29873
+ const degradeScope = opts?.degradeScope ?? DEFAULT_DEGRADE_SCOPE;
29874
+ const verified = partition.verified;
29875
+ let unverified = partition.unverified;
29876
+ let isolated = unverified.length > 0 ? createIsolatedScanner({ verified, unverified }, opts) : void 0;
29877
+ let retired = false;
29878
+ function inProcess(text, context) {
29879
+ return scan(text, verified, context);
29880
+ }
29881
+ async function retire() {
29882
+ const live = isolated;
29883
+ isolated = void 0;
29884
+ unverified = [];
29885
+ if (live) await live.close();
29886
+ }
29887
+ async function degrade() {
29888
+ retired = true;
29889
+ await retire();
29890
+ }
29891
+ async function attempt(active, text, context, attribute) {
29892
+ try {
29893
+ return await active.scan(text, context, { attribute });
29894
+ } catch (error51) {
29895
+ return {
29896
+ status: "unavailable",
29897
+ reason: error51 instanceof Error ? error51.message : "the scan worker failed unexpectedly"
29898
+ };
29899
+ }
29900
+ }
29901
+ async function guardedScan(text, context) {
29902
+ const active = isolated;
29903
+ if (!active) return inProcess(text, context);
29904
+ let outcome = await attempt(active, text, context, false);
29905
+ if (outcome.status === "ok") return outcome.findings;
29906
+ if (outcome.status === "timeout") outcome = await attempt(active, text, context, true);
29907
+ const dropped = unverified.length;
29908
+ if (outcome.status === "ok") {
29909
+ warnDegraded(
29910
+ degradeScope,
29911
+ dropped,
29912
+ "a scan overran its bound once and no rule could be held responsible"
29913
+ );
29914
+ const findings = outcome.findings;
29915
+ await degrade();
29916
+ return findings;
29917
+ }
29918
+ if (outcome.status === "timeout") {
29919
+ const culprit = outcome.culpritIndex === void 0 ? void 0 : unverified[outcome.culpritIndex];
29920
+ if (culprit) {
29921
+ await quarantineRule(
29922
+ gateway,
29923
+ culprit,
29924
+ outcome.elapsedMs,
29925
+ `it did not finish within the ${outcome.elapsedMs.toFixed(0)}ms isolated-scan bound and was terminated; excluded from every later scan.`
29926
+ );
29927
+ }
29928
+ warnDegraded(
29929
+ degradeScope,
29930
+ dropped,
29931
+ 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`
29932
+ );
29933
+ } else {
29934
+ warnDegraded(degradeScope, dropped, outcome.reason);
29935
+ }
29936
+ await degrade();
29937
+ return inProcess(text, context);
29938
+ }
29939
+ return {
29940
+ scan: guardedScan,
29941
+ degraded: () => retired,
29942
+ async close() {
29943
+ await retire();
28429
29944
  }
28430
29945
  };
28431
29946
  }
28432
29947
 
28433
29948
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
28434
- import { arch, hostname as hostname3, platform, release } from "os";
29949
+ import { arch, hostname as hostname4, platform, release } from "os";
28435
29950
 
28436
29951
  // ../../packages/plugin-sdk/src/nudge.ts
28437
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28438
- import { join as join10 } from "path";
29952
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
29953
+ import { join as join11 } from "path";
28439
29954
 
28440
29955
  // ../../packages/plugin-sdk/src/paths.ts
28441
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
28442
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
29956
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
29957
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
28443
29958
  function toPosix(path) {
28444
29959
  return path.split(sep3).join("/");
28445
29960
  }
@@ -28449,7 +29964,7 @@ function findProjectRoot(startDir, recognizeMarker) {
28449
29964
  let root = null;
28450
29965
  for (let level = 0; level < MAX_PROJECT_ROOT_LEVELS; level += 1) {
28451
29966
  if (directoryHasMarker(dir, recognizeMarker)) root = dir;
28452
- const parent = dirname2(dir);
29967
+ const parent = dirname3(dir);
28453
29968
  if (parent === dir) break;
28454
29969
  dir = parent;
28455
29970
  }
@@ -28457,7 +29972,7 @@ function findProjectRoot(startDir, recognizeMarker) {
28457
29972
  }
28458
29973
  function directoryHasMarker(dir, recognizeMarker) {
28459
29974
  try {
28460
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
29975
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
28461
29976
  if (entry.isFile() && recognizeMarker(entry.name) != null) return true;
28462
29977
  }
28463
29978
  } catch {
@@ -28468,69 +29983,42 @@ function directoryHasMarker(dir, recognizeMarker) {
28468
29983
  function resolveNonGitProject(startDir, recognizeMarker) {
28469
29984
  const projectRoot = findProjectRoot(startDir, recognizeMarker);
28470
29985
  const realRoot = realpathSync2(projectRoot);
28471
- return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename3(realRoot) };
29986
+ return { root: projectRoot, projectKey: `path:${realRoot}`, project: basename4(realRoot) };
28472
29987
  }
28473
29988
 
28474
29989
  // ../../packages/plugin-sdk/src/project-files.ts
28475
29990
  var import_ignore = __toESM(require_ignore(), 1);
28476
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28477
- import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
29991
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
29992
+ import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
28478
29993
 
28479
- // ../../packages/plugin-sdk/src/rule-quarantine.ts
28480
- var PASS_BUDGET_MS = 2e3;
28481
- function ruleProbeKey(rule) {
28482
- if (rule.matcher.type !== "regex") return void 0;
28483
- return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
28484
- }
28485
- function warnQuarantined(rule, worstMs) {
28486
- const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
28487
- process.stderr.write(
28488
- `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
28489
- `
28490
- );
28491
- }
28492
- async function filterUnsafeRules(rules, gateway, opts) {
28493
- const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
28494
- const passStart = performance.now();
28495
- const safe = [];
28496
- for (const rule of rules) {
28497
- const key = ruleProbeKey(rule);
28498
- if (key === void 0) {
28499
- safe.push(rule);
28500
- continue;
28501
- }
28502
- let cached2;
28503
- try {
28504
- cached2 = await gateway.getRuleProbeVerdict(key);
28505
- } catch {
28506
- cached2 = void 0;
28507
- }
28508
- if (cached2) {
28509
- if (cached2.verdict === "safe") safe.push(rule);
28510
- else warnQuarantined(rule, cached2.worstProbeMs);
28511
- continue;
28512
- }
28513
- if (performance.now() - passStart >= passBudgetMs) {
28514
- warnQuarantined(rule, void 0);
28515
- continue;
28516
- }
28517
- let isSafe;
28518
- let worstMs;
28519
- try {
28520
- ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
28521
- } catch {
28522
- isSafe = false;
28523
- worstMs = Number.POSITIVE_INFINITY;
28524
- }
28525
- await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
28526
- if (isSafe) safe.push(rule);
28527
- else warnQuarantined(rule, worstMs);
28528
- }
28529
- return safe;
28530
- }
29994
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
29995
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
29996
+ if (typeof v === "string" && v.trim() === "") return void 0;
29997
+ return v;
29998
+ }, external_exports.string().optional()).catch(void 0);
29999
+ var optionalFlag = external_exports.preprocess((v) => {
30000
+ if (typeof v !== "string") return false;
30001
+ const normalized = v.trim().toLowerCase();
30002
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
30003
+ }, external_exports.boolean()).catch(false);
30004
+ var antigravityProviderEnvShape = {
30005
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
30006
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
30007
+ };
30008
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
30009
+
30010
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
30011
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
30012
+ if (typeof v === "string" && v.trim() === "") return void 0;
30013
+ return v;
30014
+ }, external_exports.string().optional()).catch(void 0);
30015
+ var codexProviderEnvShape = {
30016
+ OPENAI_BASE_URL: optionalBaseUrl3
30017
+ };
30018
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
28531
30019
 
28532
30020
  // ../../packages/plugin-sdk/src/runtime.ts
28533
- import { randomUUID as randomUUID12 } from "crypto";
30021
+ import { randomUUID as randomUUID14 } from "crypto";
28534
30022
  var ENFORCEMENT_CEILING_ENABLED = false;
28535
30023
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
28536
30024
  function entryIsActive(entry, now) {
@@ -28555,6 +30043,7 @@ function createPluginRuntime(gateway, settings, opts) {
28555
30043
  const dataDir2 = opts?.dataDir;
28556
30044
  let policies = [];
28557
30045
  let rules = [];
30046
+ let scanner;
28558
30047
  let bundleExceptions = [];
28559
30048
  let initialized = false;
28560
30049
  const ruleActionIndex = /* @__PURE__ */ new Map();
@@ -28580,8 +30069,24 @@ function createPluginRuntime(gateway, settings, opts) {
28580
30069
  return key !== void 0 && bundledProbeKeys.has(key);
28581
30070
  });
28582
30071
  const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
28583
- const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
28584
- rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
30072
+ let prober;
30073
+ const gated = await filterUnsafeRules(needsGate, gateway, {
30074
+ prober: {
30075
+ probe: (rule) => {
30076
+ prober ??= createIsolatedScanner({ verified: [], unverified: [] }, opts?.scanIsolation);
30077
+ return prober.probe(rule);
30078
+ }
30079
+ }
30080
+ });
30081
+ await prober?.close();
30082
+ const verified = bundle.rulesComplete ? [...ciVerified] : [...getLoadedRules(), ...ciVerified];
30083
+ const unverified = [];
30084
+ for (const rule of gated) {
30085
+ if (rule.matcher.type === "regex") unverified.push(rule);
30086
+ else verified.push(rule);
30087
+ }
30088
+ rules = [...verified, ...unverified];
30089
+ scanner = createGuardedScanner({ verified, unverified }, gateway, opts?.scanIsolation);
28585
30090
  bundleExceptions = bundle.exceptions ?? [];
28586
30091
  initialized = true;
28587
30092
  }
@@ -28721,7 +30226,7 @@ function createPluginRuntime(gateway, settings, opts) {
28721
30226
  const pair = `${finding.ruleId}:${fp}`;
28722
30227
  if (seen.has(pair)) continue;
28723
30228
  seen.add(pair);
28724
- const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
30229
+ const reference = randomUUID14().replaceAll("-", "").slice(0, 6);
28725
30230
  const maskedValue = maskMatch(finding.rawMatch);
28726
30231
  try {
28727
30232
  await gateway.recordBlockedDetection({
@@ -28745,8 +30250,10 @@ function createPluginRuntime(gateway, settings, opts) {
28745
30250
  async function evaluate2(text, context, ctx) {
28746
30251
  try {
28747
30252
  await ensureInitialized();
30253
+ if (!scanner) throw new Error("the runtime initialized without a scanner");
28748
30254
  const shielded = shieldPointers(text);
28749
- const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
30255
+ const matched = await scanner.scan(shielded.text, context);
30256
+ const findings = dropShieldedFindings(matched, shielded.spans);
28750
30257
  const fpCache = /* @__PURE__ */ new Map();
28751
30258
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
28752
30259
  const decision = decide(findings, text, excepted);
@@ -28803,7 +30310,7 @@ function createPluginRuntime(gateway, settings, opts) {
28803
30310
  valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
28804
30311
  }) : void 0;
28805
30312
  return {
28806
- id: randomUUID12(),
30313
+ id: randomUUID14(),
28807
30314
  eventId: event.id,
28808
30315
  ruleId: match.ruleId,
28809
30316
  category: match.category,
@@ -28829,25 +30336,32 @@ function createPluginRuntime(gateway, settings, opts) {
28829
30336
  const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
28830
30337
  return contentHashOf(JSON.stringify(sorted));
28831
30338
  } catch {
28832
- return `unresolved-${randomUUID12()}`;
30339
+ return `unresolved-${randomUUID14()}`;
28833
30340
  }
28834
30341
  }
30342
+ function scanIsolationDegraded() {
30343
+ return scanner?.degraded() ?? false;
30344
+ }
28835
30345
  async function close() {
30346
+ try {
30347
+ await scanner?.close();
30348
+ } catch {
30349
+ }
28836
30350
  await gateway.close();
28837
30351
  }
28838
- return { processText, capture, rulesetFingerprint, close };
30352
+ return { processText, capture, rulesetFingerprint, scanIsolationDegraded, close };
28839
30353
  }
28840
30354
 
28841
30355
  // ../../packages/plugin-sdk/src/suppressions.ts
28842
30356
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28843
30357
 
28844
30358
  // ../../packages/plugin-sdk/src/throttle.ts
28845
- import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28846
- import { join as join12 } from "path";
30359
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
30360
+ import { join as join13 } from "path";
28847
30361
 
28848
30362
  // ../../packages/scanner/src/discover.ts
28849
- import { readdirSync as readdirSync4 } from "fs";
28850
- import { join as join13 } from "path";
30363
+ import { readdirSync as readdirSync5 } from "fs";
30364
+ import { join as join14 } from "path";
28851
30365
 
28852
30366
  // ../../packages/scanner/src/constants.ts
28853
30367
  var COMMON_SKIP_DIRS = ["node_modules", "__pycache__", ".venv", "venv", ".cache"];
@@ -28876,7 +30390,7 @@ function discoverGitRepos(opts) {
28876
30390
  if (depth > maxDepth || excludePaths.has(dir)) return;
28877
30391
  let entries;
28878
30392
  try {
28879
- entries = readdirSync4(dir, { withFileTypes: true, encoding: "utf8" });
30393
+ entries = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
28880
30394
  } catch {
28881
30395
  return;
28882
30396
  }
@@ -28892,7 +30406,7 @@ function discoverGitRepos(opts) {
28892
30406
  if (!entry.isDirectory()) continue;
28893
30407
  if (DISCOVER_SKIP.has(entry.name)) continue;
28894
30408
  if (entry.name.startsWith(".")) continue;
28895
- visit(join13(dir, entry.name), depth + 1);
30409
+ visit(join14(dir, entry.name), depth + 1);
28896
30410
  }
28897
30411
  }
28898
30412
  for (const root of searchRoots) {
@@ -28902,8 +30416,8 @@ function discoverGitRepos(opts) {
28902
30416
  }
28903
30417
 
28904
30418
  // ../../packages/scanner/src/render.ts
28905
- import { basename as basename5, relative as relative2 } from "path";
28906
- var SEVERITY_ORDER2 = ["critical", "high", "medium", "low"];
30419
+ import { basename as basename6, relative as relative2 } from "path";
30420
+ var SEVERITY_ORDER3 = ["critical", "high", "medium", "low"];
28907
30421
  var SEVERITY_GLYPH = {
28908
30422
  critical: "\u2588",
28909
30423
  high: "\u2593",
@@ -28934,7 +30448,7 @@ function findingsLabel(total, gitignored) {
28934
30448
  return `${String(total)} (${String(gitignored)} in .gitignore'd files \u2014 informational)`;
28935
30449
  }
28936
30450
  function severitySection(bySeverity) {
28937
- const rows = SEVERITY_ORDER2.filter((s) => (bySeverity[s] ?? 0) > 0).map((s) => [
30451
+ const rows = SEVERITY_ORDER3.filter((s) => (bySeverity[s] ?? 0) > 0).map((s) => [
28938
30452
  `${SEVERITY_GLYPH[s] ?? ""} ${s}`,
28939
30453
  String(bySeverity[s])
28940
30454
  ]);
@@ -28982,7 +30496,7 @@ function renderMultiRepoSummary(summary, opts = {}) {
28982
30496
  "\n"
28983
30497
  );
28984
30498
  }
28985
- const repoRows = summary.repos.filter((r) => r.summary.scanned > 0 || r.summary.findings > 0).map((r) => [basename5(r.rootDir), String(r.summary.scanned), String(r.summary.findings)]);
30499
+ const repoRows = summary.repos.filter((r) => r.summary.scanned > 0 || r.summary.findings > 0).map((r) => [basename6(r.rootDir), String(r.summary.scanned), String(r.summary.findings)]);
28986
30500
  const repoSection = repoRows.length > 0 ? ["", indent(table(["REPO", "SCANNED", "FINDINGS"], repoRows))].join("\n") : "";
28987
30501
  return [
28988
30502
  "\u2713 Multi-repo scan complete",
@@ -28995,11 +30509,11 @@ function renderMultiRepoSummary(summary, opts = {}) {
28995
30509
  }
28996
30510
 
28997
30511
  // ../../packages/scanner/src/scan.ts
28998
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
30512
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
28999
30513
  import { extname as extname2, isAbsolute as isAbsolute2, relative as relative4 } from "path";
29000
30514
 
29001
30515
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
29002
- import { randomUUID as randomUUID13 } from "crypto";
30516
+ import { randomUUID as randomUUID15 } from "crypto";
29003
30517
 
29004
30518
  // ../../packages/plugin-runtime/src/recorder.ts
29005
30519
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -29161,7 +30675,7 @@ var StandaloneDataGateway = class {
29161
30675
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
29162
30676
  const installed = this.installedScanRules();
29163
30677
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
29164
- id: randomUUID13(),
30678
+ id: randomUUID15(),
29165
30679
  scope: "global",
29166
30680
  target: { ruleId },
29167
30681
  action,
@@ -29314,16 +30828,16 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
29314
30828
  }
29315
30829
 
29316
30830
  // ../../packages/plugin-runtime/src/handle-session-start.ts
29317
- import { randomUUID as randomUUID14 } from "crypto";
30831
+ import { randomUUID as randomUUID16 } from "crypto";
29318
30832
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
29319
30833
 
29320
30834
  // ../../packages/scanner/src/manifests.ts
29321
- import { statSync as statSync6 } from "fs";
30835
+ import { statSync as statSync8 } from "fs";
29322
30836
 
29323
30837
  // ../../packages/scanner/src/walk.ts
29324
30838
  var import_ignore2 = __toESM(require_ignore(), 1);
29325
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
29326
- import { extname, join as join14, relative as relative3, sep as sep5 } from "path";
30839
+ import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
30840
+ import { extname, join as join15, relative as relative3, sep as sep5 } from "path";
29327
30841
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
29328
30842
  ".ts",
29329
30843
  ".tsx",
@@ -29355,7 +30869,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
29355
30869
  var DEFAULT_MAX_BYTES = 512 * 1024;
29356
30870
  function readIgnoreLayer(dir, filename) {
29357
30871
  try {
29358
- const content = readFileSync8(join14(dir, filename), "utf8");
30872
+ const content = readFileSync9(join15(dir, filename), "utf8");
29359
30873
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
29360
30874
  } catch {
29361
30875
  return void 0;
@@ -29377,7 +30891,7 @@ function* walkTree(rootDir, opts = {}) {
29377
30891
  function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
29378
30892
  let dirents;
29379
30893
  try {
29380
- dirents = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
30894
+ dirents = readdirSync6(dir, { withFileTypes: true, encoding: "utf8" });
29381
30895
  } catch {
29382
30896
  return;
29383
30897
  }
@@ -29387,7 +30901,7 @@ function* walkTree(rootDir, opts = {}) {
29387
30901
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
29388
30902
  for (const entry of dirents) {
29389
30903
  const name = entry.name;
29390
- const fullPath = join14(dir, name);
30904
+ const fullPath = join15(dir, name);
29391
30905
  if (entry.isDirectory()) {
29392
30906
  const skipState = evaluate(dirSkipLayers, fullPath, true);
29393
30907
  if (skipState !== "unignored" && (SKIP_DIRS.has(name) || skipState === "ignored")) {
@@ -29421,7 +30935,7 @@ function* walkSourceFiles(opts = {}) {
29421
30935
  let size;
29422
30936
  let mtime;
29423
30937
  try {
29424
- const st = statSync5(file2.path);
30938
+ const st = statSync7(file2.path);
29425
30939
  size = st.size;
29426
30940
  mtime = st.mtime;
29427
30941
  } catch {
@@ -29441,7 +30955,7 @@ function* walkSourceFiles(opts = {}) {
29441
30955
  if (opts.shouldRead && !opts.shouldRead(meta3)) continue;
29442
30956
  let content;
29443
30957
  try {
29444
- content = readFileSync8(file2.path, "utf8");
30958
+ content = readFileSync9(file2.path, "utf8");
29445
30959
  } catch {
29446
30960
  continue;
29447
30961
  }
@@ -29463,7 +30977,7 @@ function collectManifests(rootDir, maxFileSizeBytes = MAX_MANIFEST_BYTES) {
29463
30977
  const kind = manifestKindOf(file2.name);
29464
30978
  if (kind === null) continue;
29465
30979
  try {
29466
- const st = statSync6(file2.path);
30980
+ const st = statSync8(file2.path);
29467
30981
  if (st.size > maxFileSizeBytes) continue;
29468
30982
  found.push({ path: file2.path, kind, mtime: st.mtime.toISOString(), size: st.size });
29469
30983
  } catch {
@@ -29561,7 +31075,7 @@ function isUnderRoot(path, rootDir) {
29561
31075
  async function sweepDeletedFiles(gateway, rootDir, previous) {
29562
31076
  const deleted = [];
29563
31077
  for (const path of previous.keys()) {
29564
- if (!isUnderRoot(path, rootDir) || existsSync7(path)) continue;
31078
+ if (!isUnderRoot(path, rootDir) || existsSync9(path)) continue;
29565
31079
  deleted.push(path);
29566
31080
  await resolveRemovedFindings(gateway, path, [], { deleted: true });
29567
31081
  }
@@ -29656,6 +31170,9 @@ async function scanDir(runtime, gateway, config2, seen, ledger, rootDir, opts) {
29656
31170
  if (committed === null) {
29657
31171
  return { rootDir, scanned, skipped, findings, gitignoredFindings, byRule, bySeverity };
29658
31172
  }
31173
+ if (runtime.scanIsolationDegraded()) {
31174
+ return { rootDir, scanned, skipped, findings, gitignoredFindings, byRule, bySeverity };
31175
+ }
29659
31176
  await gateway.recordScanned(ledgerable(updates, egress, committed));
29660
31177
  return { rootDir, scanned, skipped, findings, gitignoredFindings, byRule, bySeverity };
29661
31178
  }
@@ -29665,7 +31182,7 @@ function scanManifests(egress, ledger, updates, rootDir) {
29665
31182
  if (prev?.mtime === manifest.mtime) continue;
29666
31183
  let content;
29667
31184
  try {
29668
- content = readFileSync9(manifest.path, "utf8");
31185
+ content = readFileSync10(manifest.path, "utf8");
29669
31186
  } catch {
29670
31187
  continue;
29671
31188
  }