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

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