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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -492,16 +492,15 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // src/backfill.ts
495
- import { fileURLToPath } from "url";
495
+ import { fileURLToPath as fileURLToPath2 } from "url";
496
496
 
497
497
  // ../../packages/plugin-sdk/src/config.ts
498
- import { existsSync as existsSync4 } from "fs";
499
- import { join as join7 } from "path";
498
+ import { existsSync as existsSync5 } from "fs";
499
+ import { join as join8 } from "path";
500
500
 
501
501
  // ../../packages/persistence/src/database.ts
502
- import { randomUUID as randomUUID9 } from "crypto";
503
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
504
- import { join, sep } from "path";
502
+ import { randomUUID as randomUUID10 } from "crypto";
503
+ import { join as join2, sep } from "path";
505
504
  import { DatabaseSync } from "node:sqlite";
506
505
 
507
506
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -581,6 +580,14 @@ var SQLITE_MIGRATIONS = [
581
580
  {
582
581
  tag: "0018_serious_tana_nile",
583
582
  sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
583
+ },
584
+ {
585
+ tag: "0019_audit_started_at_index",
586
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
587
+ },
588
+ {
589
+ tag: "0020_secret_vault_pagination_indexes",
590
+ 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');"
584
591
  }
585
592
  ];
586
593
 
@@ -15318,7 +15325,17 @@ var Finding = external_exports.object({
15318
15325
  }).meta({ id: "Finding" });
15319
15326
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15320
15327
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15321
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15328
+ var FindingProvider = external_exports.enum([
15329
+ "claudecode",
15330
+ "claudedesktop",
15331
+ "cursor",
15332
+ "copilot",
15333
+ "chatgpt",
15334
+ "claudeai",
15335
+ "codex",
15336
+ "antigravity",
15337
+ "api"
15338
+ ]).meta({ id: "FindingProvider" });
15322
15339
  var FindingCategory = external_exports.enum([
15323
15340
  "secret",
15324
15341
  "pii",
@@ -15372,7 +15389,16 @@ var FindingInstance = external_exports.object({
15372
15389
  confidence: external_exports.number().min(0).max(1),
15373
15390
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15374
15391
  // that predate the resolution feature stay valid.
15375
- status: FindingStatus.optional()
15392
+ status: FindingStatus.optional(),
15393
+ // The audit event this finding was captured from. Optional so callers that
15394
+ // do not project it stay valid. An at-rest finding is content-addressed by
15395
+ // finding_key and its row is upserted on re-detection, so this names the
15396
+ // MOST RECENT detection event, not the first.
15397
+ eventId: external_exports.string().optional(),
15398
+ // The session that event belongs to, when it has one — the seam a
15399
+ // per-instance "view session" link needs. Absent for events captured
15400
+ // outside a session.
15401
+ sessionId: external_exports.string().optional()
15376
15402
  }).meta({ id: "FindingInstance" });
15377
15403
  var FindingGroup = external_exports.object({
15378
15404
  id: external_exports.string(),
@@ -15416,7 +15442,11 @@ var FindingFacets = external_exports.object({
15416
15442
  // for every instance, so every group lands in a bucket; a status-less
15417
15443
  // group (possible only for callers whose rows carry no statuses) is
15418
15444
  // counted under no value.
15419
- status: external_exports.array(FindingFacetItem)
15445
+ status: external_exports.array(FindingFacetItem),
15446
+ // Host tool (attributes.tool_name). Present only on the instance-level
15447
+ // reads, which can filter by it; the grouped read omits the dimension
15448
+ // because a group spans tools.
15449
+ tool: external_exports.array(FindingFacetItem).optional()
15420
15450
  }).meta({ id: "FindingFacets" });
15421
15451
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15422
15452
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15434,6 +15464,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15434
15464
  // Scope to findings whose event carries this session id (the Activity page's
15435
15465
  // session → findings drilldown). Findings without a session never match.
15436
15466
  sessionId: external_exports.string().optional(),
15467
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15468
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15469
+ // means all time — this list has no default window.
15470
+ from: external_exports.iso.datetime().optional(),
15471
+ // A group or instance id that must appear in the page even when the cursor
15472
+ // has already advanced past its sort position. This is what keeps the
15473
+ // Findings page's one-shot ?finding= deep link resolving once the list
15474
+ // paginates: the target group is appended out of sort order rather than
15475
+ // scanning forward for it. Never affects totals, facets or the cursor.
15476
+ includeId: external_exports.string().optional(),
15437
15477
  groupBy: external_exports.literal("type").optional(),
15438
15478
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15439
15479
  cursor: external_exports.string().optional()
@@ -15478,15 +15518,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15478
15518
  detection: FindingDetectionRef,
15479
15519
  policy: FindingPolicyRef
15480
15520
  }).meta({ id: "FindingInstanceDetail" });
15521
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15522
+ var ListFindingInstancesQuery = external_exports.object({
15523
+ severity: external_exports.array(Severity).optional(),
15524
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15525
+ subtype: external_exports.array(external_exports.string()).optional(),
15526
+ provider: external_exports.array(FindingProvider).optional(),
15527
+ action: external_exports.array(FindingAction).optional(),
15528
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15529
+ // the grouped query's group-level fold.
15530
+ status: external_exports.array(FindingStatus).optional(),
15531
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15532
+ // where the free-text `q` can only match the rendered "via Bash" label.
15533
+ tool: external_exports.array(external_exports.string()).optional(),
15534
+ // Exact repository / file-path matches, for the drill-down out of the
15535
+ // locations view. A row whose event carries no repo/file matches neither.
15536
+ repo: external_exports.string().optional(),
15537
+ file: external_exports.string().optional(),
15538
+ q: external_exports.string().optional(),
15539
+ sessionId: external_exports.string().optional(),
15540
+ from: external_exports.iso.datetime().optional(),
15541
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15542
+ cursor: external_exports.string().optional()
15543
+ });
15544
+ var ListFindingInstancesResponse = external_exports.object({
15545
+ // Instances matching the filters across the whole scope, not just this
15546
+ // page — cursor-independent, like the grouped list's totals.
15547
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15548
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15549
+ // dimension still excludes its own filter.
15550
+ facets: FindingFacets,
15551
+ items: external_exports.array(FindingInstanceDetail),
15552
+ nextCursor: external_exports.string().nullable()
15553
+ }).meta({ id: "ListFindingInstancesResponse" });
15554
+ var FindingLocationFile = external_exports.object({
15555
+ // Empty when the instances carried no file path (a prompt or a tool call
15556
+ // with no file attribution).
15557
+ file: external_exports.string(),
15558
+ instanceCount: external_exports.number().int().nonnegative(),
15559
+ maxSeverity: Severity,
15560
+ latestDetectedAt: external_exports.iso.datetime(),
15561
+ // Folded from the instances' derived statuses with the same
15562
+ // open-dominates precedence a group uses.
15563
+ status: FindingStatus.optional(),
15564
+ // Distinct rules seen at this location, capped — the row shows them as
15565
+ // chips, and the count is what conveys scale.
15566
+ ruleIds: external_exports.array(external_exports.string())
15567
+ }).meta({ id: "FindingLocationFile" });
15568
+ var FindingLocationRepo = external_exports.object({
15569
+ /** Empty when the instances carried no repo attribute. */
15570
+ repo: external_exports.string(),
15571
+ instanceCount: external_exports.number().int().nonnegative(),
15572
+ maxSeverity: Severity,
15573
+ latestDetectedAt: external_exports.iso.datetime(),
15574
+ status: FindingStatus.optional(),
15575
+ files: external_exports.array(FindingLocationFile)
15576
+ }).meta({ id: "FindingLocationRepo" });
15577
+ var ListFindingLocationsQuery = external_exports.object({
15578
+ severity: external_exports.array(Severity).optional(),
15579
+ subtype: external_exports.array(external_exports.string()).optional(),
15580
+ provider: external_exports.array(FindingProvider).optional(),
15581
+ action: external_exports.array(FindingAction).optional(),
15582
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15583
+ // instances that match, and folds its status from those.
15584
+ status: external_exports.array(FindingStatus).optional(),
15585
+ tool: external_exports.array(external_exports.string()).optional(),
15586
+ q: external_exports.string().optional(),
15587
+ sessionId: external_exports.string().optional(),
15588
+ from: external_exports.iso.datetime().optional(),
15589
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15590
+ });
15591
+ var ListFindingLocationsResponse = external_exports.object({
15592
+ totals: external_exports.object({
15593
+ findings: external_exports.number().int().nonnegative(),
15594
+ repos: external_exports.number().int().nonnegative(),
15595
+ files: external_exports.number().int().nonnegative()
15596
+ }),
15597
+ /** Sorted by max severity, then most recent. */
15598
+ items: external_exports.array(FindingLocationRepo),
15599
+ /** Whether `limit` truncated the repo list. */
15600
+ hasMore: external_exports.boolean()
15601
+ }).meta({ id: "ListFindingLocationsResponse" });
15481
15602
 
15482
15603
  // ../../packages/schema/src/zod/harness-map.ts
15483
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15604
+ var Harness = external_exports.enum([
15605
+ "claudecode",
15606
+ "cursor",
15607
+ "copilot",
15608
+ "codex",
15609
+ "antigravity",
15610
+ "windsurf",
15611
+ "claudedesktop",
15612
+ "chatgpt",
15613
+ "claudeai",
15614
+ "api"
15615
+ ]).meta({ id: "Harness" });
15484
15616
  var TOOL_TO_HARNESS = {
15485
15617
  "claude-code": "claudecode",
15486
15618
  "claude-desktop": "claudedesktop",
15487
15619
  "github-copilot": "copilot",
15488
15620
  cursor: "cursor",
15489
- chatgpt: "chatgpt"
15621
+ chatgpt: "chatgpt",
15622
+ codex: "codex",
15623
+ antigravity: "antigravity",
15624
+ "claude-ai": "claudeai"
15490
15625
  };
15491
15626
  function harnessFromTool(tool) {
15492
15627
  return TOOL_TO_HARNESS[tool] ?? tool;
@@ -15947,7 +16082,18 @@ var ActivityOverviewResponse = external_exports.object({
15947
16082
  // ../../packages/schema/src/zod/event.ts
15948
16083
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15949
16084
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15950
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16085
+ var SourceTool = external_exports.enum([
16086
+ "claude-code",
16087
+ "claude-desktop",
16088
+ "cursor",
16089
+ "chatgpt",
16090
+ "claude-ai",
16091
+ "github-copilot",
16092
+ "codex",
16093
+ "antigravity",
16094
+ "cli",
16095
+ "unknown"
16096
+ ]).meta({ id: "SourceTool" });
15951
16097
  var EventMetadata = external_exports.object({
15952
16098
  sessionId: external_exports.string().optional(),
15953
16099
  repo: external_exports.string().optional(),
@@ -16018,7 +16164,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
16018
16164
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
16019
16165
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
16020
16166
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
16021
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16167
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
16022
16168
  var AccessCounts = external_exports.object({
16023
16169
  open: external_exports.number().int().nonnegative(),
16024
16170
  approved: external_exports.number().int().nonnegative(),
@@ -16287,6 +16433,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16287
16433
  useCount: true,
16288
16434
  conditions: true
16289
16435
  });
16436
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16290
16437
 
16291
16438
  // ../../packages/schema/src/zod/rule.ts
16292
16439
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17097,6 +17244,35 @@ var EgressWriteSummary = external_exports.object({
17097
17244
  droppedFiles: external_exports.array(external_exports.string()).default([])
17098
17245
  }).meta({ id: "EgressWriteSummary" });
17099
17246
 
17247
+ // ../../packages/schema/src/zod/exception-action.ts
17248
+ var confirmation = external_exports.string().optional();
17249
+ var ApproveBlockedInput = external_exports.object({
17250
+ reference: external_exports.string(),
17251
+ scope: external_exports.string(),
17252
+ reason: external_exports.string(),
17253
+ confirmation
17254
+ });
17255
+ var AddExceptionInput = external_exports.object({
17256
+ ruleId: external_exports.string(),
17257
+ value: external_exports.string(),
17258
+ scope: external_exports.string(),
17259
+ reason: external_exports.string(),
17260
+ confirmation
17261
+ });
17262
+ var GrantRevealInput = external_exports.object({
17263
+ pointer: external_exports.string(),
17264
+ scope: external_exports.string(),
17265
+ justification: external_exports.string(),
17266
+ confirmation
17267
+ });
17268
+ var RevokeExceptionInput = external_exports.object({
17269
+ id: external_exports.string(),
17270
+ reason: external_exports.string()
17271
+ });
17272
+ var RotateKeyInput = external_exports.object({
17273
+ confirmation: external_exports.string()
17274
+ });
17275
+
17100
17276
  // ../../packages/schema/src/zod/findings-group-build.ts
17101
17277
  function toApiAction(dbVal) {
17102
17278
  const map2 = {
@@ -17152,6 +17328,8 @@ function buildFindingGroups(rows, opts = {}) {
17152
17328
  repo: r.repo,
17153
17329
  file: r.file,
17154
17330
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17331
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17332
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17155
17333
  action: toApiAction(effectiveDbAction),
17156
17334
  detectedAt: r.occurredAt,
17157
17335
  confidence: r.confidence,
@@ -17283,14 +17461,17 @@ function applyFindingFilters(groups, opts) {
17283
17461
  }
17284
17462
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17285
17463
  var SEVERITY_RANK = SEVERITY_ORDER;
17464
+ function compareFindingGroupOrder(a, b) {
17465
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17466
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17467
+ const severityDiff = rankA - rankB;
17468
+ if (severityDiff !== 0) return severityDiff;
17469
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17470
+ if (recencyDiff !== 0) return recencyDiff;
17471
+ return a.id.localeCompare(b.id);
17472
+ }
17286
17473
  function sortFindingGroups(groups) {
17287
- return [...groups].sort((a, b) => {
17288
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17289
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17290
- const severityDiff = rankA - rankB;
17291
- if (severityDiff !== 0) return severityDiff;
17292
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17293
- });
17474
+ return [...groups].sort(compareFindingGroupOrder);
17294
17475
  }
17295
17476
  function computeFindingFacets(allGroups, opts) {
17296
17477
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17346,15 +17527,158 @@ function computeFindingFacets(allGroups, opts) {
17346
17527
  for (const g of forStatus) {
17347
17528
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17348
17529
  }
17349
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17530
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17531
+ return {
17532
+ severity: toItems2(severityMap),
17533
+ provider: toItems2(providerMap),
17534
+ action: toItems2(actionMap),
17535
+ subtype: toItems2(subtypeMap),
17536
+ status: toItems2(statusMap)
17537
+ };
17538
+ }
17539
+
17540
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17541
+ function rowHaystack(row) {
17542
+ return [
17543
+ row.ruleId,
17544
+ row.category,
17545
+ row.maskedMatch,
17546
+ row.repo,
17547
+ row.file,
17548
+ row.toolName ? `via ${row.toolName}` : "",
17549
+ row.id
17550
+ ].join(" ").toLowerCase();
17551
+ }
17552
+ function matchesDimension(row, opts, dimension) {
17553
+ switch (dimension) {
17554
+ case "severity":
17555
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17556
+ case "subtype":
17557
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17558
+ case "providers":
17559
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17560
+ case "actions":
17561
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17562
+ case "statuses":
17563
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17564
+ case "tools":
17565
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17566
+ case "repo":
17567
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17568
+ case "file":
17569
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17570
+ case "q":
17571
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17572
+ }
17573
+ }
17574
+ var DIMENSIONS = [
17575
+ "severity",
17576
+ "subtype",
17577
+ "providers",
17578
+ "actions",
17579
+ "statuses",
17580
+ "tools",
17581
+ "repo",
17582
+ "file",
17583
+ "q"
17584
+ ];
17585
+ function matchesInstanceFilters(row, opts, except) {
17586
+ for (const dimension of DIMENSIONS) {
17587
+ if (dimension === except) continue;
17588
+ if (!matchesDimension(row, opts, dimension)) return false;
17589
+ }
17590
+ return true;
17591
+ }
17592
+ function toItems(counts) {
17593
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17594
+ }
17595
+ function bump(counts, value) {
17596
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17597
+ }
17598
+ function createInstanceFacetAccumulator(opts) {
17599
+ const severity = /* @__PURE__ */ new Map();
17600
+ const subtype = /* @__PURE__ */ new Map();
17601
+ const provider = /* @__PURE__ */ new Map();
17602
+ const action = /* @__PURE__ */ new Map();
17603
+ const status = /* @__PURE__ */ new Map();
17604
+ const tool = /* @__PURE__ */ new Map();
17605
+ return {
17606
+ add(row) {
17607
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17608
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17609
+ if (matchesInstanceFilters(row, opts, "providers")) {
17610
+ bump(provider, toApiProvider(row.sourceTool));
17611
+ }
17612
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17613
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17614
+ bump(status, row.status);
17615
+ }
17616
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17617
+ bump(tool, row.toolName);
17618
+ }
17619
+ },
17620
+ facets: () => ({
17621
+ severity: toItems(severity),
17622
+ subtype: toItems(subtype),
17623
+ provider: toItems(provider),
17624
+ action: toItems(action),
17625
+ status: toItems(status),
17626
+ tool: toItems(tool)
17627
+ })
17628
+ };
17629
+ }
17630
+ function toInstanceDetail(row) {
17631
+ const category = toApiCategory(row.category);
17632
+ return {
17633
+ id: row.id,
17634
+ provider: toApiProvider(row.sourceTool),
17635
+ repo: row.repo,
17636
+ file: row.file,
17637
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17638
+ eventId: row.eventId,
17639
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17640
+ action: toApiAction(row.actionTaken),
17641
+ detectedAt: row.occurredAt,
17642
+ confidence: row.confidence,
17643
+ ...row.status === void 0 ? {} : { status: row.status },
17644
+ groupId: row.ruleId,
17645
+ category,
17646
+ subtype: row.ruleId,
17647
+ severity: row.severity,
17648
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17649
+ detection: { id: row.ruleId, name: null },
17650
+ policy: { id: `category:${category}`, name: category }
17651
+ };
17652
+ }
17653
+ var SEVERITY_ORDER2 = {
17654
+ critical: 0,
17655
+ high: 1,
17656
+ medium: 2,
17657
+ low: 3
17658
+ };
17659
+ function newLocationAccumulator() {
17350
17660
  return {
17351
- severity: toItems(severityMap),
17352
- provider: toItems(providerMap),
17353
- action: toItems(actionMap),
17354
- subtype: toItems(subtypeMap),
17355
- status: toItems(statusMap)
17661
+ instanceCount: 0,
17662
+ // Sorts after every known severity, so the first row always wins the
17663
+ // comparison below rather than an unknown value pinning the location.
17664
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17665
+ maxSeverity: "low",
17666
+ latestDetectedAt: "",
17667
+ statuses: [],
17668
+ ruleIds: /* @__PURE__ */ new Set()
17356
17669
  };
17357
17670
  }
17671
+ function addToLocation(acc, row) {
17672
+ acc.instanceCount += 1;
17673
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17674
+ if (rank < acc.maxSeverityRank) {
17675
+ acc.maxSeverityRank = rank;
17676
+ acc.maxSeverity = row.severity;
17677
+ }
17678
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17679
+ acc.statuses.push(row.status);
17680
+ acc.ruleIds.add(row.ruleId);
17681
+ }
17358
17682
 
17359
17683
  // ../../packages/schema/src/zod/installed-pack.ts
17360
17684
  var InstalledPack = external_exports.object({
@@ -17495,6 +17819,50 @@ var VaultInventoryEntry = external_exports.object({
17495
17819
  revealGrantId: external_exports.string().nullable(),
17496
17820
  sightings: external_exports.array(VaultSighting)
17497
17821
  });
17822
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17823
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17824
+ var MAX_VAULT_PAGE_LIMIT = 200;
17825
+ var ListVaultInventoryQuery = external_exports.object({
17826
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17827
+ // Opaque; names the last row of the page just served.
17828
+ cursor: external_exports.string().optional()
17829
+ });
17830
+ var ListVaultInventoryResponse = external_exports.object({
17831
+ // Vaulted values across the whole store, not just this page — cursor-
17832
+ // independent, so paging never changes what the count claims.
17833
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17834
+ items: external_exports.array(VaultInventoryEntry),
17835
+ // `null` once the last page is reached.
17836
+ nextCursor: external_exports.string().nullable()
17837
+ });
17838
+ var ListVaultReuseQuery = external_exports.object({
17839
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17840
+ cursor: external_exports.string().optional()
17841
+ });
17842
+ var ListVaultReuseResponse = external_exports.object({
17843
+ // Reused values across the whole store — the number the section's claim
17844
+ // ("values detected in more than one place") is about.
17845
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17846
+ items: external_exports.array(VaultInventoryEntry),
17847
+ nextCursor: external_exports.string().nullable()
17848
+ });
17849
+ var ListVaultDerefsQuery = external_exports.object({
17850
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17851
+ // hides them and counts them into `hiddenBatched` instead, so the model
17852
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17853
+ // over a Server Action, which preserves the type, never as a URL param.
17854
+ includeBatched: external_exports.boolean().optional(),
17855
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17856
+ cursor: external_exports.string().optional()
17857
+ });
17858
+ var ListVaultDerefsResponse = external_exports.object({
17859
+ items: external_exports.array(VaultDeref),
17860
+ nextCursor: external_exports.string().nullable(),
17861
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17862
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17863
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17864
+ hiddenBatched: external_exports.number().int().nonnegative()
17865
+ });
17498
17866
  var VaultKeyCustody = external_exports.string();
17499
17867
  var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17500
17868
  var VAULT_CONSENT_VERSION = 1;
@@ -17871,7 +18239,7 @@ var TopSourcesQuery = external_exports.object({
17871
18239
  // Omit for both kinds.
17872
18240
  kind: external_exports.enum(SOURCE_KINDS).optional()
17873
18241
  });
17874
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18242
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17875
18243
  var ScanCoverageProvider = external_exports.object({
17876
18244
  provider: Provider,
17877
18245
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -18124,6 +18492,195 @@ function captureId(sessionId, contentHash, filePath = null) {
18124
18492
  );
18125
18493
  }
18126
18494
 
18495
+ // ../../packages/persistence/src/internal/snapshot.ts
18496
+ import { randomUUID } from "crypto";
18497
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18498
+ import { basename, dirname, join } from "path";
18499
+
18500
+ // ../../packages/persistence/src/paths.ts
18501
+ import {
18502
+ chmodSync,
18503
+ linkSync,
18504
+ lstatSync,
18505
+ mkdirSync,
18506
+ renameSync,
18507
+ rmSync,
18508
+ writeFileSync
18509
+ } from "fs";
18510
+ import { threadId } from "worker_threads";
18511
+ var DATA_DIR_MODE = 448;
18512
+ var DATA_FILE_MODE = 384;
18513
+ var DB_FILENAME = "aka.db";
18514
+ function isSymlink(path) {
18515
+ try {
18516
+ return lstatSync(path).isSymbolicLink();
18517
+ } catch {
18518
+ return false;
18519
+ }
18520
+ }
18521
+ function chmodBestEffort(path, mode) {
18522
+ if (isSymlink(path)) return;
18523
+ try {
18524
+ chmodSync(path, mode);
18525
+ } catch {
18526
+ }
18527
+ }
18528
+ function tightenDir(dir) {
18529
+ chmodBestEffort(dir, DATA_DIR_MODE);
18530
+ }
18531
+ function ensureDataDirSync(dir) {
18532
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18533
+ tightenDir(dir);
18534
+ }
18535
+ function dbSidecars(file2) {
18536
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18537
+ }
18538
+ function tightenFile(file2) {
18539
+ chmodBestEffort(file2, DATA_FILE_MODE);
18540
+ }
18541
+ function tightenPerms(file2) {
18542
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18543
+ }
18544
+ function classifyOccupant(file2) {
18545
+ try {
18546
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
18547
+ return { kind: "gone" };
18548
+ } catch (err) {
18549
+ if (err.code === "ENOENT") return { kind: "gone" };
18550
+ return { kind: "unknown", cause: err };
18551
+ }
18552
+ }
18553
+ var KeyUnclaimableError = class extends Error {
18554
+ code = "key-unclaimable";
18555
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
18556
+ // defines the property anyway, so an error carrying nothing would still answer
18557
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
18558
+ // captured and then lost, which is worse than its plain absence.
18559
+ constructor(message, cause) {
18560
+ super(message, cause === void 0 ? void 0 : { cause });
18561
+ this.name = "KeyUnclaimableError";
18562
+ }
18563
+ };
18564
+ function createOwnerOnlyFileSync(file2, data) {
18565
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
18566
+ try {
18567
+ rmSync(tmp, { force: true });
18568
+ } catch {
18569
+ }
18570
+ let created;
18571
+ try {
18572
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18573
+ created = publishByLink(tmp, file2, data);
18574
+ } finally {
18575
+ try {
18576
+ rmSync(tmp, { force: true });
18577
+ } catch {
18578
+ }
18579
+ }
18580
+ if (created) tightenFile(file2);
18581
+ return created;
18582
+ }
18583
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
18584
+ function publishByLink(tmp, file2, data) {
18585
+ try {
18586
+ linkSync(tmp, file2);
18587
+ return true;
18588
+ } catch (err) {
18589
+ const code = err.code;
18590
+ if (code === "EEXIST") return false;
18591
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
18592
+ }
18593
+ try {
18594
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
18595
+ return true;
18596
+ } catch (err) {
18597
+ if (err.code === "EEXIST") return false;
18598
+ throw err;
18599
+ }
18600
+ }
18601
+
18602
+ // ../../packages/persistence/src/internal/snapshot.ts
18603
+ function backupPath(file2, tag) {
18604
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18605
+ }
18606
+ var STALE_PARTIAL_MS = 5 * 6e4;
18607
+ function reapStalePartials(file2) {
18608
+ const dir = dirname(file2);
18609
+ const prefix = `${basename(file2)}.`;
18610
+ let entries;
18611
+ try {
18612
+ entries = readdirSync(dir);
18613
+ } catch {
18614
+ return;
18615
+ }
18616
+ for (const name of entries) {
18617
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18618
+ const partial2 = join(dir, name);
18619
+ try {
18620
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18621
+ rmSync2(partial2, { force: true });
18622
+ }
18623
+ } catch {
18624
+ }
18625
+ }
18626
+ }
18627
+ function snapshotStore(db, backup) {
18628
+ const partial2 = `${backup}.partial`;
18629
+ try {
18630
+ rmSync2(partial2, { force: true });
18631
+ db.prepare("VACUUM INTO ?").run(partial2);
18632
+ tightenFile(partial2);
18633
+ renameSync2(partial2, backup);
18634
+ } catch (error51) {
18635
+ try {
18636
+ rmSync2(partial2, { force: true });
18637
+ } catch {
18638
+ }
18639
+ throw error51;
18640
+ }
18641
+ }
18642
+ function moveStoreAside(file2, backup) {
18643
+ const undo = [];
18644
+ renameSync2(file2, backup);
18645
+ undo.push([backup, file2]);
18646
+ try {
18647
+ for (const sidecar of dbSidecars(file2)) {
18648
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18649
+ try {
18650
+ renameSync2(sidecar, moved);
18651
+ undo.push([moved, sidecar]);
18652
+ } catch {
18653
+ rmSync2(sidecar, { force: true });
18654
+ }
18655
+ }
18656
+ } catch (error51) {
18657
+ for (const [from, to] of undo.reverse()) {
18658
+ try {
18659
+ renameSync2(from, to);
18660
+ } catch {
18661
+ }
18662
+ }
18663
+ throw error51;
18664
+ }
18665
+ tightenPerms(backup);
18666
+ }
18667
+ function discardStore(file2, backup) {
18668
+ try {
18669
+ rmSync2(file2, { force: true });
18670
+ for (const sidecar of dbSidecars(file2)) {
18671
+ rmSync2(sidecar, { force: true });
18672
+ }
18673
+ } catch (error51) {
18674
+ if (existsSync(file2)) {
18675
+ try {
18676
+ rmSync2(backup, { force: true });
18677
+ } catch {
18678
+ }
18679
+ }
18680
+ throw error51;
18681
+ }
18682
+ }
18683
+
18127
18684
  // ../../packages/persistence/src/internal/sql-text.ts
18128
18685
  function escapeLikePattern(s) {
18129
18686
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18265,55 +18822,6 @@ function mapRowsTolerant(rows, map2) {
18265
18822
  return out;
18266
18823
  }
18267
18824
 
18268
- // ../../packages/persistence/src/paths.ts
18269
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18270
- var DATA_DIR_MODE = 448;
18271
- var DATA_FILE_MODE = 384;
18272
- var DB_FILENAME = "aka.db";
18273
- function chmodBestEffort(path, mode) {
18274
- try {
18275
- chmodSync(path, mode);
18276
- } catch {
18277
- }
18278
- }
18279
- function tightenDir(dir) {
18280
- chmodBestEffort(dir, DATA_DIR_MODE);
18281
- }
18282
- function ensureDataDirSync(dir) {
18283
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18284
- tightenDir(dir);
18285
- }
18286
- function dbSidecars(file2) {
18287
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18288
- }
18289
- function tightenFile(file2) {
18290
- try {
18291
- if (lstatSync(file2).isSymbolicLink()) return;
18292
- } catch {
18293
- }
18294
- chmodBestEffort(file2, DATA_FILE_MODE);
18295
- }
18296
- function tightenPerms(file2) {
18297
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18298
- }
18299
- function writeOwnerOnlyFileSync(file2, data) {
18300
- const tmp = `${file2}.${String(process.pid)}.tmp`;
18301
- try {
18302
- rmSync(tmp, { force: true });
18303
- } catch {
18304
- }
18305
- try {
18306
- writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18307
- renameSync(tmp, file2);
18308
- } finally {
18309
- try {
18310
- rmSync(tmp, { force: true });
18311
- } catch {
18312
- }
18313
- }
18314
- tightenFile(file2);
18315
- }
18316
-
18317
18825
  // ../../packages/persistence/src/migrations.ts
18318
18826
  function describeObject(object2) {
18319
18827
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18429,9 +18937,9 @@ function applyLegacyDropMigration(db, file2) {
18429
18937
  }
18430
18938
  }
18431
18939
  function backupBeforeLegacyDrop(db, file2) {
18432
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18433
- db.prepare("VACUUM INTO ?").run(backup);
18434
- tightenFile(backup);
18940
+ reapStalePartials(file2);
18941
+ const backup = backupPath(file2, "pre-drop");
18942
+ snapshotStore(db, backup);
18435
18943
  return backup;
18436
18944
  }
18437
18945
  var TOKEN_USAGE_COLUMNS = [
@@ -18775,6 +19283,25 @@ function parseJsonObject(s) {
18775
19283
  return void 0;
18776
19284
  }
18777
19285
 
19286
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19287
+ function encodeKeysetCursor(payload) {
19288
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19289
+ }
19290
+ function decodeKeysetCursor(cursor) {
19291
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19292
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19293
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19294
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19295
+ // a null cursor, which a caller reads as "end of list". That is the one
19296
+ // outcome a cursor that does not decode must never produce, since the
19297
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19298
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19299
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19300
+ return parsed;
19301
+ }
19302
+ return null;
19303
+ }
19304
+
18778
19305
  // ../../packages/persistence/src/repositories/activity.ts
18779
19306
  var DAY_MS = 864e5;
18780
19307
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18820,16 +19347,6 @@ function utcWindow(nowMs) {
18820
19347
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18821
19348
  return { startMs, endMs: startMs + DAY_MS };
18822
19349
  }
18823
- function encodeCursor(payload) {
18824
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18825
- }
18826
- function decodeCursor(cursor) {
18827
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18828
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18829
- return parsed;
18830
- }
18831
- return null;
18832
- }
18833
19350
  var DB_EVENT_TYPE_TO_KIND = {
18834
19351
  session: "session",
18835
19352
  prompt: "prompt",
@@ -18974,7 +19491,7 @@ var SqliteActivityRepository = class {
18974
19491
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18975
19492
  }
18976
19493
  listSessions(query) {
18977
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19494
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18978
19495
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18979
19496
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18980
19497
  const conditions = [SESSION_ROOT];
@@ -19048,7 +19565,7 @@ var SqliteActivityRepository = class {
19048
19565
  )
19049
19566
  );
19050
19567
  const last = page[page.length - 1];
19051
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19568
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19052
19569
  return Promise.resolve({ items, nextCursor, emptyCount });
19053
19570
  }
19054
19571
  getSession(sessionId) {
@@ -19921,7 +20438,7 @@ var SqliteEventsRepository = class {
19921
20438
  };
19922
20439
 
19923
20440
  // ../../packages/persistence/src/repositories/exceptions.ts
19924
- import { randomUUID } from "crypto";
20441
+ import { randomUUID as randomUUID2 } from "crypto";
19925
20442
 
19926
20443
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19927
20444
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19957,8 +20474,9 @@ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19957
20474
  AND conditions IS NULL
19958
20475
  AND ${ACTIVE_PREDICATE}`;
19959
20476
  var SqliteExceptionsRepository = class {
19960
- constructor(db) {
20477
+ constructor(db, now = () => Date.now()) {
19961
20478
  this.db = db;
20479
+ this.now = now;
19962
20480
  this.consumeStmt = db.prepare(
19963
20481
  `UPDATE exceptions
19964
20482
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19976,6 +20494,7 @@ var SqliteExceptionsRepository = class {
19976
20494
  );
19977
20495
  }
19978
20496
  db;
20497
+ now;
19979
20498
  consumeStmt;
19980
20499
  insertBlockedStmt;
19981
20500
  sweepBlockedStmt;
@@ -20002,8 +20521,8 @@ var SqliteExceptionsRepository = class {
20002
20521
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
20003
20522
  );
20004
20523
  }
20005
- const id = randomUUID();
20006
- const now = Date.now();
20524
+ const id = randomUUID2();
20525
+ const now = this.now();
20007
20526
  try {
20008
20527
  this.insertExceptionRow(id, input, now);
20009
20528
  } catch (err) {
@@ -20081,7 +20600,7 @@ var SqliteExceptionsRepository = class {
20081
20600
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
20082
20601
  const rows = allRows(
20083
20602
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
20084
- opts?.includeTerminal ? {} : { now: Date.now() }
20603
+ opts?.includeTerminal ? {} : { now: this.now() }
20085
20604
  );
20086
20605
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
20087
20606
  return Promise.resolve(exceptions);
@@ -20116,7 +20635,7 @@ var SqliteExceptionsRepository = class {
20116
20635
  * already revoked.
20117
20636
  */
20118
20637
  revoke(id, revokedBy, reason) {
20119
- const now = Date.now();
20638
+ const now = this.now();
20120
20639
  const result = this.db.prepare(
20121
20640
  `UPDATE exceptions
20122
20641
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -20130,7 +20649,7 @@ var SqliteExceptionsRepository = class {
20130
20649
  * callers must treat identically — means it does not and the detection is
20131
20650
  * enforced as usual. Deliberately NOT wrapped in try/catch.
20132
20651
  */
20133
- consume(id, now = Date.now()) {
20652
+ consume(id, now = this.now()) {
20134
20653
  const result = this.consumeStmt.run({ id, now });
20135
20654
  return Promise.resolve(Number(result.changes) === 1);
20136
20655
  }
@@ -20139,7 +20658,7 @@ var SqliteExceptionsRepository = class {
20139
20658
  * version — what rides the policy bundle to the hook. Grants written under
20140
20659
  * a different (rotated-away) key never match, so they are excluded at read.
20141
20660
  */
20142
- activeBundleEntries(keyVersion, now = Date.now()) {
20661
+ activeBundleEntries(keyVersion, now = this.now()) {
20143
20662
  const rows = allRows(
20144
20663
  this.db.prepare(
20145
20664
  `SELECT * FROM exceptions
@@ -20171,7 +20690,7 @@ var SqliteExceptionsRepository = class {
20171
20690
  * than the retention window on every write, so the ledger self-limits.
20172
20691
  */
20173
20692
  recordBlocked(entry) {
20174
- const now = Date.now();
20693
+ const now = this.now();
20175
20694
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
20176
20695
  this.insertBlockedStmt.run({
20177
20696
  reference: entry.reference,
@@ -20194,7 +20713,7 @@ var SqliteExceptionsRepository = class {
20194
20713
  WHERE blocked_at > :cutoff
20195
20714
  ORDER BY blocked_at DESC, rowid DESC`
20196
20715
  ),
20197
- { cutoff: Date.now() - windowMs }
20716
+ { cutoff: this.now() - windowMs }
20198
20717
  );
20199
20718
  return Promise.resolve(
20200
20719
  rows.map((row) => ({
@@ -20222,8 +20741,9 @@ var SqliteExceptionsRepository = class {
20222
20741
  * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20223
20742
  * grant instead. Fail closed until reveal-side condition evaluation exists.
20224
20743
  */
20225
- activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20744
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20226
20745
  try {
20746
+ const at = now ?? this.now();
20227
20747
  const row = getRow(
20228
20748
  this.db.prepare(
20229
20749
  `SELECT id FROM exceptions
@@ -20232,7 +20752,7 @@ var SqliteExceptionsRepository = class {
20232
20752
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20233
20753
  LIMIT 1`
20234
20754
  ),
20235
- { ruleId, valueFingerprint, keyVersion, now }
20755
+ { ruleId, valueFingerprint, keyVersion, now: at }
20236
20756
  );
20237
20757
  return Promise.resolve(row ?? null);
20238
20758
  } catch (err) {
@@ -20246,7 +20766,7 @@ var SqliteExceptionsRepository = class {
20246
20766
  * predicate, so correctness never depends on this sweep; it only bounds how
20247
20767
  * long the audit evidence is kept locally. Returns the deleted count.
20248
20768
  */
20249
- sweepTerminal(retentionMs, now = Date.now()) {
20769
+ sweepTerminal(retentionMs, now = this.now()) {
20250
20770
  const result = this.db.prepare(
20251
20771
  `DELETE FROM exceptions
20252
20772
  WHERE updated_at < :cutoff
@@ -20309,6 +20829,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20309
20829
 
20310
20830
  // ../../packages/persistence/src/repositories/findings.ts
20311
20831
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20832
+ var SCAN_BATCH_ROWS = 1e3;
20833
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20834
+ var LOCATION_RULE_IDS_CAP = 20;
20835
+ function compareLocationOrder(a, b) {
20836
+ return compareFindingGroupOrder(
20837
+ {
20838
+ severity: a.maxSeverity,
20839
+ latestDetectedAt: a.latestDetectedAt,
20840
+ id: ""
20841
+ },
20842
+ {
20843
+ severity: b.maxSeverity,
20844
+ latestDetectedAt: b.latestDetectedAt,
20845
+ id: ""
20846
+ }
20847
+ );
20848
+ }
20312
20849
  var CONCAT_SEP = ",";
20313
20850
  var TUPLE_SEP = "|";
20314
20851
  function splitConcat(value) {
@@ -20321,6 +20858,33 @@ function deriveInstanceStatus(row) {
20321
20858
  latestResolutionStatus: row.latest_status
20322
20859
  });
20323
20860
  }
20861
+ function encodeGroupCursor(group) {
20862
+ const payload = {
20863
+ sev: group.severity,
20864
+ t: group.latestDetectedAt,
20865
+ id: group.id
20866
+ };
20867
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20868
+ }
20869
+ function decodeGroupCursor(cursor) {
20870
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20871
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20872
+ return {
20873
+ severity: parsed.sev,
20874
+ latestDetectedAt: parsed.t,
20875
+ id: parsed.id
20876
+ };
20877
+ }
20878
+ return null;
20879
+ }
20880
+ function firstAfter(sorted, cursor) {
20881
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20882
+ return index === -1 ? sorted.length : index;
20883
+ }
20884
+ function findDeepLinked(sorted, page, id) {
20885
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20886
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20887
+ }
20324
20888
  var DAY_MS3 = 864e5;
20325
20889
  var SqliteFindingsRepository = class {
20326
20890
  constructor(db) {
@@ -20430,8 +20994,13 @@ var SqliteFindingsRepository = class {
20430
20994
  */
20431
20995
  listGroupedFindings(query) {
20432
20996
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20433
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20434
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20997
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20998
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20999
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
21000
+ const sessionParams = {
21001
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
21002
+ ...fromMs === void 0 ? {} : { fromMs }
21003
+ };
20435
21004
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20436
21005
  predicate,
20437
21006
  params: sessionParams
@@ -20439,7 +21008,8 @@ var SqliteFindingsRepository = class {
20439
21008
  const rows = allRows(
20440
21009
  this.db.prepare(
20441
21010
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20442
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
21011
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
21012
+ kind, finding_key, latest_status
20443
21013
  FROM (
20444
21014
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20445
21015
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20449,6 +21019,7 @@ var SqliteFindingsRepository = class {
20449
21019
  json_extract(e.attributes, '$.repo') AS repo,
20450
21020
  json_extract(e.attributes, '$.file_path') AS file,
20451
21021
  json_extract(e.attributes, '$.tool_name') AS tool_name,
21022
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20452
21023
  e.event_type AS kind, f.finding_key AS finding_key,
20453
21024
  latest.status AS latest_status,
20454
21025
  ROW_NUMBER() OVER (
@@ -20480,6 +21051,8 @@ var SqliteFindingsRepository = class {
20480
21051
  repo: r.repo ?? "",
20481
21052
  file: r.file ?? "",
20482
21053
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
21054
+ eventId: r.event_id,
21055
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20483
21056
  status: deriveInstanceStatus(r)
20484
21057
  }));
20485
21058
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20503,18 +21076,23 @@ var SqliteFindingsRepository = class {
20503
21076
  groups: sorted.length
20504
21077
  };
20505
21078
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21079
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21080
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21081
+ const page = sorted.slice(start, start + limit);
21082
+ const lastOnPage = page.at(-1);
21083
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21084
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20506
21085
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20507
- const items = sorted.slice(0, limit).map(
20508
- (g) => statusSet ? {
20509
- ...g,
20510
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20511
- } : g
20512
- );
21086
+ const narrow = (g) => statusSet ? {
21087
+ ...g,
21088
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21089
+ } : g;
21090
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20513
21091
  return Promise.resolve({
20514
21092
  totals,
20515
21093
  facets,
20516
21094
  items,
20517
- nextCursor: null,
21095
+ nextCursor,
20518
21096
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20519
21097
  });
20520
21098
  }
@@ -20546,6 +21124,266 @@ var SqliteFindingsRepository = class {
20546
21124
  * request actually carries a `q`. (Substring matching is unaffected by a
20547
21125
  * path repeating across tuples.)
20548
21126
  */
21127
+ /**
21128
+ * The instance-level (flat) findings list: one row per finding, newest first,
21129
+ * paged by keyset.
21130
+ *
21131
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21132
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21133
+ * them changes no reported number. Severity, subtype, provider, action,
21134
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21135
+ * facet excludes its own filter, so a row the filter rejects still has to be
21136
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21137
+ * Several could not be expressed there anyway: status comes from the one
21138
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21139
+ * none of the mappers names", which no IN-list can say.
21140
+ *
21141
+ * The scan runs from the top of the scope on every request, not from the
21142
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21143
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21144
+ * while the counting runs, and only the page itself is retained.
21145
+ */
21146
+ listFindingInstances(query) {
21147
+ const opts = {
21148
+ severity: query.severity,
21149
+ subtype: query.subtype,
21150
+ providers: query.provider,
21151
+ actions: query.action,
21152
+ statuses: query.status,
21153
+ tools: query.tool,
21154
+ repo: query.repo,
21155
+ file: query.file,
21156
+ q: query.q
21157
+ };
21158
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21159
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21160
+ const accumulator = createInstanceFacetAccumulator(opts);
21161
+ const items = [];
21162
+ let total = 0;
21163
+ let last;
21164
+ let hasMore = false;
21165
+ for (const row of this.scanFindingRows({
21166
+ sessionId: query.sessionId,
21167
+ from: query.from
21168
+ })) {
21169
+ accumulator.add(row);
21170
+ if (!matchesInstanceFilters(row, opts)) continue;
21171
+ total += 1;
21172
+ if (items.length < limit) {
21173
+ items.push(toInstanceDetail(row));
21174
+ last = row;
21175
+ } else {
21176
+ hasMore = true;
21177
+ }
21178
+ }
21179
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21180
+ if (cursor !== null) {
21181
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21182
+ return Promise.resolve({
21183
+ totals: { findings: total },
21184
+ facets: accumulator.facets(),
21185
+ items: resumed.items,
21186
+ nextCursor: resumed.nextCursor
21187
+ });
21188
+ }
21189
+ return Promise.resolve({
21190
+ totals: { findings: total },
21191
+ facets: accumulator.facets(),
21192
+ items,
21193
+ nextCursor
21194
+ });
21195
+ }
21196
+ /**
21197
+ * The page of matching rows strictly after `cursor`. Separate from the
21198
+ * counting pass because that one starts at the top of the scope by design;
21199
+ * this one narrows the scan with the same keyset predicate the activity list
21200
+ * uses, so a later page costs less than the first rather than more.
21201
+ */
21202
+ pageAfter(cursor, opts, limit, query) {
21203
+ const items = [];
21204
+ let last;
21205
+ let hasMore = false;
21206
+ for (const row of this.scanFindingRows({
21207
+ sessionId: query.sessionId,
21208
+ from: query.from,
21209
+ after: cursor
21210
+ })) {
21211
+ if (!matchesInstanceFilters(row, opts)) continue;
21212
+ if (items.length < limit) {
21213
+ items.push(toInstanceDetail(row));
21214
+ last = row;
21215
+ } else {
21216
+ hasMore = true;
21217
+ break;
21218
+ }
21219
+ }
21220
+ return {
21221
+ items,
21222
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21223
+ };
21224
+ }
21225
+ /**
21226
+ * The same findings folded by location: repository, then file within it.
21227
+ *
21228
+ * The grouping keys come from the capturing event's attributes, which is what
21229
+ * the local store relates a finding to — there is no finding↔asset row to
21230
+ * group by instead. A repo or file the event did not record folds into the
21231
+ * empty-string bucket, which the view renders but does not link, since no
21232
+ * filter can name it.
21233
+ */
21234
+ listFindingLocations(query) {
21235
+ const opts = {
21236
+ severity: query.severity,
21237
+ subtype: query.subtype,
21238
+ providers: query.provider,
21239
+ actions: query.action,
21240
+ statuses: query.status,
21241
+ tools: query.tool,
21242
+ q: query.q
21243
+ };
21244
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21245
+ const byRepo = /* @__PURE__ */ new Map();
21246
+ let total = 0;
21247
+ for (const row of this.scanFindingRows({
21248
+ sessionId: query.sessionId,
21249
+ from: query.from
21250
+ })) {
21251
+ if (!matchesInstanceFilters(row, opts)) continue;
21252
+ total += 1;
21253
+ let files = byRepo.get(row.repo);
21254
+ if (files === void 0) {
21255
+ files = /* @__PURE__ */ new Map();
21256
+ byRepo.set(row.repo, files);
21257
+ }
21258
+ let acc = files.get(row.file);
21259
+ if (acc === void 0) {
21260
+ acc = newLocationAccumulator();
21261
+ files.set(row.file, acc);
21262
+ }
21263
+ addToLocation(acc, row);
21264
+ }
21265
+ let fileCount = 0;
21266
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21267
+ fileCount += files.size;
21268
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21269
+ file: file2,
21270
+ instanceCount: acc.instanceCount,
21271
+ maxSeverity: acc.maxSeverity,
21272
+ latestDetectedAt: acc.latestDetectedAt,
21273
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21274
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21275
+ })).sort(compareLocationOrder);
21276
+ const rollup = fileRows.reduce(
21277
+ (a, f) => ({
21278
+ instanceCount: a.instanceCount + f.instanceCount,
21279
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21280
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21281
+ }),
21282
+ {
21283
+ instanceCount: 0,
21284
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21285
+ latestDetectedAt: ""
21286
+ }
21287
+ );
21288
+ const statuses = fileRows.map((f) => f.status);
21289
+ const folded = foldGroupStatus(statuses);
21290
+ return {
21291
+ repo,
21292
+ instanceCount: rollup.instanceCount,
21293
+ maxSeverity: rollup.maxSeverity,
21294
+ latestDetectedAt: rollup.latestDetectedAt,
21295
+ ...folded === void 0 ? {} : { status: folded },
21296
+ files: fileRows
21297
+ };
21298
+ });
21299
+ repos.sort(compareLocationOrder);
21300
+ return Promise.resolve({
21301
+ totals: { findings: total, repos: repos.length, files: fileCount },
21302
+ items: repos.slice(0, limit),
21303
+ hasMore: repos.length > limit
21304
+ });
21305
+ }
21306
+ /**
21307
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21308
+ *
21309
+ * A generator so a caller streams the scope without it ever being an array:
21310
+ * the flat list counts and facets the whole filtered scope, which on a large
21311
+ * store is far more rows than any page. Each batch advances the same keyset
21312
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21313
+ * rather than one unbounded result set.
21314
+ *
21315
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21316
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21317
+ * makes it a point lookup per row, and the derived table would re-materialize
21318
+ * a window over the whole resolution table once per batch.
21319
+ *
21320
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21321
+ * would be missing from its own facet, which is computed by excluding that
21322
+ * dimension — see listFindingInstances.
21323
+ */
21324
+ *scanFindingRows(scope) {
21325
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21326
+ const params = [];
21327
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21328
+ conditions.push("e.root_session_id = ?");
21329
+ params.push(scope.sessionId);
21330
+ }
21331
+ if (scope.from !== void 0) {
21332
+ conditions.push("e.started_at >= ?");
21333
+ params.push(isoToEpochMillis(scope.from));
21334
+ }
21335
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21336
+ d.severity AS severity, f.masked_match AS masked_match,
21337
+ f.action_taken AS action_taken, f.confidence AS confidence,
21338
+ e.started_at AS occurred_at,
21339
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21340
+ json_extract(e.attributes, '$.repo') AS repo,
21341
+ json_extract(e.attributes, '$.file_path') AS file,
21342
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21343
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21344
+ e.event_type AS kind, f.finding_key AS finding_key,
21345
+ ${latestResolutionStatusSql("f")} AS latest_status
21346
+ FROM inspection_findings f
21347
+ JOIN audit_events e ON e.id = f.audit_event_id
21348
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21349
+ WHERE ${conditions.join(" AND ")}
21350
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21351
+ ORDER BY e.started_at DESC, f.id DESC
21352
+ LIMIT ?`;
21353
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21354
+ for (; ; ) {
21355
+ const rows = allRows(this.db.prepare(sql), [
21356
+ ...params,
21357
+ after.startedAtMs,
21358
+ after.startedAtMs,
21359
+ after.id,
21360
+ SCAN_BATCH_ROWS
21361
+ ]);
21362
+ for (const r of rows) {
21363
+ yield {
21364
+ id: r.id,
21365
+ ruleId: r.rule_id,
21366
+ category: r.category,
21367
+ severity: r.severity,
21368
+ maskedMatch: r.masked_match,
21369
+ actionTaken: r.action_taken,
21370
+ confidence: r.confidence,
21371
+ occurredAt: epochMillisToIso(r.occurred_at),
21372
+ sourceTool: r.source_tool,
21373
+ repo: r.repo ?? "",
21374
+ file: r.file ?? "",
21375
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21376
+ eventId: r.event_id,
21377
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21378
+ status: deriveInstanceStatus(r)
21379
+ };
21380
+ }
21381
+ if (rows.length < SCAN_BATCH_ROWS) return;
21382
+ const lastRow = rows[rows.length - 1];
21383
+ if (lastRow === void 0) return;
21384
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21385
+ }
21386
+ }
20549
21387
  groupAggregates(withSearchText, scope) {
20550
21388
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20551
21389
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20806,7 +21644,7 @@ var SqliteInspectionFindingsRepository = class {
20806
21644
  };
20807
21645
 
20808
21646
  // ../../packages/persistence/src/repositories/installed-packs.ts
20809
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21647
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20810
21648
 
20811
21649
  // ../../packages/persistence/src/semver.ts
20812
21650
  function parse3(version2) {
@@ -20957,7 +21795,7 @@ var SqliteInstalledPacksRepository = class {
20957
21795
  let behind = false;
20958
21796
  for (const row of rows) {
20959
21797
  const params = {
20960
- id: randomUUID2(),
21798
+ id: randomUUID3(),
20961
21799
  namespace: row.namespace,
20962
21800
  packId: row.packId,
20963
21801
  version: row.version,
@@ -20969,7 +21807,7 @@ var SqliteInstalledPacksRepository = class {
20969
21807
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20970
21808
  this.upsertAvailableStmt.run({
20971
21809
  ...params,
20972
- id: randomUUID2(),
21810
+ id: randomUUID3(),
20973
21811
  recordedBy: meta3?.recordedBy ?? null
20974
21812
  });
20975
21813
  } else {
@@ -21292,14 +22130,15 @@ var SqliteInventoryRepository = class {
21292
22130
  };
21293
22131
 
21294
22132
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21295
- import { randomUUID as randomUUID3 } from "crypto";
22133
+ import { randomUUID as randomUUID4 } from "crypto";
21296
22134
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21297
22135
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21298
22136
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21299
22137
  var HARNESS_LABELS = {
21300
22138
  claudecode: "Claude Code",
21301
22139
  cursor: "Cursor",
21302
- codex: "Codex"
22140
+ codex: "Codex",
22141
+ antigravity: "Antigravity"
21303
22142
  };
21304
22143
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21305
22144
  var EMPTY_PROJECT_AGG = {
@@ -21314,6 +22153,7 @@ function resolveHarnessId(attrs, row) {
21314
22153
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21315
22154
  if (t.includes("cursor")) return "cursor";
21316
22155
  if (t.includes("codex")) return "codex";
22156
+ if (t.includes("antigravity")) return "antigravity";
21317
22157
  return null;
21318
22158
  }
21319
22159
  function isLiveRealClaudeCode(rows) {
@@ -21772,7 +22612,7 @@ var SqliteInventoryAssetsRepository = class {
21772
22612
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21773
22613
  VALUES (:id, :projectId, :path, :access, :now, :now)
21774
22614
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21775
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22615
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21776
22616
  }
21777
22617
  return true;
21778
22618
  }
@@ -21793,7 +22633,7 @@ var SqliteInventoryAssetsRepository = class {
21793
22633
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21794
22634
  VALUES (:id, :assetId, :trust, :now, :now)
21795
22635
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21796
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22636
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21797
22637
  }
21798
22638
  this.configRowsCache = void 0;
21799
22639
  return "ok";
@@ -22090,7 +22930,7 @@ var SqliteInventoryAssetsRepository = class {
22090
22930
  };
22091
22931
 
22092
22932
  // ../../packages/persistence/src/repositories/policies.ts
22093
- import { randomUUID as randomUUID4 } from "crypto";
22933
+ import { randomUUID as randomUUID5 } from "crypto";
22094
22934
  var SqlitePoliciesRepository = class {
22095
22935
  constructor(db) {
22096
22936
  this.db = db;
@@ -22125,7 +22965,7 @@ var SqlitePoliciesRepository = class {
22125
22965
  failOpenTransaction(this.db, () => {
22126
22966
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
22127
22967
  stmt.run({
22128
- id: randomUUID4(),
22968
+ id: randomUUID5(),
22129
22969
  target: JSON.stringify({ category }),
22130
22970
  action,
22131
22971
  now: Date.now()
@@ -22145,7 +22985,7 @@ var SqlitePoliciesRepository = class {
22145
22985
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
22146
22986
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
22147
22987
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
22148
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22988
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
22149
22989
  }
22150
22990
  // Caps every global per-category policy currently set to block/redact down
22151
22991
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22213,7 +23053,7 @@ var SqlitePolicyCatalogRepository = class {
22213
23053
  };
22214
23054
 
22215
23055
  // ../../packages/persistence/src/repositories/project-files.ts
22216
- import { randomUUID as randomUUID5 } from "crypto";
23056
+ import { randomUUID as randomUUID6 } from "crypto";
22217
23057
  var SqliteProjectFilesRepository = class {
22218
23058
  constructor(db) {
22219
23059
  this.db = db;
@@ -22245,7 +23085,7 @@ var SqliteProjectFilesRepository = class {
22245
23085
  const stamp = Math.max(now, maxStamp + 1);
22246
23086
  for (const file2 of scan2.files) {
22247
23087
  this.upsertStmt.run({
22248
- id: randomUUID5(),
23088
+ id: randomUUID6(),
22249
23089
  projectId,
22250
23090
  path: file2.path,
22251
23091
  name: file2.name,
@@ -22259,7 +23099,7 @@ var SqliteProjectFilesRepository = class {
22259
23099
  };
22260
23100
 
22261
23101
  // ../../packages/persistence/src/repositories/resolutions.ts
22262
- import { randomUUID as randomUUID6 } from "crypto";
23102
+ import { randomUUID as randomUUID7 } from "crypto";
22263
23103
  var SqliteResolutionsRepository = class {
22264
23104
  constructor(db, now = () => Date.now()) {
22265
23105
  this.db = db;
@@ -22313,7 +23153,7 @@ var SqliteResolutionsRepository = class {
22313
23153
  */
22314
23154
  insertResolution(r) {
22315
23155
  this.insertStmt.run({
22316
- id: randomUUID6(),
23156
+ id: randomUUID7(),
22317
23157
  findingKey: r.findingKey,
22318
23158
  status: FindingStatus.parse(r.status),
22319
23159
  method: ResolutionMethod.parse(r.method),
@@ -22372,13 +23212,51 @@ var SqliteRuleProbeCacheRepository = class {
22372
23212
  this.readStmt = db.prepare(
22373
23213
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22374
23214
  );
23215
+ this.countQuarantinedStmt = db.prepare(
23216
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23217
+ );
23218
+ this.clearQuarantinedStmt = db.prepare(
23219
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23220
+ );
22375
23221
  }
22376
23222
  db;
22377
23223
  upsertStmt;
22378
23224
  readStmt;
23225
+ countQuarantinedStmt;
23226
+ clearQuarantinedStmt;
22379
23227
  getVerdict(ruleKey) {
22380
23228
  return getRow(this.readStmt, { ruleKey });
22381
23229
  }
23230
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23231
+ countQuarantined() {
23232
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23233
+ }
23234
+ /**
23235
+ * Forgets every quarantine verdict, so the rules behind them are measured
23236
+ * again on the next load. This is the undo for a verdict the machine reached
23237
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23238
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23239
+ * loaded or slow machine can reach about a rule that is in fact fine.
23240
+ *
23241
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23242
+ * keeping, and dropping it would make every rule pay the battery again.
23243
+ *
23244
+ * Reports `refused` from the write's own result rather than inferring it from
23245
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23246
+ * swallows a contended DELETE (another writer holding the lock past
23247
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23248
+ * leaves the count unchanged, which is indistinguishable from "there was
23249
+ * nothing to clear". An undo that reports success while the quarantines are
23250
+ * still in place is worse than one that fails, because the rules it claimed
23251
+ * to restore are silently still disabled.
23252
+ */
23253
+ clearQuarantined() {
23254
+ const before = this.countQuarantined();
23255
+ const committed = failOpenTransaction(this.db, () => {
23256
+ this.clearQuarantinedStmt.run();
23257
+ });
23258
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23259
+ }
22382
23260
  setVerdict(ruleKey, verdict, worstProbeMs2) {
22383
23261
  failOpenTransaction(this.db, () => {
22384
23262
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
@@ -22433,7 +23311,39 @@ var SqliteScanLedgerRepository = class {
22433
23311
  };
22434
23312
 
22435
23313
  // ../../packages/persistence/src/repositories/secret-vault.ts
22436
- import { randomUUID as randomUUID7 } from "crypto";
23314
+ import { randomUUID as randomUUID8 } from "crypto";
23315
+ function pageLimit(requested, fallback) {
23316
+ if (requested === void 0) return fallback;
23317
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23318
+ }
23319
+ function encodeReuseCursor(payload) {
23320
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23321
+ }
23322
+ function decodeReuseCursor(cursor) {
23323
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23324
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23325
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23326
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23327
+ // malformed cursor must never produce, since restarting from the top is the
23328
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23329
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23330
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23331
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23332
+ }
23333
+ return null;
23334
+ }
23335
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23336
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23337
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23338
+ v.occurrence_count, v.first_seen, v.last_seen`;
23339
+ function toSighting(row) {
23340
+ return {
23341
+ location: row.location,
23342
+ kind: row.kind,
23343
+ firstSeen: new Date(row.first_seen).toISOString(),
23344
+ lastSeen: new Date(row.last_seen).toISOString()
23345
+ };
23346
+ }
22437
23347
  var SELECT_COLUMNS = `
22438
23348
  pointer_id AS pointerId,
22439
23349
  value_fingerprint AS valueFingerprint,
@@ -22617,39 +23527,67 @@ var SqliteSecretVaultRepository = class {
22617
23527
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
22618
23528
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22619
23529
  ).run({
22620
- id: randomUUID7(),
23530
+ id: randomUUID8(),
22621
23531
  pointerId: entry.pointerId,
22622
23532
  location: entry.location,
22623
23533
  kind: entry.kind,
22624
23534
  now
22625
23535
  });
22626
23536
  }
22627
- listSightings(pointerId) {
23537
+ /**
23538
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23539
+ * than one query per row. A pointer with no sightings still gets an entry, so
23540
+ * the caller never has to distinguish "none" from "missing".
23541
+ *
23542
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23543
+ * the instance the way the fixed-shape ones in the constructor are.
23544
+ */
23545
+ sightingsFor(pointerIds) {
23546
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23547
+ if (pointerIds.length === 0) return byPointer;
22628
23548
  const rows = allRows(
22629
23549
  this.db.prepare(
22630
- `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22631
- WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
23550
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23551
+ FROM secret_vault_sighting
23552
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23553
+ ORDER BY last_seen DESC`
22632
23554
  ),
22633
- { pointerId }
23555
+ pointerIds
22634
23556
  );
23557
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23558
+ return byPointer;
23559
+ }
23560
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23561
+ toInventoryEntries(rows) {
23562
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
22635
23563
  return rows.map((r) => ({
22636
- location: r.location,
22637
- kind: r.kind,
23564
+ pointerId: r.pointer_id,
23565
+ category: r.category,
23566
+ ...r.provider === null ? {} : { provider: r.provider },
23567
+ maskedMatch: r.masked_match,
23568
+ occurrences: r.occurrence_count,
22638
23569
  firstSeen: new Date(r.first_seen).toISOString(),
22639
- lastSeen: new Date(r.last_seen).toISOString()
23570
+ lastSeen: new Date(r.last_seen).toISOString(),
23571
+ revealGrantId: r.grant_id,
23572
+ sightings: sightings.get(r.pointer_id) ?? []
22640
23573
  }));
22641
23574
  }
22642
23575
  /**
22643
- * The dashboard inventory: every vaulted value's descriptor data joined with
22644
- * its sightings and the active reveal-to-model grant when one exists.
22645
- * Raw-free by construction — neither the fingerprint nor the ciphertext
22646
- * columns are selected.
23576
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23577
+ * value's descriptor data joined with its sightings and the active
23578
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23579
+ * the fingerprint nor the ciphertext columns are selected.
23580
+ *
23581
+ * `totals.values` counts the whole store, not the page, so the count a reader
23582
+ * sees never depends on how far they have paged.
22647
23583
  */
22648
- listInventory(now = Date.now()) {
23584
+ listInventory(query = {}, now = Date.now()) {
23585
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23586
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23587
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
22649
23588
  const rows = allRows(
22650
23589
  this.db.prepare(
22651
- `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22652
- v.occurrence_count, v.first_seen, v.last_seen,
23590
+ `SELECT ${INVENTORY_COLUMNS},
22653
23591
  (SELECT e.id FROM exceptions e
22654
23592
  WHERE e.rule_id = v.rule_id
22655
23593
  AND e.value_fingerprint = v.value_fingerprint
@@ -22657,45 +23595,109 @@ var SqliteSecretVaultRepository = class {
22657
23595
  AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22658
23596
  LIMIT 1) AS grant_id
22659
23597
  FROM secret_vault v
22660
- ORDER BY v.last_seen DESC`
23598
+ ${where}
23599
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23600
+ LIMIT :limit`
22661
23601
  ),
22662
- { now }
23602
+ bindParams({
23603
+ now,
23604
+ limit: limit + 1,
23605
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23606
+ })
22663
23607
  );
22664
- return rows.map((r) => ({
22665
- pointerId: r.pointer_id,
22666
- category: r.category,
22667
- ...r.provider === null ? {} : { provider: r.provider },
22668
- maskedMatch: r.masked_match,
22669
- occurrences: r.occurrence_count,
22670
- firstSeen: new Date(r.first_seen).toISOString(),
22671
- lastSeen: new Date(r.last_seen).toISOString(),
22672
- revealGrantId: r.grant_id,
22673
- sightings: this.listSightings(r.pointer_id)
22674
- }));
23608
+ const hasMore = rows.length > limit;
23609
+ const page = hasMore ? rows.slice(0, limit) : rows;
23610
+ const last = page[page.length - 1];
23611
+ return {
23612
+ totals: { values: this.countEntries() },
23613
+ items: this.toInventoryEntries(page),
23614
+ // Minted from the last row of the PAGE, never the extra probe row.
23615
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23616
+ };
23617
+ }
23618
+ /**
23619
+ * Values reused on this machine — detected more than once, or written to more
23620
+ * than one location — most-reused first, one page at a time.
23621
+ *
23622
+ * Its own read rather than a filter over an inventory page: reuse is a
23623
+ * property of the whole store, and deriving it from 50 newest rows would
23624
+ * under-report exactly the values a reader most needs to see.
23625
+ */
23626
+ listReuse(query = {}, now = Date.now()) {
23627
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23628
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23629
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23630
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23631
+ const rows = allRows(
23632
+ this.db.prepare(
23633
+ `SELECT ${INVENTORY_COLUMNS},
23634
+ (SELECT e.id FROM exceptions e
23635
+ WHERE e.rule_id = v.rule_id
23636
+ AND e.value_fingerprint = v.value_fingerprint
23637
+ AND e.key_version = v.fingerprint_key_version
23638
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23639
+ LIMIT 1) AS grant_id
23640
+ FROM secret_vault v
23641
+ WHERE ${REUSED_PREDICATE} ${after}
23642
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23643
+ LIMIT :limit`
23644
+ ),
23645
+ bindParams({
23646
+ now,
23647
+ limit: limit + 1,
23648
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23649
+ })
23650
+ );
23651
+ const hasMore = rows.length > limit;
23652
+ const page = hasMore ? rows.slice(0, limit) : rows;
23653
+ const last = page[page.length - 1];
23654
+ return {
23655
+ totals: { reused: this.countReused() },
23656
+ items: this.toInventoryEntries(page),
23657
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23658
+ };
22675
23659
  }
22676
23660
  /**
22677
- * The de-reference trail, newest first. By default the batched, high-volume
22678
- * reasons (display, view-render) are hidden and counted instead — the rows
22679
- * that matter as a signal are the model crossings, and burying them under
22680
- * render noise would defeat the audit's purpose.
23661
+ * The de-reference trail, newest first, one page at a time. By default the
23662
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23663
+ * instead — the rows that matter as a signal are the model crossings, and
23664
+ * burying them under render noise would defeat the audit's purpose.
23665
+ *
23666
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23667
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23668
+ * the reader pages.
22681
23669
  */
22682
- listDerefs(opts) {
22683
- const limit = opts?.limit ?? 200;
22684
- const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
23670
+ listDerefs(query = {}) {
23671
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23672
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23673
+ const conditions = [];
23674
+ if (query.includeBatched !== true) {
23675
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23676
+ }
23677
+ if (cursor !== null) {
23678
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23679
+ }
23680
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
22685
23681
  const rows = allRows(
22686
23682
  this.db.prepare(
22687
23683
  `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22688
23684
  FROM secret_vault_deref ${where}
22689
- ORDER BY at DESC, rowid DESC LIMIT :limit`
23685
+ ORDER BY at DESC, id DESC LIMIT :limit`
22690
23686
  ),
22691
- { limit }
23687
+ bindParams({
23688
+ limit: limit + 1,
23689
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23690
+ })
22692
23691
  );
22693
- const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
23692
+ const hasMore = rows.length > limit;
23693
+ const page = hasMore ? rows.slice(0, limit) : rows;
23694
+ const last = page[page.length - 1];
23695
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
22694
23696
  this.db,
22695
23697
  `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22696
23698
  );
22697
23699
  return {
22698
- rows: rows.map((r) => ({
23700
+ items: page.map((r) => ({
22699
23701
  id: r.id,
22700
23702
  pointerId: r.pointer_id,
22701
23703
  at: new Date(r.at).toISOString(),
@@ -22705,12 +23707,20 @@ var SqliteSecretVaultRepository = class {
22705
23707
  ...r.grant_id === null ? {} : { grantId: r.grant_id },
22706
23708
  pointerCount: r.pointer_count
22707
23709
  })),
23710
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
22708
23711
  hiddenBatched
22709
23712
  };
22710
23713
  }
22711
23714
  countEntries() {
22712
23715
  return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22713
23716
  }
23717
+ /** Values reused on this machine — the reuse list's page-independent total. */
23718
+ countReused() {
23719
+ return countScalar(
23720
+ this.db,
23721
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23722
+ );
23723
+ }
22714
23724
  };
22715
23725
 
22716
23726
  // ../../packages/persistence/src/repositories/security.ts
@@ -22725,7 +23735,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22725
23735
  var SCAN_COVERAGE = [
22726
23736
  { provider: "claudecode", coverage: 100, supported: true },
22727
23737
  { provider: "cursor", coverage: 0, supported: false },
22728
- { provider: "codex", coverage: 0, supported: false },
23738
+ { provider: "codex", coverage: 80, supported: true },
23739
+ { provider: "antigravity", coverage: 60, supported: true },
23740
+ { provider: "claudeai", coverage: 0, supported: false },
22729
23741
  { provider: "chatgpt", coverage: 0, supported: false },
22730
23742
  { provider: "copilot", coverage: 0, supported: false },
22731
23743
  { provider: "api", coverage: 0, supported: false }
@@ -23058,7 +24070,7 @@ var SqliteSecurityRepository = class {
23058
24070
  };
23059
24071
 
23060
24072
  // ../../packages/persistence/src/repositories/shares.ts
23061
- import { randomUUID as randomUUID8 } from "crypto";
24073
+ import { randomUUID as randomUUID9 } from "crypto";
23062
24074
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
23063
24075
  var IN_CHUNK = 500;
23064
24076
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -23314,7 +24326,7 @@ var SqliteSharesRepository = class {
23314
24326
  (id, destination_id, host, decision, created_at, updated_at)
23315
24327
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
23316
24328
  ).run({
23317
- id: randomUUID8(),
24329
+ id: randomUUID9(),
23318
24330
  destinationId,
23319
24331
  host: dest.host,
23320
24332
  decision,
@@ -23463,7 +24475,7 @@ var SqliteSharesRepository = class {
23463
24475
  let destinationId = destIds.get(hit.host);
23464
24476
  if (destinationId === void 0) {
23465
24477
  destStmt.run({
23466
- id: randomUUID8(),
24478
+ id: randomUUID9(),
23467
24479
  kind: hit.kind,
23468
24480
  name: hit.name,
23469
24481
  host: hit.host,
@@ -23479,7 +24491,7 @@ var SqliteSharesRepository = class {
23479
24491
  let endpointId = endpointIds.get(endpointKey);
23480
24492
  if (endpointId === void 0) {
23481
24493
  endpointStmt.run({
23482
- id: randomUUID8(),
24494
+ id: randomUUID9(),
23483
24495
  destinationId,
23484
24496
  method: hit.method,
23485
24497
  transport: hit.transport,
@@ -23492,7 +24504,7 @@ var SqliteSharesRepository = class {
23492
24504
  endpointIds.set(endpointKey, endpointId);
23493
24505
  }
23494
24506
  siteStmt.run({
23495
- id: randomUUID8(),
24507
+ id: randomUUID9(),
23496
24508
  endpointId,
23497
24509
  project: input.project,
23498
24510
  projectKey: input.projectKey,
@@ -23857,6 +24869,9 @@ function purgeSampleData(db) {
23857
24869
  }
23858
24870
 
23859
24871
  // ../../packages/persistence/src/database.ts
24872
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24873
+ "aka.persistence.unsafeTestOnlyRawHandle"
24874
+ );
23860
24875
  function linkHost(input, hostId) {
23861
24876
  return hostId ? { ...input, hostId } : input;
23862
24877
  }
@@ -23878,21 +24893,34 @@ function openWithPragmas(file2) {
23878
24893
  }
23879
24894
  return db;
23880
24895
  }
23881
- function backupLegacyStore(file2) {
23882
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23883
- renameSync2(file2, backup);
23884
- tightenFile(backup);
23885
- for (const sidecar of dbSidecars(file2)) {
23886
- if (existsSync(sidecar)) rmSync2(sidecar);
24896
+ function backupLegacyStore(db, file2) {
24897
+ reapStalePartials(file2);
24898
+ const backup = backupPath(file2, "legacy");
24899
+ let snapshotted = false;
24900
+ let snapshotError;
24901
+ try {
24902
+ snapshotStore(db, backup);
24903
+ snapshotted = true;
24904
+ } catch (error51) {
24905
+ snapshotError = error51;
24906
+ } finally {
24907
+ db.close();
24908
+ }
24909
+ if (!snapshotted) {
24910
+ akaWarn(
24911
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24912
+ );
24913
+ moveStoreAside(file2, backup);
24914
+ return backup;
23887
24915
  }
24916
+ discardStore(file2, backup);
23888
24917
  return backup;
23889
24918
  }
23890
24919
  function openAndInitialize(file2) {
23891
24920
  let db = openWithPragmas(file2);
23892
24921
  try {
23893
24922
  if (isForeignSqliteLineage(db)) {
23894
- db.close();
23895
- const backup = backupLegacyStore(file2);
24923
+ const backup = backupLegacyStore(db, file2);
23896
24924
  db = openWithPragmas(file2);
23897
24925
  akaWarn(
23898
24926
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23936,7 +24964,7 @@ function openAndInitialize(file2) {
23936
24964
  }
23937
24965
  function openLocalDatabase(dir) {
23938
24966
  ensureDataDirSync(dir);
23939
- const file2 = join(dir, DB_FILENAME);
24967
+ const file2 = join2(dir, DB_FILENAME);
23940
24968
  const {
23941
24969
  db,
23942
24970
  events,
@@ -24053,7 +25081,7 @@ function openLocalDatabase(dir) {
24053
25081
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
24054
25082
  if (!definitionId) continue;
24055
25083
  inspectionFindings.insertFinding({
24056
- id: randomUUID9(),
25084
+ id: randomUUID10(),
24057
25085
  auditEventId: record2.scanEvent.id,
24058
25086
  inspectionDefinitionId: definitionId,
24059
25087
  span: finding.span,
@@ -24159,7 +25187,9 @@ function openLocalDatabase(dir) {
24159
25187
  transaction,
24160
25188
  close: () => {
24161
25189
  db.close();
24162
- }
25190
+ },
25191
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25192
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
24163
25193
  };
24164
25194
  }
24165
25195
 
@@ -24183,6 +25213,20 @@ var UserGrantPolicyProvider = class {
24183
25213
  }
24184
25214
  };
24185
25215
 
25216
+ // ../../packages/persistence/src/file-lock.ts
25217
+ import { randomUUID as randomUUID11 } from "crypto";
25218
+ import {
25219
+ closeSync,
25220
+ existsSync as existsSync2,
25221
+ openSync,
25222
+ readFileSync,
25223
+ rmSync as rmSync3,
25224
+ statSync as statSync2,
25225
+ writeFileSync as writeFileSync2
25226
+ } from "fs";
25227
+ import { hostname as hostname3 } from "os";
25228
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25229
+
24186
25230
  // ../../packages/persistence/src/finding-key.ts
24187
25231
  import { createHash as createHash3 } from "crypto";
24188
25232
  function normalizeFilePath(filePath) {
@@ -24195,13 +25239,13 @@ function computeFindingKey(input) {
24195
25239
 
24196
25240
  // ../../packages/persistence/src/fingerprint.ts
24197
25241
  import { createHmac, randomBytes } from "crypto";
24198
- import { existsSync as existsSync2, readFileSync } from "fs";
24199
- import { join as join2 } from "path";
25242
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25243
+ import { join as join3 } from "path";
24200
25244
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
24201
- var KEY_FILENAME = "exception.key";
25245
+ var EXCEPTION_KEY_FILENAME = "exception.key";
24202
25246
  var KEY_MATERIAL_BYTES = 32;
24203
25247
  function keyFilePath(dataDir2) {
24204
- return join2(dataDir2, KEY_FILENAME);
25248
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
24205
25249
  }
24206
25250
  function parseKeyFile(raw) {
24207
25251
  const parsed = JSON.parse(raw);
@@ -24239,8 +25283,8 @@ var FloorUnreadableError = class extends Error {
24239
25283
  }
24240
25284
  };
24241
25285
  function storedKeyVersionFloor(dataDir2) {
24242
- const file2 = join2(dataDir2, DB_FILENAME);
24243
- if (!existsSync2(file2)) return 0;
25286
+ const file2 = join3(dataDir2, DB_FILENAME);
25287
+ if (!existsSync3(file2)) return 0;
24244
25288
  let db;
24245
25289
  try {
24246
25290
  db = new DatabaseSync2(file2, { readOnly: true });
@@ -24265,18 +25309,36 @@ function storedKeyVersionFloor(dataDir2) {
24265
25309
  db?.close();
24266
25310
  }
24267
25311
  }
24268
- function writeKeyFile(dataDir2, key) {
25312
+ function serializeKey(key) {
25313
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
25314
+ }
25315
+ function createKeyFile(dataDir2, key) {
24269
25316
  ensureDataDirSync(dataDir2);
24270
25317
  const file2 = keyFilePath(dataDir2);
24271
- const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
24272
- writeOwnerOnlyFileSync(file2, `${body}
24273
- `);
24274
- return key;
25318
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
25319
+ `)) return key;
25320
+ const winner = readFingerprintKey(dataDir2);
25321
+ if (winner) {
25322
+ tightenFile(file2);
25323
+ return winner;
25324
+ }
25325
+ const occupant = classifyOccupant(file2);
25326
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
25327
+ }
25328
+ function occupantMessage(file2, kind) {
25329
+ switch (kind) {
25330
+ case "symlink":
25331
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
25332
+ case "gone":
25333
+ return "exception key file was removed while it was being created";
25334
+ case "unknown":
25335
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
25336
+ }
24275
25337
  }
24276
25338
  function readFingerprintKey(dataDir2) {
24277
25339
  let raw;
24278
25340
  try {
24279
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25341
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
24280
25342
  } catch (err) {
24281
25343
  if (err.code === "ENOENT") return null;
24282
25344
  throw err instanceof Error ? err : new Error(String(err));
@@ -24289,7 +25351,7 @@ function loadOrCreateFingerprintKey(dataDir2) {
24289
25351
  tightenFile(keyFilePath(dataDir2));
24290
25352
  return existing;
24291
25353
  }
24292
- return writeKeyFile(dataDir2, {
25354
+ return createKeyFile(dataDir2, {
24293
25355
  version: storedKeyVersionFloor(dataDir2) + 1,
24294
25356
  material: randomBytes(KEY_MATERIAL_BYTES)
24295
25357
  });
@@ -24302,21 +25364,21 @@ function fingerprintValue(key, raw) {
24302
25364
  import { renameSync as renameSync3 } from "fs";
24303
25365
  import { mkdir } from "fs/promises";
24304
25366
  import { homedir } from "os";
24305
- import { join as join3 } from "path";
25367
+ import { join as join4 } from "path";
24306
25368
  function defaultDataDir() {
24307
- return join3(homedir(), ".aka");
25369
+ return join4(homedir(), ".aka");
24308
25370
  }
24309
25371
  function settingsDir(base = defaultDataDir()) {
24310
- return join3(base, "settings");
25372
+ return join4(base, "settings");
24311
25373
  }
24312
25374
  function dataDir(base = defaultDataDir()) {
24313
- return join3(base, "data");
25375
+ return join4(base, "data");
24314
25376
  }
24315
25377
  function dbPath(base = defaultDataDir()) {
24316
- return join3(dataDir(base), "aka.db");
25378
+ return join4(dataDir(base), "aka.db");
24317
25379
  }
24318
25380
  function keysDir(base = defaultDataDir()) {
24319
- return join3(base, "keys");
25381
+ return join4(base, "keys");
24320
25382
  }
24321
25383
  function ensureLayoutDirSync(dir = defaultDataDir()) {
24322
25384
  ensureDataDirSync(dir);
@@ -24329,8 +25391,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24329
25391
  for (const { name, dest } of moves) {
24330
25392
  try {
24331
25393
  ensureDataDirSync(dest);
24332
- const moved = join3(dest, name);
24333
- renameSync3(join3(base, name), moved);
25394
+ const moved = join4(dest, name);
25395
+ renameSync3(join4(base, name), moved);
24334
25396
  tightenFile(moved);
24335
25397
  } catch {
24336
25398
  }
@@ -24338,10 +25400,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
24338
25400
  }
24339
25401
 
24340
25402
  // ../../packages/persistence/src/settings.ts
24341
- import { readFileSync as readFileSync2 } from "fs";
24342
- import { join as join4 } from "path";
25403
+ import { readFileSync as readFileSync3 } from "fs";
25404
+ import { join as join5 } from "path";
25405
+ var SETTINGS_FILENAME = "settings.json";
24343
25406
  function readWorkspaceSettings(base = defaultDataDir()) {
24344
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25407
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
24345
25408
  if (!record2) return defaultWorkspaceSettings();
24346
25409
  try {
24347
25410
  return WorkspaceSettings.parse(record2);
@@ -24352,7 +25415,7 @@ function readWorkspaceSettings(base = defaultDataDir()) {
24352
25415
  function readJson(file2) {
24353
25416
  let text;
24354
25417
  try {
24355
- text = readFileSync2(file2, "utf8");
25418
+ text = readFileSync3(file2, "utf8");
24356
25419
  } catch {
24357
25420
  return null;
24358
25421
  }
@@ -24473,13 +25536,18 @@ import { randomBytes as randomBytes2 } from "crypto";
24473
25536
  import {
24474
25537
  chmodSync as chmodSync2,
24475
25538
  mkdirSync as mkdirSync2,
24476
- readFileSync as readFileSync3,
25539
+ readFileSync as readFileSync4,
24477
25540
  renameSync as renameSync4,
24478
- rmSync as rmSync3,
24479
- statSync,
24480
- writeFileSync as writeFileSync2
25541
+ rmSync as rmSync4,
25542
+ statSync as statSync3,
25543
+ writeFileSync as writeFileSync3
24481
25544
  } from "fs";
24482
- import { join as join5 } from "path";
25545
+ import { join as join6 } from "path";
25546
+ var VAULT_OCCUPANT_REASON = {
25547
+ symlink: "the path is a symlink; remove it so a keyring can be created",
25548
+ gone: "the path was occupied but holds no keyring (removed while it was being created)",
25549
+ unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
25550
+ };
24483
25551
  var VaultKeyEpochMissingError = class extends Error {
24484
25552
  version;
24485
25553
  constructor(version2) {
@@ -24572,28 +25640,28 @@ function claimRotationLock(lock, owner) {
24572
25640
  throw asError(err);
24573
25641
  }
24574
25642
  try {
24575
- writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
25643
+ writeFileSync3(join6(lock, LOCK_OWNER_FILE), `${owner}
24576
25644
  `, { mode: DATA_FILE_MODE });
24577
25645
  return true;
24578
25646
  } catch (err) {
24579
- rmSync3(lock, { recursive: true, force: true });
25647
+ rmSync4(lock, { recursive: true, force: true });
24580
25648
  throw asError(err);
24581
25649
  }
24582
25650
  }
24583
25651
  function acquireRotationLock(keysDir2) {
24584
- const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
25652
+ const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
24585
25653
  const owner = randomBytes2(16).toString("hex");
24586
25654
  if (claimRotationLock(lock, owner)) return { lock, owner };
24587
25655
  let held;
24588
25656
  try {
24589
- held = statSync(lock);
25657
+ held = statSync3(lock);
24590
25658
  } catch {
24591
25659
  throw new Error(ROTATION_IN_PROGRESS);
24592
25660
  }
24593
25661
  if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
24594
25662
  const aside = `${lock}.stale.${owner}`;
24595
25663
  try {
24596
- const now = statSync(lock);
25664
+ const now = statSync3(lock);
24597
25665
  if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
24598
25666
  throw new Error(ROTATION_IN_PROGRESS);
24599
25667
  }
@@ -24602,17 +25670,17 @@ function acquireRotationLock(keysDir2) {
24602
25670
  if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
24603
25671
  throw new Error(ROTATION_IN_PROGRESS, { cause: err });
24604
25672
  }
24605
- rmSync3(aside, { recursive: true, force: true });
25673
+ rmSync4(aside, { recursive: true, force: true });
24606
25674
  if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
24607
25675
  return { lock, owner };
24608
25676
  }
24609
25677
  function releaseRotationLock(lease) {
24610
25678
  try {
24611
- if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
25679
+ if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
24612
25680
  } catch {
24613
25681
  return;
24614
25682
  }
24615
- rmSync3(lease.lock, { recursive: true, force: true });
25683
+ rmSync4(lease.lock, { recursive: true, force: true });
24616
25684
  }
24617
25685
  function withRotationLock(keysDir2, work) {
24618
25686
  ensureDataDirSync(keysDir2);
@@ -24629,7 +25697,7 @@ var FileKeyProvider = class {
24629
25697
  this.#keysDir = keysDir2;
24630
25698
  }
24631
25699
  get filePath() {
24632
- return join5(this.#keysDir, VAULT_KEY_FILENAME);
25700
+ return join6(this.#keysDir, VAULT_KEY_FILENAME);
24633
25701
  }
24634
25702
  loadOrCreate() {
24635
25703
  return asAsync(() => {
@@ -24659,7 +25727,7 @@ var FileKeyProvider = class {
24659
25727
  #read() {
24660
25728
  let raw;
24661
25729
  try {
24662
- raw = readFileSync3(this.filePath, "utf8");
25730
+ raw = readFileSync4(this.filePath, "utf8");
24663
25731
  } catch (err) {
24664
25732
  if (err.code === "ENOENT") return null;
24665
25733
  throw err instanceof Error ? err : new Error(String(err));
@@ -24667,34 +25735,32 @@ var FileKeyProvider = class {
24667
25735
  return parseKeyring(raw);
24668
25736
  }
24669
25737
  /**
24670
- * First mint: the keyring is created at its FINAL path with a
24671
- * creation-exclusive write, so two processes racing a fresh machine cannot
24672
- * each mint a different epoch 1 with tmp + rename the loser's replace
24673
- * would orphan everything the winner had already sealed. On EEXIST the
24674
- * loser re-reads and adopts the winner's keyring; it minted nothing.
24675
- * Atomic replace is unnecessary here: nothing can be mid-read of a file
24676
- * that did not exist, and a torn exclusive write parses as corrupt on the
24677
- * next read and fails secure rather than being re-minted over.
25738
+ * First mint: the keyring is CREATED, never replaced, so two processes racing
25739
+ * a fresh machine cannot each mint a different epoch 1 — with tmp + rename
25740
+ * the loser's replace would orphan everything the winner had already sealed.
25741
+ * The loser re-reads and adopts the winner's keyring; it minted nothing.
25742
+ *
25743
+ * `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
25744
+ * at the final path, so the keyring never exists at zero length: a reader —
25745
+ * including the loser, re-reading in order to adopt sees the file absent or
25746
+ * whole, and never mistakes a live keyring for a corrupt one. A corrupt file
25747
+ * still throws from the parse and is never re-minted over.
24678
25748
  */
24679
25749
  #createExclusive() {
24680
25750
  ensureDataDirSync(this.#keysDir);
24681
25751
  const keyring = mintKeyring();
24682
- try {
24683
- writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
24684
- `, {
24685
- flag: "wx",
24686
- mode: DATA_FILE_MODE
24687
- });
24688
- } catch (err) {
24689
- if (err.code !== "EEXIST") throw asError(err);
24690
- const winner = this.#read();
24691
- if (!winner) {
24692
- throw new Error("vault: key file vanished during first mint", { cause: err });
24693
- }
24694
- return winner;
25752
+ if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
25753
+ `)) return keyring;
25754
+ const winner = this.#read();
25755
+ if (!winner) {
25756
+ const occupant = classifyOccupant(this.filePath);
25757
+ throw new KeyUnclaimableError(
25758
+ `vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
25759
+ occupant.cause
25760
+ );
24695
25761
  }
24696
25762
  tightenFileMode(this.filePath);
24697
- return keyring;
25763
+ return winner;
24698
25764
  }
24699
25765
  /**
24700
25766
  * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
@@ -24705,7 +25771,7 @@ var FileKeyProvider = class {
24705
25771
  ensureDataDirSync(this.#keysDir);
24706
25772
  const file2 = this.filePath;
24707
25773
  const tmp = `${file2}.tmp`;
24708
- writeFileSync2(tmp, `${serializeKeyring(keyring)}
25774
+ writeFileSync3(tmp, `${serializeKeyring(keyring)}
24709
25775
  `, { mode: DATA_FILE_MODE });
24710
25776
  renameSync4(tmp, file2);
24711
25777
  tightenFileMode(file2);
@@ -24831,7 +25897,7 @@ function createKeyProvider(custody, keysDir2) {
24831
25897
  }
24832
25898
 
24833
25899
  // ../../packages/persistence/src/vault/vault.ts
24834
- import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
25900
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
24835
25901
  var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
24836
25902
  var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
24837
25903
  var VAULT_PURGE_POINTER_ID = "*";
@@ -24858,14 +25924,12 @@ function parsePointer(token) {
24858
25924
  var SecretVault = class {
24859
25925
  #repo;
24860
25926
  #keys;
24861
- #fingerprintKey;
24862
25927
  #isConsented;
24863
25928
  #verifyGrant;
24864
25929
  #now;
24865
25930
  constructor(deps) {
24866
25931
  this.#repo = deps.repo;
24867
25932
  this.#keys = deps.keys;
24868
- this.#fingerprintKey = deps.fingerprintKey;
24869
25933
  this.#isConsented = deps.isConsented;
24870
25934
  this.#verifyGrant = deps.verifyGrant;
24871
25935
  this.#now = deps.now ?? (() => Date.now());
@@ -24874,10 +25938,23 @@ var SecretVault = class {
24874
25938
  * Store a value and return the pointer that stands for it. The same value
24875
25939
  * always yields the same pointer on this machine — one row, one pointer id,
24876
25940
  * one category — which is what makes dedup and reuse counting work.
25941
+ *
25942
+ * `fingerprintKey` is the exception-key epoch this value's fingerprint is
25943
+ * derived under — a different key from the vault's, with different rotation
25944
+ * semantics. It is a parameter of the WRITE rather than a constructor dep,
25945
+ * and a thunk rather than a value, so that the only way to reach a key is to
25946
+ * store something: a read-only caller never names it, and a caller whose
25947
+ * source mints on absence mints only once consent has actually opened the
25948
+ * write. `refreshFingerprints` takes its key the same way, for the same
25949
+ * reason.
25950
+ *
25951
+ * Resolved once per call, so the fingerprint and the version it is recorded
25952
+ * under can never come from two different epochs.
24877
25953
  */
24878
- async tokenize(raw, meta3) {
25954
+ async tokenize(raw, meta3, fingerprintKey) {
24879
25955
  if (!this.#isConsented()) return CONSENT_ABSENT;
24880
- const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
25956
+ const fpKey = fingerprintKey();
25957
+ const valueFingerprint = fingerprintValue(fpKey, raw);
24881
25958
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
24882
25959
  const now = this.#now();
24883
25960
  if (existing) {
@@ -24893,7 +25970,7 @@ var SecretVault = class {
24893
25970
  {
24894
25971
  pointerId: base32Encode(pointerId),
24895
25972
  valueFingerprint,
24896
- fingerprintKeyVersion: this.#fingerprintKey.version,
25973
+ fingerprintKeyVersion: fpKey.version,
24897
25974
  keyVersion: version2,
24898
25975
  // Recorded so the row stays OPENABLE if the wire-format constant ever
24899
25976
  // moves: it is part of this row's AEAD AAD. It is not a tag input —
@@ -25137,7 +26214,7 @@ var SecretVault = class {
25137
26214
  purgeVault() {
25138
26215
  const destroyed = this.#repo.purgeAll();
25139
26216
  this.#repo.recordDeref({
25140
- id: randomUUID10(),
26217
+ id: randomUUID12(),
25141
26218
  pointerId: VAULT_PURGE_POINTER_ID,
25142
26219
  at: this.#now(),
25143
26220
  target: "human",
@@ -25206,7 +26283,7 @@ var SecretVault = class {
25206
26283
  }
25207
26284
  #audit(pointerId, opts, outcome) {
25208
26285
  this.#repo.recordDeref({
25209
- id: randomUUID10(),
26286
+ id: randomUUID12(),
25210
26287
  pointerId,
25211
26288
  at: this.#now(),
25212
26289
  target: opts.target,
@@ -25221,15 +26298,15 @@ var SecretVault = class {
25221
26298
  };
25222
26299
 
25223
26300
  // ../../packages/persistence/src/warn-era-cap.ts
25224
- import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25225
- import { join as join6 } from "path";
26301
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
26302
+ import { join as join7 } from "path";
25226
26303
  var MARKER = "warn-era-capped";
25227
26304
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25228
26305
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25229
- const marker = join6(dataDir2, MARKER);
25230
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
26306
+ const marker = join7(dataDir2, MARKER);
26307
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
25231
26308
  const capped = db.policies.capCategoryActions();
25232
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
26309
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
25233
26310
  `, { mode: DATA_FILE_MODE });
25234
26311
  return { capped };
25235
26312
  }
@@ -25293,11 +26370,11 @@ function providerFromModelId(modelId) {
25293
26370
  }
25294
26371
 
25295
26372
  // ../../packages/plugin-sdk/src/config.ts
25296
- function loadConfig(base = defaultDataDir()) {
26373
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
25297
26374
  try {
25298
26375
  ensureLayoutDirSync(base);
25299
- const settingsFile = join7(settingsDir(base), "settings.json");
25300
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
26376
+ const settingsFile = join8(settingsDir(base), "settings.json");
26377
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
25301
26378
  } catch {
25302
26379
  }
25303
26380
  migrateLegacyLayout(base);
@@ -25308,21 +26385,21 @@ function loadConfig(base = defaultDataDir()) {
25308
26385
  dbPath: dbPath(base),
25309
26386
  settingsDir: settingsDir(base),
25310
26387
  onboarded: settings.onboardedAt != null,
25311
- provider: resolveProviderSafe()
26388
+ provider: resolveProviderSafe(resolveProviderFn)
25312
26389
  };
25313
26390
  }
25314
- function resolveProviderSafe() {
26391
+ function resolveProviderSafe(resolveProviderFn) {
25315
26392
  try {
25316
- return resolveProvider();
26393
+ return resolveProviderFn();
25317
26394
  } catch {
25318
26395
  return { provider: "anthropic" };
25319
26396
  }
25320
26397
  }
25321
26398
 
25322
26399
  // ../../packages/plugin-sdk/src/config-inventory.ts
25323
- import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
26400
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
25324
26401
  import { homedir as homedir2 } from "os";
25325
- import { basename as basename2, join as join9 } from "path";
26402
+ import { basename as basename3, join as join10 } from "path";
25326
26403
 
25327
26404
  // ../../packages/detections/src/egress/registry.ts
25328
26405
  var EXTRACTOR_VERSION = "1";
@@ -25896,6 +26973,40 @@ function escapeRegExp2(value) {
25896
26973
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25897
26974
  }
25898
26975
 
26976
+ // ../../packages/detections/src/regex-cache.ts
26977
+ var singles = /* @__PURE__ */ new WeakMap();
26978
+ var keywordLists = /* @__PURE__ */ new WeakMap();
26979
+ var labelLists = /* @__PURE__ */ new WeakMap();
26980
+ function listCache(kind) {
26981
+ return kind === "keyword" ? keywordLists : labelLists;
26982
+ }
26983
+ function memoizedRegExp(owner, build) {
26984
+ const cached2 = singles.get(owner);
26985
+ if (cached2 !== void 0) {
26986
+ cached2.lastIndex = 0;
26987
+ return cached2;
26988
+ }
26989
+ const compiled = build();
26990
+ singles.set(owner, compiled);
26991
+ return compiled;
26992
+ }
26993
+ function memoizedRegExpList(kind, owner, build) {
26994
+ const cache = listCache(kind);
26995
+ const cached2 = cache.get(owner);
26996
+ if (cached2 !== void 0) {
26997
+ if (cached2.stateful) {
26998
+ for (const re of cached2.entries) if (re !== void 0) re.lastIndex = 0;
26999
+ }
27000
+ return cached2.entries;
27001
+ }
27002
+ const entries = build();
27003
+ cache.set(owner, {
27004
+ entries,
27005
+ stateful: entries.some((re) => re !== void 0 && (re.global || re.sticky))
27006
+ });
27007
+ return entries;
27008
+ }
27009
+
25899
27010
  // ../../packages/detections/src/matchers/limits.ts
25900
27011
  var MAX_MATCHES_PER_RULE = 1e4;
25901
27012
  var MAX_REGEX_INPUT_LENGTH = 2e5;
@@ -25906,10 +27017,17 @@ var KeywordMatcher2 = class {
25906
27017
  if (rule.matcher.type !== "keyword") return [];
25907
27018
  const { keywords, caseSensitive } = rule.matcher;
25908
27019
  const spans = [];
25909
- for (const kw of keywords) {
25910
- if (kw.length === 0) continue;
27020
+ const compiled = memoizedRegExpList(
27021
+ "keyword",
27022
+ rule.matcher,
27023
+ () => keywords.map((kw) => {
27024
+ if (kw.length === 0) return void 0;
27025
+ return new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
27026
+ })
27027
+ );
27028
+ for (const re of compiled) {
27029
+ if (re === void 0) continue;
25911
27030
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
25912
- const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
25913
27031
  let m;
25914
27032
  while ((m = re.exec(text)) !== null) {
25915
27033
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -25925,7 +27043,10 @@ var RegexMatcher2 = class {
25925
27043
  match(text, rule) {
25926
27044
  if (rule.matcher.type !== "regex") return [];
25927
27045
  const { pattern, flags, captureGroup } = rule.matcher;
25928
- const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
27046
+ const re = memoizedRegExp(
27047
+ rule.matcher,
27048
+ () => new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`)
27049
+ );
25929
27050
  const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
25930
27051
  const spans = [];
25931
27052
  let m;
@@ -26033,11 +27154,15 @@ function isCorroborated(candidate, candidates, text) {
26033
27154
  const labels = req.labels;
26034
27155
  if (labels && labels.length > 0) {
26035
27156
  const haystack = text.slice(Math.max(0, winStart), winEnd);
26036
- for (const label of labels) {
26037
- const trimmed = label.trim();
26038
- if (trimmed.length === 0) continue;
26039
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
26040
- if (re.test(haystack)) return true;
27157
+ for (const re of memoizedRegExpList(
27158
+ "label",
27159
+ req,
27160
+ () => labels.map((label) => {
27161
+ const trimmed = label.trim();
27162
+ return trimmed.length === 0 ? void 0 : new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
27163
+ })
27164
+ )) {
27165
+ if (re?.test(haystack)) return true;
26041
27166
  }
26042
27167
  }
26043
27168
  return false;
@@ -26305,13 +27430,14 @@ function probesFor(rule) {
26305
27430
  const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
26306
27431
  return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
26307
27432
  }
26308
- function worstProbeMs(rule) {
27433
+ var wallClock = () => performance.now();
27434
+ function worstProbeMs(rule, now = wallClock) {
26309
27435
  let ms = 0;
26310
27436
  let probe = "";
26311
27437
  for (const text of probesFor(rule)) {
26312
- const start = performance.now();
27438
+ const start = now();
26313
27439
  scan(text, [rule]);
26314
- const elapsed = performance.now() - start;
27440
+ const elapsed = now() - start;
26315
27441
  if (elapsed > ms) {
26316
27442
  ms = elapsed;
26317
27443
  probe = text;
@@ -27979,7 +29105,7 @@ var gcp_service_account_default = {
27979
29105
  severity: "critical",
27980
29106
  matcher: {
27981
29107
  type: "regex",
27982
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
29108
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
27983
29109
  flags: "g"
27984
29110
  },
27985
29111
  examples: [
@@ -28400,8 +29526,8 @@ function scanText(text, ruleVersions) {
28400
29526
  }
28401
29527
 
28402
29528
  // ../../packages/plugin-sdk/src/repo.ts
28403
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28404
- import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
29529
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
29530
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
28405
29531
  function resolveRepoIdentity(cwd) {
28406
29532
  try {
28407
29533
  const root = findGitRoot(cwd);
@@ -28414,7 +29540,7 @@ function resolveRepoIdentity(cwd) {
28414
29540
  // win32) so the persistence layer's `/`-separated checkout-path patterns
28415
29541
  // (the ghost sweep + the read-side worktree filter) match it as written.
28416
29542
  url: url2 ?? headRoot.split(sep2).join("/"),
28417
- name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
29543
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
28418
29544
  };
28419
29545
  } catch {
28420
29546
  return void 0;
@@ -28434,36 +29560,36 @@ function resolveRepoNwo(cwd) {
28434
29560
  function findGitRoot(start) {
28435
29561
  let dir = start;
28436
29562
  for (; ; ) {
28437
- if (existsSync5(join8(dir, ".git"))) return dir;
28438
- const parent = dirname(dir);
29563
+ if (existsSync6(join9(dir, ".git"))) return dir;
29564
+ const parent = dirname2(dir);
28439
29565
  if (parent === dir) return void 0;
28440
29566
  dir = parent;
28441
29567
  }
28442
29568
  }
28443
29569
  function resolveGitContext(root) {
28444
- const dotGit = join8(root, ".git");
29570
+ const dotGit = join9(root, ".git");
28445
29571
  try {
28446
- if (statSync2(dotGit).isDirectory()) {
28447
- return { configPath: join8(dotGit, "config"), headRoot: root };
29572
+ if (statSync4(dotGit).isDirectory()) {
29573
+ return { configPath: join9(dotGit, "config"), headRoot: root };
28448
29574
  }
28449
29575
  } catch {
28450
29576
  return void 0;
28451
29577
  }
28452
29578
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
28453
29579
  if (!target) return void 0;
28454
- const gitdir = isAbsolute(target) ? target : join8(root, target);
28455
- if (existsSync5(join8(gitdir, "config"))) {
28456
- return { configPath: join8(gitdir, "config"), headRoot: root };
29580
+ const gitdir = isAbsolute(target) ? target : join9(root, target);
29581
+ if (existsSync6(join9(gitdir, "config"))) {
29582
+ return { configPath: join9(gitdir, "config"), headRoot: root };
28457
29583
  }
28458
- const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
29584
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
28459
29585
  if (!commonRaw) return void 0;
28460
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
28461
- const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
28462
- return { configPath: join8(commonGitDir, "config"), headRoot };
29586
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
29587
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
29588
+ return { configPath: join9(commonGitDir, "config"), headRoot };
28463
29589
  }
28464
29590
  function safeRead(path) {
28465
29591
  try {
28466
- return readFileSync4(path, "utf8");
29592
+ return readFileSync5(path, "utf8");
28467
29593
  } catch {
28468
29594
  return void 0;
28469
29595
  }
@@ -28514,13 +29640,13 @@ function nwoFromUrl(url2) {
28514
29640
  }
28515
29641
 
28516
29642
  // ../../packages/plugin-sdk/src/events.ts
28517
- import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
29643
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28518
29644
  function contentHashOf(text) {
28519
29645
  return createHash4("sha256").update(text).digest("hex");
28520
29646
  }
28521
29647
  function buildIngestEvent(input) {
28522
29648
  return {
28523
- id: randomUUID11(),
29649
+ id: randomUUID13(),
28524
29650
  sourceTool: input.sourceTool,
28525
29651
  kind: input.kind,
28526
29652
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -28531,21 +29657,473 @@ function buildIngestEvent(input) {
28531
29657
  // SDK boot in the fail-open hook path). Preserve any id the caller already set.
28532
29658
  metadata: {
28533
29659
  ...input.metadata,
28534
- correlationId: input.metadata?.correlationId ?? randomUUID11()
29660
+ correlationId: input.metadata?.correlationId ?? randomUUID13()
29661
+ }
29662
+ };
29663
+ }
29664
+
29665
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
29666
+ import { existsSync as existsSync7 } from "fs";
29667
+ import { fileURLToPath } from "url";
29668
+ import { Worker } from "worker_threads";
29669
+ var ISOLATED_SCAN_BUDGET_MS = 2e3;
29670
+ var ISOLATED_PROBE_BUDGET_MS = 1e3;
29671
+ var ISOLATED_START_BUDGET_MS = 5e3;
29672
+ var ATTRIBUTION_MIN_RULE_MS = 500;
29673
+ var ATTRIBUTION_MIN_SHARE = 0.5;
29674
+ var resolvedWorkerUrl;
29675
+ function resolveWorkerUrl() {
29676
+ if (resolvedWorkerUrl !== void 0) return resolvedWorkerUrl ?? void 0;
29677
+ for (const name of ["scan-worker.js", "scan-worker.ts"]) {
29678
+ const candidate = new URL(name, import.meta.url);
29679
+ try {
29680
+ if (existsSync7(fileURLToPath(candidate))) {
29681
+ resolvedWorkerUrl = candidate;
29682
+ return candidate;
29683
+ }
29684
+ } catch {
29685
+ }
29686
+ }
29687
+ resolvedWorkerUrl = null;
29688
+ return void 0;
29689
+ }
29690
+ function messageOf(error51) {
29691
+ return error51 instanceof Error ? error51.message : String(error51);
29692
+ }
29693
+ function createIsolatedScanner(data, opts = {}) {
29694
+ const budgetMs = opts.budgetMs ?? ISOLATED_SCAN_BUDGET_MS;
29695
+ const probeBudgetMs = opts.probeBudgetMs ?? ISOLATED_PROBE_BUDGET_MS;
29696
+ const startBudgetMs = opts.startBudgetMs ?? ISOLATED_START_BUDGET_MS;
29697
+ const minAttributionMs = opts.minAttributionMs ?? ATTRIBUTION_MIN_RULE_MS;
29698
+ let worker;
29699
+ let readyWorker;
29700
+ let broken;
29701
+ let closed = false;
29702
+ let nextJobId = 1;
29703
+ let pending;
29704
+ const terminating = /* @__PURE__ */ new Set();
29705
+ let chain = Promise.resolve();
29706
+ function clearTimers(job) {
29707
+ if (job.startupTimer !== void 0) clearTimeout(job.startupTimer);
29708
+ if (job.timer !== void 0) clearTimeout(job.timer);
29709
+ }
29710
+ function take() {
29711
+ const job = pending;
29712
+ if (!job) return void 0;
29713
+ pending = void 0;
29714
+ clearTimers(job);
29715
+ worker?.unref();
29716
+ return job;
29717
+ }
29718
+ function failPending(outcome) {
29719
+ take()?.fail(outcome);
29720
+ }
29721
+ function kill(dead) {
29722
+ if (worker === dead) worker = void 0;
29723
+ if (readyWorker === dead) readyWorker = void 0;
29724
+ const done = dead.terminate().catch(() => void 0);
29725
+ terminating.add(done);
29726
+ void done.finally(() => terminating.delete(done));
29727
+ }
29728
+ function onDeadline(job) {
29729
+ if (pending !== job) return;
29730
+ const now = performance.now();
29731
+ const runningMs = now - job.progressAt;
29732
+ const elapsedMs = now - job.startedAt;
29733
+ const blamed = job.progressIndex >= 0 && runningMs >= minAttributionMs && runningMs >= elapsedMs * ATTRIBUTION_MIN_SHARE;
29734
+ const culpritIndex = blamed ? job.progressIndex : void 0;
29735
+ kill(job.worker);
29736
+ failPending({ status: "timeout", culpritIndex, elapsedMs });
29737
+ }
29738
+ function ensureWorker() {
29739
+ if (worker) return worker;
29740
+ const url2 = opts.workerUrl ?? resolveWorkerUrl();
29741
+ if (!url2) {
29742
+ return {
29743
+ error: "the scan worker script was not found next to this bundle"
29744
+ };
29745
+ }
29746
+ let started;
29747
+ try {
29748
+ started = new Worker(url2, { workerData: data });
29749
+ } catch (error51) {
29750
+ return { error: `could not start the scan worker: ${messageOf(error51)}` };
29751
+ }
29752
+ opts.onWorkerStart?.(started.threadId);
29753
+ started.on("message", (message) => {
29754
+ if (worker !== started) return;
29755
+ if (message.kind === "ready") {
29756
+ readyWorker = started;
29757
+ if (pending?.worker === started) beginDeadline(pending);
29758
+ return;
29759
+ }
29760
+ if (message.kind === "progress") {
29761
+ if (pending?.worker === started) {
29762
+ pending.progressIndex = message.index;
29763
+ pending.progressAt = performance.now();
29764
+ }
29765
+ return;
29766
+ }
29767
+ if (pending?.id !== message.id) return;
29768
+ if (message.kind === "failed") {
29769
+ failPending({
29770
+ status: "unavailable",
29771
+ reason: `the scan worker failed: ${message.message}`
29772
+ });
29773
+ return;
29774
+ }
29775
+ const job = take();
29776
+ if (job && !job.reply(message)) {
29777
+ job.fail({ status: "unavailable", reason: "the scan worker answered the wrong job" });
29778
+ }
29779
+ });
29780
+ started.on("error", (error51) => {
29781
+ if (worker !== started) return;
29782
+ broken = messageOf(error51);
29783
+ worker = void 0;
29784
+ if (readyWorker === started) readyWorker = void 0;
29785
+ failPending({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29786
+ });
29787
+ started.on("exit", () => {
29788
+ if (worker !== started) return;
29789
+ broken ??= "the scan worker exited before answering";
29790
+ worker = void 0;
29791
+ if (readyWorker === started) readyWorker = void 0;
29792
+ failPending({ status: "unavailable", reason: "the scan worker exited before answering" });
29793
+ });
29794
+ started.unref();
29795
+ worker = started;
29796
+ return started;
29797
+ }
29798
+ function beginDeadline(job) {
29799
+ if (job.startupTimer !== void 0) {
29800
+ clearTimeout(job.startupTimer);
29801
+ job.startupTimer = void 0;
29802
+ }
29803
+ if (job.timer !== void 0) return;
29804
+ job.startedAt = performance.now();
29805
+ job.progressAt = job.startedAt;
29806
+ job.timer = setTimeout(() => {
29807
+ onDeadline(job);
29808
+ }, job.budgetMs);
29809
+ }
29810
+ function runOne(spec, fail) {
29811
+ if (closed) {
29812
+ fail({ status: "unavailable", reason: "the scan worker is closed" });
29813
+ return;
29814
+ }
29815
+ if (broken !== void 0) {
29816
+ fail({ status: "unavailable", reason: `the scan worker crashed: ${broken}` });
29817
+ return;
29818
+ }
29819
+ const started = ensureWorker();
29820
+ if (!(started instanceof Worker)) {
29821
+ broken = started.error;
29822
+ fail({ status: "unavailable", reason: started.error });
29823
+ return;
29824
+ }
29825
+ const id = nextJobId++;
29826
+ const now = performance.now();
29827
+ const job = {
29828
+ id,
29829
+ worker: started,
29830
+ budgetMs: spec.budgetMs,
29831
+ startedAt: now,
29832
+ progressIndex: -1,
29833
+ progressAt: now,
29834
+ startupTimer: void 0,
29835
+ timer: void 0,
29836
+ reply: spec.reply,
29837
+ fail
29838
+ };
29839
+ pending = job;
29840
+ started.ref();
29841
+ if (readyWorker === started) {
29842
+ beginDeadline(job);
29843
+ } else {
29844
+ job.startupTimer = setTimeout(() => {
29845
+ if (pending !== job) return;
29846
+ kill(job.worker);
29847
+ failPending({
29848
+ status: "unavailable",
29849
+ reason: `the scan worker did not start within ${String(startBudgetMs)}ms`
29850
+ });
29851
+ }, startBudgetMs);
29852
+ }
29853
+ try {
29854
+ started.postMessage(spec.build(id));
29855
+ } catch (error51) {
29856
+ failPending({
29857
+ // The thread went away between the ref and the post.
29858
+ status: "unavailable",
29859
+ reason: `could not reach the scan worker: ${messageOf(error51)}`
29860
+ });
29861
+ }
29862
+ }
29863
+ function enqueue(spec) {
29864
+ const next = chain.then(
29865
+ () => new Promise((resolve2) => {
29866
+ spec(resolve2);
29867
+ })
29868
+ );
29869
+ chain = next.then(
29870
+ () => void 0,
29871
+ () => void 0
29872
+ );
29873
+ return next;
29874
+ }
29875
+ return {
29876
+ scan(text, context, scanOpts) {
29877
+ return enqueue((resolve2) => {
29878
+ runOne(
29879
+ {
29880
+ budgetMs,
29881
+ build: (id) => ({
29882
+ kind: "scan",
29883
+ id,
29884
+ text,
29885
+ filePath: context?.filePath,
29886
+ attribute: scanOpts?.attribute === true
29887
+ }),
29888
+ reply: (message) => {
29889
+ if (message.kind !== "result") return false;
29890
+ resolve2({ status: "ok", findings: message.findings });
29891
+ return true;
29892
+ }
29893
+ },
29894
+ resolve2
29895
+ );
29896
+ });
29897
+ },
29898
+ probe(rule) {
29899
+ return enqueue((resolve2) => {
29900
+ runOne(
29901
+ {
29902
+ budgetMs: probeBudgetMs,
29903
+ build: (id) => ({ kind: "probe", id, rule }),
29904
+ reply: (message) => {
29905
+ if (message.kind !== "probed") return false;
29906
+ resolve2({ status: "ok", safe: message.safe, worstMs: message.worstMs });
29907
+ return true;
29908
+ }
29909
+ },
29910
+ resolve2
29911
+ );
29912
+ });
29913
+ },
29914
+ async close() {
29915
+ closed = true;
29916
+ const live = worker;
29917
+ worker = void 0;
29918
+ readyWorker = void 0;
29919
+ failPending({ status: "unavailable", reason: "the scan worker is closed" });
29920
+ if (live) kill(live);
29921
+ await Promise.all([...terminating]);
29922
+ }
29923
+ };
29924
+ }
29925
+
29926
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
29927
+ var PASS_BUDGET_MS = 2e3;
29928
+ var UNQUARANTINE_HINT = "clear it with `aka detections unquarantine`";
29929
+ function ruleProbeKey(rule) {
29930
+ if (rule.matcher.type !== "regex") return void 0;
29931
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
29932
+ }
29933
+ function warn(rule, verb, detail, recoverable) {
29934
+ const hint = recoverable ? ` (${UNQUARANTINE_HINT})` : "";
29935
+ process.stderr.write(`[aka] ${verb} rule "${rule.id}": ${detail}${hint}
29936
+ `);
29937
+ }
29938
+ function warnQuarantined(rule, worstMs, cached2) {
29939
+ warn(
29940
+ rule,
29941
+ "quarantined",
29942
+ 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.",
29943
+ cached2
29944
+ );
29945
+ }
29946
+ function warnUnmeasured(rule) {
29947
+ warn(
29948
+ rule,
29949
+ "skipped",
29950
+ "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.",
29951
+ false
29952
+ );
29953
+ }
29954
+ function warnUnmeasurable(reason, count) {
29955
+ process.stderr.write(
29956
+ `[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.
29957
+ `
29958
+ );
29959
+ }
29960
+ async function quarantineRule(gateway, rule, worstMs, detail) {
29961
+ const key = ruleProbeKey(rule);
29962
+ let cached2 = false;
29963
+ if (key !== void 0) {
29964
+ try {
29965
+ await gateway.setRuleProbeVerdict(key, "quarantined", worstMs);
29966
+ cached2 = true;
29967
+ } catch {
29968
+ }
29969
+ }
29970
+ warn(rule, "quarantined", detail, cached2);
29971
+ }
29972
+ async function filterUnsafeRules(rules, gateway, opts) {
29973
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
29974
+ const prober = opts?.prober;
29975
+ const passStart = performance.now();
29976
+ const safe = [];
29977
+ const unmeasurable = /* @__PURE__ */ new Map();
29978
+ try {
29979
+ for (const rule of rules) {
29980
+ const key = ruleProbeKey(rule);
29981
+ if (key === void 0) {
29982
+ safe.push(rule);
29983
+ continue;
29984
+ }
29985
+ let cached2;
29986
+ try {
29987
+ cached2 = await gateway.getRuleProbeVerdict(key);
29988
+ } catch {
29989
+ cached2 = void 0;
29990
+ }
29991
+ if (cached2) {
29992
+ if (cached2.verdict === "safe") safe.push(rule);
29993
+ else warnQuarantined(rule, cached2.worstProbeMs, true);
29994
+ continue;
29995
+ }
29996
+ if (performance.now() - passStart >= passBudgetMs) {
29997
+ warnUnmeasured(rule);
29998
+ continue;
29999
+ }
30000
+ let isSafe;
30001
+ let worstMs;
30002
+ if (prober) {
30003
+ const outcome = await prober.probe(rule);
30004
+ if (outcome.status === "unavailable") {
30005
+ unmeasurable.set(outcome.reason, (unmeasurable.get(outcome.reason) ?? 0) + 1);
30006
+ continue;
30007
+ }
30008
+ isSafe = outcome.status === "ok" ? outcome.safe : false;
30009
+ worstMs = outcome.status === "ok" ? outcome.worstMs : outcome.elapsedMs;
30010
+ } else {
30011
+ try {
30012
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
30013
+ } catch {
30014
+ isSafe = false;
30015
+ worstMs = Number.POSITIVE_INFINITY;
30016
+ }
30017
+ }
30018
+ let persisted = false;
30019
+ try {
30020
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
30021
+ persisted = true;
30022
+ } catch {
30023
+ }
30024
+ if (isSafe) safe.push(rule);
30025
+ else warnQuarantined(rule, worstMs, persisted);
30026
+ }
30027
+ } finally {
30028
+ for (const [reason, count] of unmeasurable) warnUnmeasurable(reason, count);
30029
+ }
30030
+ return safe;
30031
+ }
30032
+
30033
+ // ../../packages/plugin-sdk/src/guarded-scan.ts
30034
+ var DEFAULT_DEGRADE_SCOPE = "the rest of this process";
30035
+ function warnDegraded(scope, dropped, detail) {
30036
+ process.stderr.write(
30037
+ `[aka] isolated scanning is off for ${scope}: ${detail}. ${String(dropped)} pulled/custom-pack rule(s) are excluded; the built-in packs still run.
30038
+ `
30039
+ );
30040
+ }
30041
+ function createGuardedScanner(partition, gateway, opts) {
30042
+ const degradeScope = opts?.degradeScope ?? DEFAULT_DEGRADE_SCOPE;
30043
+ const verified = partition.verified;
30044
+ let unverified = partition.unverified;
30045
+ let isolated = unverified.length > 0 ? createIsolatedScanner({ verified, unverified }, opts) : void 0;
30046
+ let retired = false;
30047
+ function inProcess(text, context) {
30048
+ return scan(text, verified, context);
30049
+ }
30050
+ async function retire() {
30051
+ const live = isolated;
30052
+ isolated = void 0;
30053
+ unverified = [];
30054
+ if (live) await live.close();
30055
+ }
30056
+ async function degrade() {
30057
+ retired = true;
30058
+ await retire();
30059
+ }
30060
+ async function attempt(active, text, context, attribute) {
30061
+ try {
30062
+ return await active.scan(text, context, { attribute });
30063
+ } catch (error51) {
30064
+ return {
30065
+ status: "unavailable",
30066
+ reason: error51 instanceof Error ? error51.message : "the scan worker failed unexpectedly"
30067
+ };
30068
+ }
30069
+ }
30070
+ async function guardedScan(text, context) {
30071
+ const active = isolated;
30072
+ if (!active) return inProcess(text, context);
30073
+ let outcome = await attempt(active, text, context, false);
30074
+ if (outcome.status === "ok") return outcome.findings;
30075
+ if (outcome.status === "timeout") outcome = await attempt(active, text, context, true);
30076
+ const dropped = unverified.length;
30077
+ if (outcome.status === "ok") {
30078
+ warnDegraded(
30079
+ degradeScope,
30080
+ dropped,
30081
+ "a scan overran its bound once and no rule could be held responsible"
30082
+ );
30083
+ const findings = outcome.findings;
30084
+ await degrade();
30085
+ return findings;
30086
+ }
30087
+ if (outcome.status === "timeout") {
30088
+ const culprit = outcome.culpritIndex === void 0 ? void 0 : unverified[outcome.culpritIndex];
30089
+ if (culprit) {
30090
+ await quarantineRule(
30091
+ gateway,
30092
+ culprit,
30093
+ outcome.elapsedMs,
30094
+ `it did not finish within the ${outcome.elapsedMs.toFixed(0)}ms isolated-scan bound and was terminated; excluded from every later scan.`
30095
+ );
30096
+ }
30097
+ warnDegraded(
30098
+ degradeScope,
30099
+ dropped,
30100
+ 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`
30101
+ );
30102
+ } else {
30103
+ warnDegraded(degradeScope, dropped, outcome.reason);
30104
+ }
30105
+ await degrade();
30106
+ return inProcess(text, context);
30107
+ }
30108
+ return {
30109
+ scan: guardedScan,
30110
+ degraded: () => retired,
30111
+ async close() {
30112
+ await retire();
28535
30113
  }
28536
30114
  };
28537
30115
  }
28538
30116
 
28539
30117
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
28540
- import { arch, hostname as hostname3, platform, release } from "os";
30118
+ import { arch, hostname as hostname4, platform, release } from "os";
28541
30119
  function resolveInventoryContext(input) {
28542
30120
  const host = {
28543
30121
  objectType: "host",
28544
30122
  // Stable-ish machine id; os/arch live in the descriptive bag (a
28545
30123
  // harder machine id can replace this without a schema change).
28546
- identityKey: hostname3(),
28547
- title: hostname3(),
28548
- attributes: { host_name: hostname3(), os: platform(), os_version: release(), arch: arch() }
30124
+ identityKey: hostname4(),
30125
+ title: hostname4(),
30126
+ attributes: { host_name: hostname4(), os: platform(), os_version: release(), arch: arch() }
28549
30127
  };
28550
30128
  const harnessAttributes = {};
28551
30129
  if (input.harnessVersion != null) harnessAttributes.harness_version = input.harnessVersion;
@@ -28566,17 +30144,43 @@ function resolveInventoryContext(input) {
28566
30144
  }
28567
30145
 
28568
30146
  // ../../packages/plugin-sdk/src/nudge.ts
28569
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28570
- import { join as join10 } from "path";
30147
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
30148
+ import { join as join11 } from "path";
28571
30149
 
28572
30150
  // ../../packages/plugin-sdk/src/paths.ts
28573
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
28574
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
30151
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
30152
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
28575
30153
 
28576
30154
  // ../../packages/plugin-sdk/src/project-files.ts
28577
30155
  var import_ignore = __toESM(require_ignore(), 1);
28578
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28579
- import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
30156
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
30157
+ import { basename as basename5, join as join12 } from "path";
30158
+
30159
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
30160
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
30161
+ if (typeof v === "string" && v.trim() === "") return void 0;
30162
+ return v;
30163
+ }, external_exports.string().optional()).catch(void 0);
30164
+ var optionalFlag = external_exports.preprocess((v) => {
30165
+ if (typeof v !== "string") return false;
30166
+ const normalized = v.trim().toLowerCase();
30167
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
30168
+ }, external_exports.boolean()).catch(false);
30169
+ var antigravityProviderEnvShape = {
30170
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
30171
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
30172
+ };
30173
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
30174
+
30175
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
30176
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
30177
+ if (typeof v === "string" && v.trim() === "") return void 0;
30178
+ return v;
30179
+ }, external_exports.string().optional()).catch(void 0);
30180
+ var codexProviderEnvShape = {
30181
+ OPENAI_BASE_URL: optionalBaseUrl3
30182
+ };
30183
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
28580
30184
 
28581
30185
  // ../../packages/plugin-sdk/src/raw-egress.ts
28582
30186
  var RawEgressError = class extends Error {
@@ -28618,61 +30222,8 @@ function safeMaskedMatch(rawMatch) {
28618
30222
  return masked;
28619
30223
  }
28620
30224
 
28621
- // ../../packages/plugin-sdk/src/rule-quarantine.ts
28622
- var PASS_BUDGET_MS = 2e3;
28623
- function ruleProbeKey(rule) {
28624
- if (rule.matcher.type !== "regex") return void 0;
28625
- return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
28626
- }
28627
- function warnQuarantined(rule, worstMs) {
28628
- const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
28629
- process.stderr.write(
28630
- `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
28631
- `
28632
- );
28633
- }
28634
- async function filterUnsafeRules(rules, gateway, opts) {
28635
- const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
28636
- const passStart = performance.now();
28637
- const safe = [];
28638
- for (const rule of rules) {
28639
- const key = ruleProbeKey(rule);
28640
- if (key === void 0) {
28641
- safe.push(rule);
28642
- continue;
28643
- }
28644
- let cached2;
28645
- try {
28646
- cached2 = await gateway.getRuleProbeVerdict(key);
28647
- } catch {
28648
- cached2 = void 0;
28649
- }
28650
- if (cached2) {
28651
- if (cached2.verdict === "safe") safe.push(rule);
28652
- else warnQuarantined(rule, cached2.worstProbeMs);
28653
- continue;
28654
- }
28655
- if (performance.now() - passStart >= passBudgetMs) {
28656
- warnQuarantined(rule, void 0);
28657
- continue;
28658
- }
28659
- let isSafe;
28660
- let worstMs;
28661
- try {
28662
- ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
28663
- } catch {
28664
- isSafe = false;
28665
- worstMs = Number.POSITIVE_INFINITY;
28666
- }
28667
- await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
28668
- if (isSafe) safe.push(rule);
28669
- else warnQuarantined(rule, worstMs);
28670
- }
28671
- return safe;
28672
- }
28673
-
28674
30225
  // ../../packages/plugin-sdk/src/runtime.ts
28675
- import { randomUUID as randomUUID12 } from "crypto";
30226
+ import { randomUUID as randomUUID14 } from "crypto";
28676
30227
  var ENFORCEMENT_CEILING_ENABLED = false;
28677
30228
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
28678
30229
  function entryIsActive(entry, now) {
@@ -28697,6 +30248,7 @@ function createPluginRuntime(gateway, settings, opts) {
28697
30248
  const dataDir2 = opts?.dataDir;
28698
30249
  let policies = [];
28699
30250
  let rules = [];
30251
+ let scanner;
28700
30252
  let bundleExceptions = [];
28701
30253
  let initialized = false;
28702
30254
  const ruleActionIndex = /* @__PURE__ */ new Map();
@@ -28722,8 +30274,24 @@ function createPluginRuntime(gateway, settings, opts) {
28722
30274
  return key !== void 0 && bundledProbeKeys.has(key);
28723
30275
  });
28724
30276
  const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
28725
- const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
28726
- rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
30277
+ let prober;
30278
+ const gated = await filterUnsafeRules(needsGate, gateway, {
30279
+ prober: {
30280
+ probe: (rule) => {
30281
+ prober ??= createIsolatedScanner({ verified: [], unverified: [] }, opts?.scanIsolation);
30282
+ return prober.probe(rule);
30283
+ }
30284
+ }
30285
+ });
30286
+ await prober?.close();
30287
+ const verified = bundle.rulesComplete ? [...ciVerified] : [...getLoadedRules(), ...ciVerified];
30288
+ const unverified = [];
30289
+ for (const rule of gated) {
30290
+ if (rule.matcher.type === "regex") unverified.push(rule);
30291
+ else verified.push(rule);
30292
+ }
30293
+ rules = [...verified, ...unverified];
30294
+ scanner = createGuardedScanner({ verified, unverified }, gateway, opts?.scanIsolation);
28727
30295
  bundleExceptions = bundle.exceptions ?? [];
28728
30296
  initialized = true;
28729
30297
  }
@@ -28863,7 +30431,7 @@ function createPluginRuntime(gateway, settings, opts) {
28863
30431
  const pair = `${finding.ruleId}:${fp}`;
28864
30432
  if (seen.has(pair)) continue;
28865
30433
  seen.add(pair);
28866
- const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
30434
+ const reference = randomUUID14().replaceAll("-", "").slice(0, 6);
28867
30435
  const maskedValue = maskMatch(finding.rawMatch);
28868
30436
  try {
28869
30437
  await gateway.recordBlockedDetection({
@@ -28887,8 +30455,10 @@ function createPluginRuntime(gateway, settings, opts) {
28887
30455
  async function evaluate(text, context, ctx) {
28888
30456
  try {
28889
30457
  await ensureInitialized();
30458
+ if (!scanner) throw new Error("the runtime initialized without a scanner");
28890
30459
  const shielded = shieldPointers(text);
28891
- const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
30460
+ const matched = await scanner.scan(shielded.text, context);
30461
+ const findings = dropShieldedFindings(matched, shielded.spans);
28892
30462
  const fpCache = /* @__PURE__ */ new Map();
28893
30463
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
28894
30464
  const decision = decide(findings, text, excepted);
@@ -28945,7 +30515,7 @@ function createPluginRuntime(gateway, settings, opts) {
28945
30515
  valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
28946
30516
  }) : void 0;
28947
30517
  return {
28948
- id: randomUUID12(),
30518
+ id: randomUUID14(),
28949
30519
  eventId: event.id,
28950
30520
  ruleId: match.ruleId,
28951
30521
  category: match.category,
@@ -28971,21 +30541,28 @@ function createPluginRuntime(gateway, settings, opts) {
28971
30541
  const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
28972
30542
  return contentHashOf(JSON.stringify(sorted));
28973
30543
  } catch {
28974
- return `unresolved-${randomUUID12()}`;
30544
+ return `unresolved-${randomUUID14()}`;
28975
30545
  }
28976
30546
  }
30547
+ function scanIsolationDegraded() {
30548
+ return scanner?.degraded() ?? false;
30549
+ }
28977
30550
  async function close() {
30551
+ try {
30552
+ await scanner?.close();
30553
+ } catch {
30554
+ }
28978
30555
  await gateway.close();
28979
30556
  }
28980
- return { processText, capture, rulesetFingerprint, close };
30557
+ return { processText, capture, rulesetFingerprint, scanIsolationDegraded, close };
28981
30558
  }
28982
30559
 
28983
30560
  // ../../packages/plugin-sdk/src/suppressions.ts
28984
30561
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28985
30562
 
28986
30563
  // ../../packages/plugin-sdk/src/throttle.ts
28987
- import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28988
- import { join as join12 } from "path";
30564
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
30565
+ import { join as join13 } from "path";
28989
30566
 
28990
30567
  // ../../packages/plugin-sdk/src/tokenize.ts
28991
30568
  function redactedPlaceholder(category) {
@@ -29227,7 +30804,6 @@ function createVaultGlue(options) {
29227
30804
  const vault = new SecretVault({
29228
30805
  repo: db.secretVault,
29229
30806
  keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
29230
- fingerprintKey: loadOrCreateFingerprintKey(dir),
29231
30807
  // Read live so a revocation applies to the very next call, not the next
29232
30808
  // process.
29233
30809
  isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
@@ -29248,8 +30824,10 @@ function createVaultGlue(options) {
29248
30824
  return decision.allow;
29249
30825
  }
29250
30826
  });
30827
+ let fingerprintKey;
30828
+ const fingerprintKeyForWrite = () => fingerprintKey ??= loadOrCreateFingerprintKey(dir);
29251
30829
  const vaultWithSightings = {
29252
- tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
30830
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3, fingerprintKeyForWrite),
29253
30831
  detokenize: (token, opts) => vault.detokenize(token, opts),
29254
30832
  describePointer: (token) => vault.describePointer(token),
29255
30833
  resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
@@ -29282,8 +30860,59 @@ var UNOPENABLE_VAULT = {
29282
30860
  resolvePointerIdentity: () => Promise.resolve(null)
29283
30861
  };
29284
30862
 
30863
+ // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
30864
+ import { writeFileSync as writeFileSync7 } from "fs";
30865
+ import { join as join14 } from "path";
30866
+
30867
+ // ../../packages/setup-wizard/src/triage/plan-file.ts
30868
+ import { mkdtempSync, readFileSync as readFileSync9, rmdirSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
30869
+ import { tmpdir } from "os";
30870
+ import { basename as basename6, dirname as dirname4, join as join15 } from "path";
30871
+ var SuppressionEntrySchema = external_exports.object({
30872
+ ruleId: external_exports.string(),
30873
+ category: DetectionCategory,
30874
+ valueFingerprint: external_exports.string(),
30875
+ keyVersion: external_exports.number(),
30876
+ maskedValue: external_exports.string(),
30877
+ justification: external_exports.string()
30878
+ });
30879
+ var ShowcaseCategorySchema = external_exports.object({
30880
+ category: DetectionCategory,
30881
+ action: BuiltinPolicyId,
30882
+ genuineCount: external_exports.number(),
30883
+ fpCount: external_exports.number(),
30884
+ reasoning: external_exports.string()
30885
+ });
30886
+ var JoinEntrySchema = external_exports.object({
30887
+ id: external_exports.string(),
30888
+ ruleId: external_exports.string(),
30889
+ category: DetectionCategory,
30890
+ valueFingerprint: external_exports.string().optional(),
30891
+ keyVersion: external_exports.number().optional(),
30892
+ maskedMatch: external_exports.string(),
30893
+ maskedContext: external_exports.string()
30894
+ });
30895
+ var PLAN_FILE_VERSION = 3;
30896
+ var PersistedPlanSchema = external_exports.object({
30897
+ version: external_exports.literal(PLAN_FILE_VERSION),
30898
+ // partialRecord (not record): a posture only covers the categories present in
30899
+ // the evidence, so an exhaustive-key record would reject every real plan.
30900
+ posture: external_exports.partialRecord(DetectionCategory, BuiltinPolicyId),
30901
+ entries: external_exports.array(SuppressionEntrySchema),
30902
+ showcase: external_exports.array(ShowcaseCategorySchema),
30903
+ join: external_exports.array(JoinEntrySchema),
30904
+ notes: external_exports.string(),
30905
+ // The store's per-category action at preview time. The downgrade view is
30906
+ // rendered from it at PREVIEW (renderPosturePlan); confirm reads it back ONLY to
30907
+ // compare against the live store and reject a stale plan (runConfirm's drift gate).
30908
+ current: external_exports.partialRecord(DetectionCategory, ActionTaken)
30909
+ });
30910
+
30911
+ // ../../packages/setup-wizard/src/triage/writeback.ts
30912
+ var TRIAGE_STATUSES = ["complete", "complete:no-history", "skipped:no-consent"];
30913
+
29285
30914
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
29286
- import { randomUUID as randomUUID13 } from "crypto";
30915
+ import { randomUUID as randomUUID15 } from "crypto";
29287
30916
 
29288
30917
  // ../../packages/plugin-runtime/src/recorder.ts
29289
30918
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -29445,7 +31074,7 @@ var StandaloneDataGateway = class {
29445
31074
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
29446
31075
  const installed = this.installedScanRules();
29447
31076
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
29448
- id: randomUUID13(),
31077
+ id: randomUUID15(),
29449
31078
  scope: "global",
29450
31079
  target: { ruleId },
29451
31080
  action,
@@ -29598,15 +31227,15 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
29598
31227
  }
29599
31228
 
29600
31229
  // ../../packages/plugin-runtime/src/handle-session-start.ts
29601
- import { randomUUID as randomUUID14 } from "crypto";
31230
+ import { randomUUID as randomUUID16 } from "crypto";
29602
31231
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
29603
31232
 
29604
31233
  // src/history/transcripts.ts
29605
- import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
31234
+ import { readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
29606
31235
  import { homedir as homedir3 } from "os";
29607
- import { join as join13 } from "path";
31236
+ import { join as join16 } from "path";
29608
31237
  function transcriptsDir(home) {
29609
- return join13(home ?? homedir3(), ".claude", "projects");
31238
+ return join16(home ?? homedir3(), ".claude", "projects");
29610
31239
  }
29611
31240
  function isRecord(value) {
29612
31241
  return typeof value === "object" && value !== null;
@@ -29849,25 +31478,25 @@ var DAY_MS5 = 24 * 60 * 60 * 1e3;
29849
31478
  function* iterateFileContents(dir, excludeSessionId) {
29850
31479
  let projects;
29851
31480
  try {
29852
- projects = readdirSync4(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
31481
+ projects = readdirSync5(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
29853
31482
  } catch {
29854
31483
  return;
29855
31484
  }
29856
31485
  for (const project of projects) {
29857
- const projectDir = join13(dir, project);
31486
+ const projectDir = join16(dir, project);
29858
31487
  let files;
29859
31488
  try {
29860
- files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
31489
+ files = readdirSync5(projectDir).filter((name) => name.endsWith(".jsonl"));
29861
31490
  } catch {
29862
31491
  continue;
29863
31492
  }
29864
31493
  for (const file2 of files) {
29865
31494
  if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
29866
31495
  continue;
29867
- const filePath = join13(projectDir, file2);
31496
+ const filePath = join16(projectDir, file2);
29868
31497
  let content;
29869
31498
  try {
29870
- content = readFileSync8(filePath, "utf8");
31499
+ content = readFileSync10(filePath, "utf8");
29871
31500
  } catch {
29872
31501
  continue;
29873
31502
  }
@@ -29999,11 +31628,11 @@ async function scanHistory(config2, opts = {}, onHit) {
29999
31628
  }
30000
31629
 
30001
31630
  // src/history/tail-scrub.ts
30002
- import { readFileSync as readFileSync10, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
31631
+ import { readFileSync as readFileSync12, renameSync as renameSync6, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync10 } from "fs";
30003
31632
 
30004
31633
  // src/remediation/redact.ts
30005
- import { readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
30006
- import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
31634
+ import { readFileSync as readFileSync11, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
31635
+ import { isAbsolute as isAbsolute2, relative, resolve } from "path";
30007
31636
  function platformRedactionScope(home) {
30008
31637
  return { artifactRoots: [transcriptsDir(home)] };
30009
31638
  }
@@ -30017,7 +31646,7 @@ function realPathOrNull(path) {
30017
31646
  function isWithinRoot(realTarget, root) {
30018
31647
  const realRoot = realPathOrNull(root);
30019
31648
  if (realRoot === null) return false;
30020
- const rel = relative2(realRoot, realTarget);
31649
+ const rel = relative(realRoot, realTarget);
30021
31650
  return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
30022
31651
  }
30023
31652
  function resolveRedactableArtifact(filePath, scope) {
@@ -30032,9 +31661,9 @@ async function scrubTranscriptTail(filePath, deps) {
30032
31661
  try {
30033
31662
  const realPath = resolveRedactableArtifact(filePath, deps.scope);
30034
31663
  if (realPath === null) return null;
30035
- const statBefore = statSync5(realPath);
31664
+ const statBefore = statSync7(realPath);
30036
31665
  if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
30037
- const content = readFileSync10(realPath, "utf8");
31666
+ const content = readFileSync12(realPath, "utf8");
30038
31667
  const lines = content.split("\n");
30039
31668
  let rewritten = 0;
30040
31669
  for (const [i, line] of lines.entries()) {
@@ -30048,16 +31677,16 @@ async function scrubTranscriptTail(filePath, deps) {
30048
31677
  if (rewritten === 0) return { rewritten: 0 };
30049
31678
  const tmpPath = `${realPath}.aka-scrub.tmp`;
30050
31679
  try {
30051
- writeFileSync7(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
30052
- const statNow = statSync5(realPath);
31680
+ writeFileSync10(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
31681
+ const statNow = statSync7(realPath);
30053
31682
  if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
30054
- rmSync5(tmpPath, { force: true, recursive: true });
31683
+ rmSync7(tmpPath, { force: true, recursive: true });
30055
31684
  return null;
30056
31685
  }
30057
31686
  renameSync6(tmpPath, realPath);
30058
31687
  } catch {
30059
31688
  try {
30060
- rmSync5(tmpPath, { force: true, recursive: true });
31689
+ rmSync7(tmpPath, { force: true, recursive: true });
30061
31690
  } catch {
30062
31691
  }
30063
31692
  return null;
@@ -30071,15 +31700,15 @@ async function scrubTranscriptTail(filePath, deps) {
30071
31700
  // src/history/tail.ts
30072
31701
  import { createHash as createHash5 } from "crypto";
30073
31702
  import {
30074
- closeSync,
31703
+ closeSync as closeSync2,
30075
31704
  fstatSync,
30076
31705
  mkdirSync as mkdirSync5,
30077
- openSync,
30078
- readFileSync as readFileSync11,
31706
+ openSync as openSync2,
31707
+ readFileSync as readFileSync13,
30079
31708
  readSync,
30080
- writeFileSync as writeFileSync8
31709
+ writeFileSync as writeFileSync11
30081
31710
  } from "fs";
30082
- import { join as join14 } from "path";
31711
+ import { join as join17 } from "path";
30083
31712
 
30084
31713
  // src/history/usage.ts
30085
31714
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
@@ -30328,7 +31957,6 @@ function fenced(body) {
30328
31957
  }
30329
31958
 
30330
31959
  // src/backfill.ts
30331
- var TRIAGE_STATUSES = ["complete", "complete:no-history", "skipped:no-consent"];
30332
31960
  function triageSentinel(count, status) {
30333
31961
  return JSON.stringify({ done: true, count, status }) + "\n";
30334
31962
  }
@@ -30437,7 +32065,7 @@ function buildTranscriptScrubber() {
30437
32065
  scope
30438
32066
  });
30439
32067
  }
30440
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
32068
+ if (process.argv[1] && fileURLToPath2(import.meta.url) === process.argv[1]) {
30441
32069
  const triage = process.argv.includes("--triage");
30442
32070
  const startedAt = Date.now();
30443
32071
  const sessionId = process.env.CLAUDE_CODE_BRIDGE_SESSION_ID;