@akasecurity/ai-tc-claude-code 0.9.3 → 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,9 +492,8 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/persistence/src/database.ts
495
- import { randomUUID as randomUUID8 } from "crypto";
496
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
497
- import { join, sep } from "path";
495
+ import { randomUUID as randomUUID10 } from "crypto";
496
+ import { join as join2, sep } from "path";
498
497
  import { DatabaseSync } from "node:sqlite";
499
498
 
500
499
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -558,6 +557,30 @@ var SQLITE_MIGRATIONS = [
558
557
  {
559
558
  tag: "0014_drop_legacy_events_findings",
560
559
  sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
560
+ },
561
+ {
562
+ tag: "0015_busy_vengeance",
563
+ sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
564
+ },
565
+ {
566
+ tag: "0016_breezy_zodiak",
567
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
568
+ },
569
+ {
570
+ tag: "0017_rainy_kat_farrell",
571
+ sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
572
+ },
573
+ {
574
+ tag: "0018_serious_tana_nile",
575
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
576
+ },
577
+ {
578
+ tag: "0019_audit_started_at_index",
579
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
580
+ },
581
+ {
582
+ tag: "0020_secret_vault_pagination_indexes",
583
+ 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');"
561
584
  }
562
585
  ];
563
586
 
@@ -15295,7 +15318,17 @@ var Finding = external_exports.object({
15295
15318
  }).meta({ id: "Finding" });
15296
15319
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15297
15320
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15298
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15321
+ var FindingProvider = external_exports.enum([
15322
+ "claudecode",
15323
+ "claudedesktop",
15324
+ "cursor",
15325
+ "copilot",
15326
+ "chatgpt",
15327
+ "claudeai",
15328
+ "codex",
15329
+ "antigravity",
15330
+ "api"
15331
+ ]).meta({ id: "FindingProvider" });
15299
15332
  var FindingCategory = external_exports.enum([
15300
15333
  "secret",
15301
15334
  "pii",
@@ -15349,7 +15382,16 @@ var FindingInstance = external_exports.object({
15349
15382
  confidence: external_exports.number().min(0).max(1),
15350
15383
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15351
15384
  // that predate the resolution feature stay valid.
15352
- status: FindingStatus.optional()
15385
+ status: FindingStatus.optional(),
15386
+ // The audit event this finding was captured from. Optional so callers that
15387
+ // do not project it stay valid. An at-rest finding is content-addressed by
15388
+ // finding_key and its row is upserted on re-detection, so this names the
15389
+ // MOST RECENT detection event, not the first.
15390
+ eventId: external_exports.string().optional(),
15391
+ // The session that event belongs to, when it has one — the seam a
15392
+ // per-instance "view session" link needs. Absent for events captured
15393
+ // outside a session.
15394
+ sessionId: external_exports.string().optional()
15353
15395
  }).meta({ id: "FindingInstance" });
15354
15396
  var FindingGroup = external_exports.object({
15355
15397
  id: external_exports.string(),
@@ -15393,7 +15435,11 @@ var FindingFacets = external_exports.object({
15393
15435
  // for every instance, so every group lands in a bucket; a status-less
15394
15436
  // group (possible only for callers whose rows carry no statuses) is
15395
15437
  // counted under no value.
15396
- status: external_exports.array(FindingFacetItem)
15438
+ status: external_exports.array(FindingFacetItem),
15439
+ // Host tool (attributes.tool_name). Present only on the instance-level
15440
+ // reads, which can filter by it; the grouped read omits the dimension
15441
+ // because a group spans tools.
15442
+ tool: external_exports.array(FindingFacetItem).optional()
15397
15443
  }).meta({ id: "FindingFacets" });
15398
15444
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15399
15445
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15411,6 +15457,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15411
15457
  // Scope to findings whose event carries this session id (the Activity page's
15412
15458
  // session → findings drilldown). Findings without a session never match.
15413
15459
  sessionId: external_exports.string().optional(),
15460
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15461
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15462
+ // means all time — this list has no default window.
15463
+ from: external_exports.iso.datetime().optional(),
15464
+ // A group or instance id that must appear in the page even when the cursor
15465
+ // has already advanced past its sort position. This is what keeps the
15466
+ // Findings page's one-shot ?finding= deep link resolving once the list
15467
+ // paginates: the target group is appended out of sort order rather than
15468
+ // scanning forward for it. Never affects totals, facets or the cursor.
15469
+ includeId: external_exports.string().optional(),
15414
15470
  groupBy: external_exports.literal("type").optional(),
15415
15471
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15416
15472
  cursor: external_exports.string().optional()
@@ -15455,15 +15511,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15455
15511
  detection: FindingDetectionRef,
15456
15512
  policy: FindingPolicyRef
15457
15513
  }).meta({ id: "FindingInstanceDetail" });
15514
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15515
+ var ListFindingInstancesQuery = external_exports.object({
15516
+ severity: external_exports.array(Severity).optional(),
15517
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15518
+ subtype: external_exports.array(external_exports.string()).optional(),
15519
+ provider: external_exports.array(FindingProvider).optional(),
15520
+ action: external_exports.array(FindingAction).optional(),
15521
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15522
+ // the grouped query's group-level fold.
15523
+ status: external_exports.array(FindingStatus).optional(),
15524
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15525
+ // where the free-text `q` can only match the rendered "via Bash" label.
15526
+ tool: external_exports.array(external_exports.string()).optional(),
15527
+ // Exact repository / file-path matches, for the drill-down out of the
15528
+ // locations view. A row whose event carries no repo/file matches neither.
15529
+ repo: external_exports.string().optional(),
15530
+ file: external_exports.string().optional(),
15531
+ q: external_exports.string().optional(),
15532
+ sessionId: external_exports.string().optional(),
15533
+ from: external_exports.iso.datetime().optional(),
15534
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15535
+ cursor: external_exports.string().optional()
15536
+ });
15537
+ var ListFindingInstancesResponse = external_exports.object({
15538
+ // Instances matching the filters across the whole scope, not just this
15539
+ // page — cursor-independent, like the grouped list's totals.
15540
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15541
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15542
+ // dimension still excludes its own filter.
15543
+ facets: FindingFacets,
15544
+ items: external_exports.array(FindingInstanceDetail),
15545
+ nextCursor: external_exports.string().nullable()
15546
+ }).meta({ id: "ListFindingInstancesResponse" });
15547
+ var FindingLocationFile = external_exports.object({
15548
+ // Empty when the instances carried no file path (a prompt or a tool call
15549
+ // with no file attribution).
15550
+ file: external_exports.string(),
15551
+ instanceCount: external_exports.number().int().nonnegative(),
15552
+ maxSeverity: Severity,
15553
+ latestDetectedAt: external_exports.iso.datetime(),
15554
+ // Folded from the instances' derived statuses with the same
15555
+ // open-dominates precedence a group uses.
15556
+ status: FindingStatus.optional(),
15557
+ // Distinct rules seen at this location, capped — the row shows them as
15558
+ // chips, and the count is what conveys scale.
15559
+ ruleIds: external_exports.array(external_exports.string())
15560
+ }).meta({ id: "FindingLocationFile" });
15561
+ var FindingLocationRepo = external_exports.object({
15562
+ /** Empty when the instances carried no repo attribute. */
15563
+ repo: external_exports.string(),
15564
+ instanceCount: external_exports.number().int().nonnegative(),
15565
+ maxSeverity: Severity,
15566
+ latestDetectedAt: external_exports.iso.datetime(),
15567
+ status: FindingStatus.optional(),
15568
+ files: external_exports.array(FindingLocationFile)
15569
+ }).meta({ id: "FindingLocationRepo" });
15570
+ var ListFindingLocationsQuery = external_exports.object({
15571
+ severity: external_exports.array(Severity).optional(),
15572
+ subtype: external_exports.array(external_exports.string()).optional(),
15573
+ provider: external_exports.array(FindingProvider).optional(),
15574
+ action: external_exports.array(FindingAction).optional(),
15575
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15576
+ // instances that match, and folds its status from those.
15577
+ status: external_exports.array(FindingStatus).optional(),
15578
+ tool: external_exports.array(external_exports.string()).optional(),
15579
+ q: external_exports.string().optional(),
15580
+ sessionId: external_exports.string().optional(),
15581
+ from: external_exports.iso.datetime().optional(),
15582
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15583
+ });
15584
+ var ListFindingLocationsResponse = external_exports.object({
15585
+ totals: external_exports.object({
15586
+ findings: external_exports.number().int().nonnegative(),
15587
+ repos: external_exports.number().int().nonnegative(),
15588
+ files: external_exports.number().int().nonnegative()
15589
+ }),
15590
+ /** Sorted by max severity, then most recent. */
15591
+ items: external_exports.array(FindingLocationRepo),
15592
+ /** Whether `limit` truncated the repo list. */
15593
+ hasMore: external_exports.boolean()
15594
+ }).meta({ id: "ListFindingLocationsResponse" });
15458
15595
 
15459
15596
  // ../../packages/schema/src/zod/harness-map.ts
15460
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15597
+ var Harness = external_exports.enum([
15598
+ "claudecode",
15599
+ "cursor",
15600
+ "copilot",
15601
+ "codex",
15602
+ "antigravity",
15603
+ "windsurf",
15604
+ "claudedesktop",
15605
+ "chatgpt",
15606
+ "claudeai",
15607
+ "api"
15608
+ ]).meta({ id: "Harness" });
15461
15609
  var TOOL_TO_HARNESS = {
15462
15610
  "claude-code": "claudecode",
15463
15611
  "claude-desktop": "claudedesktop",
15464
15612
  "github-copilot": "copilot",
15465
15613
  cursor: "cursor",
15466
- chatgpt: "chatgpt"
15614
+ chatgpt: "chatgpt",
15615
+ codex: "codex",
15616
+ antigravity: "antigravity",
15617
+ "claude-ai": "claudeai"
15467
15618
  };
15468
15619
 
15469
15620
  // ../../packages/schema/src/zod/meta.ts
@@ -15921,7 +16072,18 @@ var ActivityOverviewResponse = external_exports.object({
15921
16072
  // ../../packages/schema/src/zod/event.ts
15922
16073
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15923
16074
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15924
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16075
+ var SourceTool = external_exports.enum([
16076
+ "claude-code",
16077
+ "claude-desktop",
16078
+ "cursor",
16079
+ "chatgpt",
16080
+ "claude-ai",
16081
+ "github-copilot",
16082
+ "codex",
16083
+ "antigravity",
16084
+ "cli",
16085
+ "unknown"
16086
+ ]).meta({ id: "SourceTool" });
15925
16087
  var EventMetadata = external_exports.object({
15926
16088
  sessionId: external_exports.string().optional(),
15927
16089
  repo: external_exports.string().optional(),
@@ -15992,7 +16154,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
15992
16154
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
15993
16155
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
15994
16156
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
15995
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16157
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
15996
16158
  var AccessCounts = external_exports.object({
15997
16159
  open: external_exports.number().int().nonnegative(),
15998
16160
  approved: external_exports.number().int().nonnegative(),
@@ -16214,6 +16376,7 @@ var ExceptionConditions = external_exports.object({
16214
16376
  sourceTool: external_exports.string().optional(),
16215
16377
  provider: external_exports.string().optional()
16216
16378
  }).strict();
16379
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16217
16380
  var DetectionException = external_exports.object({
16218
16381
  id: external_exports.guid(),
16219
16382
  ruleId: external_exports.string(),
@@ -16230,6 +16393,7 @@ var DetectionException = external_exports.object({
16230
16393
  keyVersion: external_exports.number().int().positive(),
16231
16394
  // maskMatch() preview of the approved value — never the raw value.
16232
16395
  maskedValue: external_exports.string(),
16396
+ capability: ExceptionCapability.default("suppress"),
16233
16397
  scope: ExceptionScope,
16234
16398
  expiresAt: external_exports.iso.datetime().nullable(),
16235
16399
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16253,11 +16417,13 @@ var ExceptionBundleEntry = DetectionException.pick({
16253
16417
  ruleId: true,
16254
16418
  valueFingerprint: true,
16255
16419
  keyVersion: true,
16420
+ capability: true,
16256
16421
  expiresAt: true,
16257
16422
  maxUses: true,
16258
16423
  useCount: true,
16259
16424
  conditions: true
16260
16425
  });
16426
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16261
16427
 
16262
16428
  // ../../packages/schema/src/zod/rule.ts
16263
16429
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17068,6 +17234,35 @@ var EgressWriteSummary = external_exports.object({
17068
17234
  droppedFiles: external_exports.array(external_exports.string()).default([])
17069
17235
  }).meta({ id: "EgressWriteSummary" });
17070
17236
 
17237
+ // ../../packages/schema/src/zod/exception-action.ts
17238
+ var confirmation = external_exports.string().optional();
17239
+ var ApproveBlockedInput = external_exports.object({
17240
+ reference: external_exports.string(),
17241
+ scope: external_exports.string(),
17242
+ reason: external_exports.string(),
17243
+ confirmation
17244
+ });
17245
+ var AddExceptionInput = external_exports.object({
17246
+ ruleId: external_exports.string(),
17247
+ value: external_exports.string(),
17248
+ scope: external_exports.string(),
17249
+ reason: external_exports.string(),
17250
+ confirmation
17251
+ });
17252
+ var GrantRevealInput = external_exports.object({
17253
+ pointer: external_exports.string(),
17254
+ scope: external_exports.string(),
17255
+ justification: external_exports.string(),
17256
+ confirmation
17257
+ });
17258
+ var RevokeExceptionInput = external_exports.object({
17259
+ id: external_exports.string(),
17260
+ reason: external_exports.string()
17261
+ });
17262
+ var RotateKeyInput = external_exports.object({
17263
+ confirmation: external_exports.string()
17264
+ });
17265
+
17071
17266
  // ../../packages/schema/src/zod/findings-group-build.ts
17072
17267
  function toApiAction(dbVal) {
17073
17268
  const map2 = {
@@ -17123,6 +17318,8 @@ function buildFindingGroups(rows, opts = {}) {
17123
17318
  repo: r.repo,
17124
17319
  file: r.file,
17125
17320
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17321
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17322
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17126
17323
  action: toApiAction(effectiveDbAction),
17127
17324
  detectedAt: r.occurredAt,
17128
17325
  confidence: r.confidence,
@@ -17254,14 +17451,17 @@ function applyFindingFilters(groups, opts) {
17254
17451
  }
17255
17452
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17256
17453
  var SEVERITY_RANK = SEVERITY_ORDER;
17454
+ function compareFindingGroupOrder(a, b) {
17455
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17456
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17457
+ const severityDiff = rankA - rankB;
17458
+ if (severityDiff !== 0) return severityDiff;
17459
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17460
+ if (recencyDiff !== 0) return recencyDiff;
17461
+ return a.id.localeCompare(b.id);
17462
+ }
17257
17463
  function sortFindingGroups(groups) {
17258
- return [...groups].sort((a, b) => {
17259
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17260
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17261
- const severityDiff = rankA - rankB;
17262
- if (severityDiff !== 0) return severityDiff;
17263
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17264
- });
17464
+ return [...groups].sort(compareFindingGroupOrder);
17265
17465
  }
17266
17466
  function computeFindingFacets(allGroups, opts) {
17267
17467
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17317,15 +17517,158 @@ function computeFindingFacets(allGroups, opts) {
17317
17517
  for (const g of forStatus) {
17318
17518
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17319
17519
  }
17320
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17520
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17521
+ return {
17522
+ severity: toItems2(severityMap),
17523
+ provider: toItems2(providerMap),
17524
+ action: toItems2(actionMap),
17525
+ subtype: toItems2(subtypeMap),
17526
+ status: toItems2(statusMap)
17527
+ };
17528
+ }
17529
+
17530
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17531
+ function rowHaystack(row) {
17532
+ return [
17533
+ row.ruleId,
17534
+ row.category,
17535
+ row.maskedMatch,
17536
+ row.repo,
17537
+ row.file,
17538
+ row.toolName ? `via ${row.toolName}` : "",
17539
+ row.id
17540
+ ].join(" ").toLowerCase();
17541
+ }
17542
+ function matchesDimension(row, opts, dimension) {
17543
+ switch (dimension) {
17544
+ case "severity":
17545
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17546
+ case "subtype":
17547
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17548
+ case "providers":
17549
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17550
+ case "actions":
17551
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17552
+ case "statuses":
17553
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17554
+ case "tools":
17555
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17556
+ case "repo":
17557
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17558
+ case "file":
17559
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17560
+ case "q":
17561
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17562
+ }
17563
+ }
17564
+ var DIMENSIONS = [
17565
+ "severity",
17566
+ "subtype",
17567
+ "providers",
17568
+ "actions",
17569
+ "statuses",
17570
+ "tools",
17571
+ "repo",
17572
+ "file",
17573
+ "q"
17574
+ ];
17575
+ function matchesInstanceFilters(row, opts, except) {
17576
+ for (const dimension of DIMENSIONS) {
17577
+ if (dimension === except) continue;
17578
+ if (!matchesDimension(row, opts, dimension)) return false;
17579
+ }
17580
+ return true;
17581
+ }
17582
+ function toItems(counts) {
17583
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17584
+ }
17585
+ function bump(counts, value) {
17586
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17587
+ }
17588
+ function createInstanceFacetAccumulator(opts) {
17589
+ const severity = /* @__PURE__ */ new Map();
17590
+ const subtype = /* @__PURE__ */ new Map();
17591
+ const provider = /* @__PURE__ */ new Map();
17592
+ const action = /* @__PURE__ */ new Map();
17593
+ const status = /* @__PURE__ */ new Map();
17594
+ const tool = /* @__PURE__ */ new Map();
17595
+ return {
17596
+ add(row) {
17597
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17598
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17599
+ if (matchesInstanceFilters(row, opts, "providers")) {
17600
+ bump(provider, toApiProvider(row.sourceTool));
17601
+ }
17602
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17603
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17604
+ bump(status, row.status);
17605
+ }
17606
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17607
+ bump(tool, row.toolName);
17608
+ }
17609
+ },
17610
+ facets: () => ({
17611
+ severity: toItems(severity),
17612
+ subtype: toItems(subtype),
17613
+ provider: toItems(provider),
17614
+ action: toItems(action),
17615
+ status: toItems(status),
17616
+ tool: toItems(tool)
17617
+ })
17618
+ };
17619
+ }
17620
+ function toInstanceDetail(row) {
17621
+ const category = toApiCategory(row.category);
17622
+ return {
17623
+ id: row.id,
17624
+ provider: toApiProvider(row.sourceTool),
17625
+ repo: row.repo,
17626
+ file: row.file,
17627
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17628
+ eventId: row.eventId,
17629
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17630
+ action: toApiAction(row.actionTaken),
17631
+ detectedAt: row.occurredAt,
17632
+ confidence: row.confidence,
17633
+ ...row.status === void 0 ? {} : { status: row.status },
17634
+ groupId: row.ruleId,
17635
+ category,
17636
+ subtype: row.ruleId,
17637
+ severity: row.severity,
17638
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17639
+ detection: { id: row.ruleId, name: null },
17640
+ policy: { id: `category:${category}`, name: category }
17641
+ };
17642
+ }
17643
+ var SEVERITY_ORDER2 = {
17644
+ critical: 0,
17645
+ high: 1,
17646
+ medium: 2,
17647
+ low: 3
17648
+ };
17649
+ function newLocationAccumulator() {
17321
17650
  return {
17322
- severity: toItems(severityMap),
17323
- provider: toItems(providerMap),
17324
- action: toItems(actionMap),
17325
- subtype: toItems(subtypeMap),
17326
- status: toItems(statusMap)
17651
+ instanceCount: 0,
17652
+ // Sorts after every known severity, so the first row always wins the
17653
+ // comparison below rather than an unknown value pinning the location.
17654
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17655
+ maxSeverity: "low",
17656
+ latestDetectedAt: "",
17657
+ statuses: [],
17658
+ ruleIds: /* @__PURE__ */ new Set()
17327
17659
  };
17328
17660
  }
17661
+ function addToLocation(acc, row) {
17662
+ acc.instanceCount += 1;
17663
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17664
+ if (rank < acc.maxSeverityRank) {
17665
+ acc.maxSeverityRank = rank;
17666
+ acc.maxSeverity = row.severity;
17667
+ }
17668
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17669
+ acc.statuses.push(row.status);
17670
+ acc.ruleIds.add(row.ruleId);
17671
+ }
17329
17672
 
17330
17673
  // ../../packages/schema/src/zod/installed-pack.ts
17331
17674
  var InstalledPack = external_exports.object({
@@ -17357,8 +17700,161 @@ var PatchInstalledPackRequest = external_exports.object({
17357
17700
  message: "At least one field must be provided"
17358
17701
  }).meta({ id: "PatchInstalledPackRequest" });
17359
17702
 
17703
+ // ../../packages/schema/src/zod/vault.ts
17704
+ var POINTER_FORMAT_VERSION = 2;
17705
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17706
+ var POINTER_TOKEN_PATTERN = new RegExp(
17707
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17708
+ );
17709
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17710
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17711
+ var ParsedPointer = external_exports.object({
17712
+ category: DetectionCategory,
17713
+ keyVersion: external_exports.number().int().positive(),
17714
+ pointerId: external_exports.string(),
17715
+ tag: external_exports.string()
17716
+ });
17717
+ var VaultEntry = external_exports.object({
17718
+ pointerId: external_exports.string(),
17719
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17720
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17721
+ // independently of the vault encryption key below.
17722
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17723
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17724
+ // The vault-key epoch this row's ciphertext was sealed under.
17725
+ keyVersion: external_exports.number().int().positive(),
17726
+ // Fixed at first mint and never updated: the same value detected later under a
17727
+ // different rule's category keeps the category it was minted with, so one
17728
+ // value always produces exactly one wire token.
17729
+ category: DetectionCategory,
17730
+ ruleId: external_exports.string(),
17731
+ // Partial-reveal preview for badges and listings. Never the raw value.
17732
+ maskedMatch: external_exports.string(),
17733
+ provider: external_exports.string().optional(),
17734
+ ciphertext: external_exports.string(),
17735
+ nonce: external_exports.string(),
17736
+ authTag: external_exports.string(),
17737
+ // How many times this value has been detected on this machine — the reuse
17738
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17739
+ occurrenceCount: external_exports.number().int().nonnegative(),
17740
+ firstSeen: external_exports.string(),
17741
+ lastSeen: external_exports.string()
17742
+ });
17743
+ var PointerDescriptor = external_exports.object({
17744
+ category: DetectionCategory,
17745
+ provider: external_exports.string().optional(),
17746
+ maskedMatch: external_exports.string(),
17747
+ occurrences: external_exports.number().int().nonnegative(),
17748
+ firstSeen: external_exports.string(),
17749
+ lastSeen: external_exports.string()
17750
+ });
17751
+ var PointerIdentity = external_exports.object({
17752
+ ruleId: external_exports.string(),
17753
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17754
+ fingerprintKeyVersion: external_exports.number().int().positive()
17755
+ });
17756
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17757
+ var VaultDerefReason = external_exports.enum([
17758
+ "display",
17759
+ "explicit-reveal",
17760
+ "view-render",
17761
+ "model-input",
17762
+ "remediation",
17763
+ "purge"
17764
+ ]);
17765
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17766
+ var VaultDeref = external_exports.object({
17767
+ id: external_exports.guid(),
17768
+ pointerId: external_exports.string(),
17769
+ at: external_exports.string(),
17770
+ target: DetokenizeTarget,
17771
+ reason: VaultDerefReason,
17772
+ outcome: VaultDerefOutcome,
17773
+ // Present only on a model-target crossing that a reveal grant authorized.
17774
+ grantId: external_exports.string().optional(),
17775
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17776
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17777
+ pointerCount: external_exports.number().int().positive().default(1)
17778
+ });
17779
+ var VaultSightingKind = external_exports.enum([
17780
+ "prompt",
17781
+ "tool-input",
17782
+ "tool-output",
17783
+ "file",
17784
+ "transcript"
17785
+ ]);
17786
+ var VaultSighting = external_exports.object({
17787
+ location: external_exports.string(),
17788
+ kind: VaultSightingKind,
17789
+ firstSeen: external_exports.string(),
17790
+ lastSeen: external_exports.string()
17791
+ });
17792
+ var VaultInventoryEntry = external_exports.object({
17793
+ pointerId: external_exports.string(),
17794
+ category: DetectionCategory,
17795
+ provider: external_exports.string().optional(),
17796
+ maskedMatch: external_exports.string(),
17797
+ occurrences: external_exports.number().int().nonnegative(),
17798
+ firstSeen: external_exports.string(),
17799
+ lastSeen: external_exports.string(),
17800
+ // The active reveal-to-model grant covering this value, when one exists —
17801
+ // the inventory badges it, the row links to revocation.
17802
+ revealGrantId: external_exports.string().nullable(),
17803
+ sightings: external_exports.array(VaultSighting)
17804
+ });
17805
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17806
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17807
+ var MAX_VAULT_PAGE_LIMIT = 200;
17808
+ var ListVaultInventoryQuery = external_exports.object({
17809
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17810
+ // Opaque; names the last row of the page just served.
17811
+ cursor: external_exports.string().optional()
17812
+ });
17813
+ var ListVaultInventoryResponse = external_exports.object({
17814
+ // Vaulted values across the whole store, not just this page — cursor-
17815
+ // independent, so paging never changes what the count claims.
17816
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17817
+ items: external_exports.array(VaultInventoryEntry),
17818
+ // `null` once the last page is reached.
17819
+ nextCursor: external_exports.string().nullable()
17820
+ });
17821
+ var ListVaultReuseQuery = external_exports.object({
17822
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17823
+ cursor: external_exports.string().optional()
17824
+ });
17825
+ var ListVaultReuseResponse = external_exports.object({
17826
+ // Reused values across the whole store — the number the section's claim
17827
+ // ("values detected in more than one place") is about.
17828
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17829
+ items: external_exports.array(VaultInventoryEntry),
17830
+ nextCursor: external_exports.string().nullable()
17831
+ });
17832
+ var ListVaultDerefsQuery = external_exports.object({
17833
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17834
+ // hides them and counts them into `hiddenBatched` instead, so the model
17835
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17836
+ // over a Server Action, which preserves the type, never as a URL param.
17837
+ includeBatched: external_exports.boolean().optional(),
17838
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17839
+ cursor: external_exports.string().optional()
17840
+ });
17841
+ var ListVaultDerefsResponse = external_exports.object({
17842
+ items: external_exports.array(VaultDeref),
17843
+ nextCursor: external_exports.string().nullable(),
17844
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17845
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17846
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17847
+ hiddenBatched: external_exports.number().int().nonnegative()
17848
+ });
17849
+ var VaultKeyCustody = external_exports.string();
17850
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17851
+ var VaultConsent = external_exports.object({
17852
+ acknowledgedAt: external_exports.iso.datetime(),
17853
+ version: external_exports.number().int().positive()
17854
+ });
17855
+
17360
17856
  // ../../packages/schema/src/zod/local.ts
17361
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17857
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17362
17858
  var RunMode = external_exports.enum(["standalone"]);
17363
17859
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17364
17860
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17380,6 +17876,16 @@ var WorkspaceSettings = external_exports.object({
17380
17876
  // In-place egress extraction on the scan paths; disable to stop all Data
17381
17877
  // Shares writes.
17382
17878
  dataSharesInPlace: external_exports.boolean().default(true),
17879
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17880
+ // vault, instead of destroying them. Absent by default: this is a custody
17881
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17882
+ // Revoking stops future vaulting; it does not erase what is already stored —
17883
+ // purging the vault is the eraser.
17884
+ vaultConsent: VaultConsent.optional(),
17885
+ // Where the vault master key lives.
17886
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17887
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17888
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17383
17889
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17384
17890
  onboardedAt: external_exports.iso.datetime().optional(),
17385
17891
  // Records that the user consented to sending findings to the model API for
@@ -17712,7 +18218,7 @@ var TopSourcesQuery = external_exports.object({
17712
18218
  // Omit for both kinds.
17713
18219
  kind: external_exports.enum(SOURCE_KINDS).optional()
17714
18220
  });
17715
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18221
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17716
18222
  var ScanCoverageProvider = external_exports.object({
17717
18223
  provider: Provider,
17718
18224
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -17965,6 +18471,138 @@ function captureId(sessionId, contentHash, filePath = null) {
17965
18471
  );
17966
18472
  }
17967
18473
 
18474
+ // ../../packages/persistence/src/internal/snapshot.ts
18475
+ import { randomUUID } from "crypto";
18476
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18477
+ import { basename, dirname, join } from "path";
18478
+
18479
+ // ../../packages/persistence/src/paths.ts
18480
+ import {
18481
+ chmodSync,
18482
+ linkSync,
18483
+ lstatSync,
18484
+ mkdirSync,
18485
+ renameSync,
18486
+ rmSync,
18487
+ writeFileSync
18488
+ } from "fs";
18489
+ import { threadId } from "worker_threads";
18490
+ var DATA_DIR_MODE = 448;
18491
+ var DATA_FILE_MODE = 384;
18492
+ var DB_FILENAME = "aka.db";
18493
+ function isSymlink(path) {
18494
+ try {
18495
+ return lstatSync(path).isSymbolicLink();
18496
+ } catch {
18497
+ return false;
18498
+ }
18499
+ }
18500
+ function chmodBestEffort(path, mode) {
18501
+ if (isSymlink(path)) return;
18502
+ try {
18503
+ chmodSync(path, mode);
18504
+ } catch {
18505
+ }
18506
+ }
18507
+ function tightenDir(dir) {
18508
+ chmodBestEffort(dir, DATA_DIR_MODE);
18509
+ }
18510
+ function ensureDataDirSync(dir) {
18511
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18512
+ tightenDir(dir);
18513
+ }
18514
+ function dbSidecars(file2) {
18515
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18516
+ }
18517
+ function tightenFile(file2) {
18518
+ chmodBestEffort(file2, DATA_FILE_MODE);
18519
+ }
18520
+ function tightenPerms(file2) {
18521
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18522
+ }
18523
+
18524
+ // ../../packages/persistence/src/internal/snapshot.ts
18525
+ function backupPath(file2, tag) {
18526
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18527
+ }
18528
+ var STALE_PARTIAL_MS = 5 * 6e4;
18529
+ function reapStalePartials(file2) {
18530
+ const dir = dirname(file2);
18531
+ const prefix = `${basename(file2)}.`;
18532
+ let entries;
18533
+ try {
18534
+ entries = readdirSync(dir);
18535
+ } catch {
18536
+ return;
18537
+ }
18538
+ for (const name of entries) {
18539
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18540
+ const partial2 = join(dir, name);
18541
+ try {
18542
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18543
+ rmSync2(partial2, { force: true });
18544
+ }
18545
+ } catch {
18546
+ }
18547
+ }
18548
+ }
18549
+ function snapshotStore(db, backup) {
18550
+ const partial2 = `${backup}.partial`;
18551
+ try {
18552
+ rmSync2(partial2, { force: true });
18553
+ db.prepare("VACUUM INTO ?").run(partial2);
18554
+ tightenFile(partial2);
18555
+ renameSync2(partial2, backup);
18556
+ } catch (error51) {
18557
+ try {
18558
+ rmSync2(partial2, { force: true });
18559
+ } catch {
18560
+ }
18561
+ throw error51;
18562
+ }
18563
+ }
18564
+ function moveStoreAside(file2, backup) {
18565
+ const undo = [];
18566
+ renameSync2(file2, backup);
18567
+ undo.push([backup, file2]);
18568
+ try {
18569
+ for (const sidecar of dbSidecars(file2)) {
18570
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18571
+ try {
18572
+ renameSync2(sidecar, moved);
18573
+ undo.push([moved, sidecar]);
18574
+ } catch {
18575
+ rmSync2(sidecar, { force: true });
18576
+ }
18577
+ }
18578
+ } catch (error51) {
18579
+ for (const [from, to] of undo.reverse()) {
18580
+ try {
18581
+ renameSync2(from, to);
18582
+ } catch {
18583
+ }
18584
+ }
18585
+ throw error51;
18586
+ }
18587
+ tightenPerms(backup);
18588
+ }
18589
+ function discardStore(file2, backup) {
18590
+ try {
18591
+ rmSync2(file2, { force: true });
18592
+ for (const sidecar of dbSidecars(file2)) {
18593
+ rmSync2(sidecar, { force: true });
18594
+ }
18595
+ } catch (error51) {
18596
+ if (existsSync(file2)) {
18597
+ try {
18598
+ rmSync2(backup, { force: true });
18599
+ } catch {
18600
+ }
18601
+ }
18602
+ throw error51;
18603
+ }
18604
+ }
18605
+
17968
18606
  // ../../packages/persistence/src/internal/sql-text.ts
17969
18607
  function escapeLikePattern(s) {
17970
18608
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18106,38 +18744,6 @@ function mapRowsTolerant(rows, map2) {
18106
18744
  return out;
18107
18745
  }
18108
18746
 
18109
- // ../../packages/persistence/src/paths.ts
18110
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18111
- var DATA_DIR_MODE = 448;
18112
- var DATA_FILE_MODE = 384;
18113
- var DB_FILENAME = "aka.db";
18114
- function chmodBestEffort(path, mode) {
18115
- try {
18116
- chmodSync(path, mode);
18117
- } catch {
18118
- }
18119
- }
18120
- function tightenDir(dir) {
18121
- chmodBestEffort(dir, DATA_DIR_MODE);
18122
- }
18123
- function ensureDataDirSync(dir) {
18124
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18125
- tightenDir(dir);
18126
- }
18127
- function dbSidecars(file2) {
18128
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18129
- }
18130
- function tightenFile(file2) {
18131
- try {
18132
- if (lstatSync(file2).isSymbolicLink()) return;
18133
- } catch {
18134
- }
18135
- chmodBestEffort(file2, DATA_FILE_MODE);
18136
- }
18137
- function tightenPerms(file2) {
18138
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18139
- }
18140
-
18141
18747
  // ../../packages/persistence/src/migrations.ts
18142
18748
  function describeObject(object2) {
18143
18749
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18253,9 +18859,9 @@ function applyLegacyDropMigration(db, file2) {
18253
18859
  }
18254
18860
  }
18255
18861
  function backupBeforeLegacyDrop(db, file2) {
18256
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18257
- db.prepare("VACUUM INTO ?").run(backup);
18258
- tightenFile(backup);
18862
+ reapStalePartials(file2);
18863
+ const backup = backupPath(file2, "pre-drop");
18864
+ snapshotStore(db, backup);
18259
18865
  return backup;
18260
18866
  }
18261
18867
  var TOKEN_USAGE_COLUMNS = [
@@ -18599,6 +19205,25 @@ function parseJsonObject(s) {
18599
19205
  return void 0;
18600
19206
  }
18601
19207
 
19208
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19209
+ function encodeKeysetCursor(payload) {
19210
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19211
+ }
19212
+ function decodeKeysetCursor(cursor) {
19213
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19214
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19215
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19216
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19217
+ // a null cursor, which a caller reads as "end of list". That is the one
19218
+ // outcome a cursor that does not decode must never produce, since the
19219
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19220
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19221
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19222
+ return parsed;
19223
+ }
19224
+ return null;
19225
+ }
19226
+
18602
19227
  // ../../packages/persistence/src/repositories/activity.ts
18603
19228
  var DAY_MS = 864e5;
18604
19229
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18644,16 +19269,6 @@ function utcWindow(nowMs) {
18644
19269
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18645
19270
  return { startMs, endMs: startMs + DAY_MS };
18646
19271
  }
18647
- function encodeCursor(payload) {
18648
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18649
- }
18650
- function decodeCursor(cursor) {
18651
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18652
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18653
- return parsed;
18654
- }
18655
- return null;
18656
- }
18657
19272
  var DB_EVENT_TYPE_TO_KIND = {
18658
19273
  session: "session",
18659
19274
  prompt: "prompt",
@@ -18798,7 +19413,7 @@ var SqliteActivityRepository = class {
18798
19413
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18799
19414
  }
18800
19415
  listSessions(query) {
18801
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19416
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18802
19417
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18803
19418
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18804
19419
  const conditions = [SESSION_ROOT];
@@ -18872,7 +19487,7 @@ var SqliteActivityRepository = class {
18872
19487
  )
18873
19488
  );
18874
19489
  const last = page[page.length - 1];
18875
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19490
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18876
19491
  return Promise.resolve({ items, nextCursor, emptyCount });
18877
19492
  }
18878
19493
  getSession(sessionId) {
@@ -19745,7 +20360,7 @@ var SqliteEventsRepository = class {
19745
20360
  };
19746
20361
 
19747
20362
  // ../../packages/persistence/src/repositories/exceptions.ts
19748
- import { randomUUID } from "crypto";
20363
+ import { randomUUID as randomUUID2 } from "crypto";
19749
20364
 
19750
20365
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19751
20366
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19777,9 +20392,13 @@ var AmbiguousExceptionIdError = class extends Error {
19777
20392
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19778
20393
  AND (expires_at IS NULL OR expires_at > :now)
19779
20394
  AND (max_uses IS NULL OR use_count < max_uses)`;
20395
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
20396
+ AND conditions IS NULL
20397
+ AND ${ACTIVE_PREDICATE}`;
19780
20398
  var SqliteExceptionsRepository = class {
19781
- constructor(db) {
20399
+ constructor(db, now = () => Date.now()) {
19782
20400
  this.db = db;
20401
+ this.now = now;
19783
20402
  this.consumeStmt = db.prepare(
19784
20403
  `UPDATE exceptions
19785
20404
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19797,6 +20416,7 @@ var SqliteExceptionsRepository = class {
19797
20416
  );
19798
20417
  }
19799
20418
  db;
20419
+ now;
19800
20420
  consumeStmt;
19801
20421
  insertBlockedStmt;
19802
20422
  sweepBlockedStmt;
@@ -19823,8 +20443,8 @@ var SqliteExceptionsRepository = class {
19823
20443
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19824
20444
  );
19825
20445
  }
19826
- const id = randomUUID();
19827
- const now = Date.now();
20446
+ const id = randomUUID2();
20447
+ const now = this.now();
19828
20448
  try {
19829
20449
  this.insertExceptionRow(id, input, now);
19830
20450
  } catch (err) {
@@ -19868,11 +20488,11 @@ var SqliteExceptionsRepository = class {
19868
20488
  this.db.prepare(
19869
20489
  `INSERT INTO exceptions (
19870
20490
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19871
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19872
- conditions, created_by, created_via, created_at, updated_at
20491
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20492
+ justification, conditions, created_by, created_via, created_at, updated_at
19873
20493
  ) VALUES (
19874
20494
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19875
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20495
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19876
20496
  :conditions, :createdBy, :createdVia, :now, :now
19877
20497
  )`
19878
20498
  ).run({
@@ -19882,6 +20502,7 @@ var SqliteExceptionsRepository = class {
19882
20502
  valueFingerprint: input.valueFingerprint,
19883
20503
  keyVersion: input.keyVersion,
19884
20504
  maskedValue: input.maskedValue,
20505
+ capability: input.capability ?? "suppress",
19885
20506
  scope: input.scope,
19886
20507
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19887
20508
  maxUses: input.maxUses,
@@ -19901,7 +20522,7 @@ var SqliteExceptionsRepository = class {
19901
20522
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19902
20523
  const rows = allRows(
19903
20524
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19904
- opts?.includeTerminal ? {} : { now: Date.now() }
20525
+ opts?.includeTerminal ? {} : { now: this.now() }
19905
20526
  );
19906
20527
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19907
20528
  return Promise.resolve(exceptions);
@@ -19936,7 +20557,7 @@ var SqliteExceptionsRepository = class {
19936
20557
  * already revoked.
19937
20558
  */
19938
20559
  revoke(id, revokedBy, reason) {
19939
- const now = Date.now();
20560
+ const now = this.now();
19940
20561
  const result = this.db.prepare(
19941
20562
  `UPDATE exceptions
19942
20563
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -19950,7 +20571,7 @@ var SqliteExceptionsRepository = class {
19950
20571
  * callers must treat identically — means it does not and the detection is
19951
20572
  * enforced as usual. Deliberately NOT wrapped in try/catch.
19952
20573
  */
19953
- consume(id, now = Date.now()) {
20574
+ consume(id, now = this.now()) {
19954
20575
  const result = this.consumeStmt.run({ id, now });
19955
20576
  return Promise.resolve(Number(result.changes) === 1);
19956
20577
  }
@@ -19959,7 +20580,7 @@ var SqliteExceptionsRepository = class {
19959
20580
  * version — what rides the policy bundle to the hook. Grants written under
19960
20581
  * a different (rotated-away) key never match, so they are excluded at read.
19961
20582
  */
19962
- activeBundleEntries(keyVersion, now = Date.now()) {
20583
+ activeBundleEntries(keyVersion, now = this.now()) {
19963
20584
  const rows = allRows(
19964
20585
  this.db.prepare(
19965
20586
  `SELECT * FROM exceptions
@@ -19975,6 +20596,7 @@ var SqliteExceptionsRepository = class {
19975
20596
  ruleId: row.rule_id,
19976
20597
  valueFingerprint: row.value_fingerprint,
19977
20598
  keyVersion: row.key_version,
20599
+ capability: row.capability,
19978
20600
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19979
20601
  maxUses: row.max_uses,
19980
20602
  useCount: row.use_count,
@@ -19990,7 +20612,7 @@ var SqliteExceptionsRepository = class {
19990
20612
  * than the retention window on every write, so the ledger self-limits.
19991
20613
  */
19992
20614
  recordBlocked(entry) {
19993
- const now = Date.now();
20615
+ const now = this.now();
19994
20616
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
19995
20617
  this.insertBlockedStmt.run({
19996
20618
  reference: entry.reference,
@@ -20013,7 +20635,7 @@ var SqliteExceptionsRepository = class {
20013
20635
  WHERE blocked_at > :cutoff
20014
20636
  ORDER BY blocked_at DESC, rowid DESC`
20015
20637
  ),
20016
- { cutoff: Date.now() - windowMs }
20638
+ { cutoff: this.now() - windowMs }
20017
20639
  );
20018
20640
  return Promise.resolve(
20019
20641
  rows.map((row) => ({
@@ -20029,6 +20651,36 @@ var SqliteExceptionsRepository = class {
20029
20651
  }))
20030
20652
  );
20031
20653
  }
20654
+ /**
20655
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20656
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20657
+ * suppression uses — plus the capability: a suppression grant must never
20658
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20659
+ * revealed value re-enters the detection scan immediately afterward and the
20660
+ * suppression match there claims the use — one crossing, one use.
20661
+ *
20662
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20663
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20664
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20665
+ */
20666
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20667
+ try {
20668
+ const at = now ?? this.now();
20669
+ const row = getRow(
20670
+ this.db.prepare(
20671
+ `SELECT id FROM exceptions
20672
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20673
+ AND key_version = :keyVersion
20674
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20675
+ LIMIT 1`
20676
+ ),
20677
+ { ruleId, valueFingerprint, keyVersion, now: at }
20678
+ );
20679
+ return Promise.resolve(row ?? null);
20680
+ } catch (err) {
20681
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20682
+ }
20683
+ }
20032
20684
  /**
20033
20685
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20034
20686
  * exhausted) whose last transition is older than the retention window.
@@ -20036,7 +20688,7 @@ var SqliteExceptionsRepository = class {
20036
20688
  * predicate, so correctness never depends on this sweep; it only bounds how
20037
20689
  * long the audit evidence is kept locally. Returns the deleted count.
20038
20690
  */
20039
- sweepTerminal(retentionMs, now = Date.now()) {
20691
+ sweepTerminal(retentionMs, now = this.now()) {
20040
20692
  const result = this.db.prepare(
20041
20693
  `DELETE FROM exceptions
20042
20694
  WHERE updated_at < :cutoff
@@ -20056,6 +20708,7 @@ function parseExceptionRow(row) {
20056
20708
  valueFingerprint: row.value_fingerprint,
20057
20709
  keyVersion: row.key_version,
20058
20710
  maskedValue: row.masked_value,
20711
+ capability: row.capability,
20059
20712
  scope: row.scope,
20060
20713
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20061
20714
  maxUses: row.max_uses,
@@ -20098,6 +20751,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20098
20751
 
20099
20752
  // ../../packages/persistence/src/repositories/findings.ts
20100
20753
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20754
+ var SCAN_BATCH_ROWS = 1e3;
20755
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20756
+ var LOCATION_RULE_IDS_CAP = 20;
20757
+ function compareLocationOrder(a, b) {
20758
+ return compareFindingGroupOrder(
20759
+ {
20760
+ severity: a.maxSeverity,
20761
+ latestDetectedAt: a.latestDetectedAt,
20762
+ id: ""
20763
+ },
20764
+ {
20765
+ severity: b.maxSeverity,
20766
+ latestDetectedAt: b.latestDetectedAt,
20767
+ id: ""
20768
+ }
20769
+ );
20770
+ }
20101
20771
  var CONCAT_SEP = ",";
20102
20772
  var TUPLE_SEP = "|";
20103
20773
  function splitConcat(value) {
@@ -20110,6 +20780,33 @@ function deriveInstanceStatus(row) {
20110
20780
  latestResolutionStatus: row.latest_status
20111
20781
  });
20112
20782
  }
20783
+ function encodeGroupCursor(group) {
20784
+ const payload = {
20785
+ sev: group.severity,
20786
+ t: group.latestDetectedAt,
20787
+ id: group.id
20788
+ };
20789
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20790
+ }
20791
+ function decodeGroupCursor(cursor) {
20792
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20793
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20794
+ return {
20795
+ severity: parsed.sev,
20796
+ latestDetectedAt: parsed.t,
20797
+ id: parsed.id
20798
+ };
20799
+ }
20800
+ return null;
20801
+ }
20802
+ function firstAfter(sorted, cursor) {
20803
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20804
+ return index === -1 ? sorted.length : index;
20805
+ }
20806
+ function findDeepLinked(sorted, page, id) {
20807
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20808
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20809
+ }
20113
20810
  var DAY_MS3 = 864e5;
20114
20811
  var SqliteFindingsRepository = class {
20115
20812
  constructor(db) {
@@ -20219,8 +20916,13 @@ var SqliteFindingsRepository = class {
20219
20916
  */
20220
20917
  listGroupedFindings(query) {
20221
20918
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20222
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20223
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20919
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20920
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20921
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20922
+ const sessionParams = {
20923
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20924
+ ...fromMs === void 0 ? {} : { fromMs }
20925
+ };
20224
20926
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20225
20927
  predicate,
20226
20928
  params: sessionParams
@@ -20228,7 +20930,8 @@ var SqliteFindingsRepository = class {
20228
20930
  const rows = allRows(
20229
20931
  this.db.prepare(
20230
20932
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20231
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
20933
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
20934
+ kind, finding_key, latest_status
20232
20935
  FROM (
20233
20936
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20234
20937
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20238,6 +20941,7 @@ var SqliteFindingsRepository = class {
20238
20941
  json_extract(e.attributes, '$.repo') AS repo,
20239
20942
  json_extract(e.attributes, '$.file_path') AS file,
20240
20943
  json_extract(e.attributes, '$.tool_name') AS tool_name,
20944
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20241
20945
  e.event_type AS kind, f.finding_key AS finding_key,
20242
20946
  latest.status AS latest_status,
20243
20947
  ROW_NUMBER() OVER (
@@ -20269,6 +20973,8 @@ var SqliteFindingsRepository = class {
20269
20973
  repo: r.repo ?? "",
20270
20974
  file: r.file ?? "",
20271
20975
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
20976
+ eventId: r.event_id,
20977
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20272
20978
  status: deriveInstanceStatus(r)
20273
20979
  }));
20274
20980
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20292,18 +20998,23 @@ var SqliteFindingsRepository = class {
20292
20998
  groups: sorted.length
20293
20999
  };
20294
21000
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21001
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21002
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21003
+ const page = sorted.slice(start, start + limit);
21004
+ const lastOnPage = page.at(-1);
21005
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21006
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20295
21007
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20296
- const items = sorted.slice(0, limit).map(
20297
- (g) => statusSet ? {
20298
- ...g,
20299
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20300
- } : g
20301
- );
21008
+ const narrow = (g) => statusSet ? {
21009
+ ...g,
21010
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21011
+ } : g;
21012
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20302
21013
  return Promise.resolve({
20303
21014
  totals,
20304
21015
  facets,
20305
21016
  items,
20306
- nextCursor: null,
21017
+ nextCursor,
20307
21018
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20308
21019
  });
20309
21020
  }
@@ -20335,6 +21046,266 @@ var SqliteFindingsRepository = class {
20335
21046
  * request actually carries a `q`. (Substring matching is unaffected by a
20336
21047
  * path repeating across tuples.)
20337
21048
  */
21049
+ /**
21050
+ * The instance-level (flat) findings list: one row per finding, newest first,
21051
+ * paged by keyset.
21052
+ *
21053
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21054
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21055
+ * them changes no reported number. Severity, subtype, provider, action,
21056
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21057
+ * facet excludes its own filter, so a row the filter rejects still has to be
21058
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21059
+ * Several could not be expressed there anyway: status comes from the one
21060
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21061
+ * none of the mappers names", which no IN-list can say.
21062
+ *
21063
+ * The scan runs from the top of the scope on every request, not from the
21064
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21065
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21066
+ * while the counting runs, and only the page itself is retained.
21067
+ */
21068
+ listFindingInstances(query) {
21069
+ const opts = {
21070
+ severity: query.severity,
21071
+ subtype: query.subtype,
21072
+ providers: query.provider,
21073
+ actions: query.action,
21074
+ statuses: query.status,
21075
+ tools: query.tool,
21076
+ repo: query.repo,
21077
+ file: query.file,
21078
+ q: query.q
21079
+ };
21080
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21081
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21082
+ const accumulator = createInstanceFacetAccumulator(opts);
21083
+ const items = [];
21084
+ let total = 0;
21085
+ let last;
21086
+ let hasMore = false;
21087
+ for (const row of this.scanFindingRows({
21088
+ sessionId: query.sessionId,
21089
+ from: query.from
21090
+ })) {
21091
+ accumulator.add(row);
21092
+ if (!matchesInstanceFilters(row, opts)) continue;
21093
+ total += 1;
21094
+ if (items.length < limit) {
21095
+ items.push(toInstanceDetail(row));
21096
+ last = row;
21097
+ } else {
21098
+ hasMore = true;
21099
+ }
21100
+ }
21101
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21102
+ if (cursor !== null) {
21103
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21104
+ return Promise.resolve({
21105
+ totals: { findings: total },
21106
+ facets: accumulator.facets(),
21107
+ items: resumed.items,
21108
+ nextCursor: resumed.nextCursor
21109
+ });
21110
+ }
21111
+ return Promise.resolve({
21112
+ totals: { findings: total },
21113
+ facets: accumulator.facets(),
21114
+ items,
21115
+ nextCursor
21116
+ });
21117
+ }
21118
+ /**
21119
+ * The page of matching rows strictly after `cursor`. Separate from the
21120
+ * counting pass because that one starts at the top of the scope by design;
21121
+ * this one narrows the scan with the same keyset predicate the activity list
21122
+ * uses, so a later page costs less than the first rather than more.
21123
+ */
21124
+ pageAfter(cursor, opts, limit, query) {
21125
+ const items = [];
21126
+ let last;
21127
+ let hasMore = false;
21128
+ for (const row of this.scanFindingRows({
21129
+ sessionId: query.sessionId,
21130
+ from: query.from,
21131
+ after: cursor
21132
+ })) {
21133
+ if (!matchesInstanceFilters(row, opts)) continue;
21134
+ if (items.length < limit) {
21135
+ items.push(toInstanceDetail(row));
21136
+ last = row;
21137
+ } else {
21138
+ hasMore = true;
21139
+ break;
21140
+ }
21141
+ }
21142
+ return {
21143
+ items,
21144
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21145
+ };
21146
+ }
21147
+ /**
21148
+ * The same findings folded by location: repository, then file within it.
21149
+ *
21150
+ * The grouping keys come from the capturing event's attributes, which is what
21151
+ * the local store relates a finding to — there is no finding↔asset row to
21152
+ * group by instead. A repo or file the event did not record folds into the
21153
+ * empty-string bucket, which the view renders but does not link, since no
21154
+ * filter can name it.
21155
+ */
21156
+ listFindingLocations(query) {
21157
+ const opts = {
21158
+ severity: query.severity,
21159
+ subtype: query.subtype,
21160
+ providers: query.provider,
21161
+ actions: query.action,
21162
+ statuses: query.status,
21163
+ tools: query.tool,
21164
+ q: query.q
21165
+ };
21166
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21167
+ const byRepo = /* @__PURE__ */ new Map();
21168
+ let total = 0;
21169
+ for (const row of this.scanFindingRows({
21170
+ sessionId: query.sessionId,
21171
+ from: query.from
21172
+ })) {
21173
+ if (!matchesInstanceFilters(row, opts)) continue;
21174
+ total += 1;
21175
+ let files = byRepo.get(row.repo);
21176
+ if (files === void 0) {
21177
+ files = /* @__PURE__ */ new Map();
21178
+ byRepo.set(row.repo, files);
21179
+ }
21180
+ let acc = files.get(row.file);
21181
+ if (acc === void 0) {
21182
+ acc = newLocationAccumulator();
21183
+ files.set(row.file, acc);
21184
+ }
21185
+ addToLocation(acc, row);
21186
+ }
21187
+ let fileCount = 0;
21188
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21189
+ fileCount += files.size;
21190
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21191
+ file: file2,
21192
+ instanceCount: acc.instanceCount,
21193
+ maxSeverity: acc.maxSeverity,
21194
+ latestDetectedAt: acc.latestDetectedAt,
21195
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21196
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21197
+ })).sort(compareLocationOrder);
21198
+ const rollup = fileRows.reduce(
21199
+ (a, f) => ({
21200
+ instanceCount: a.instanceCount + f.instanceCount,
21201
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21202
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21203
+ }),
21204
+ {
21205
+ instanceCount: 0,
21206
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21207
+ latestDetectedAt: ""
21208
+ }
21209
+ );
21210
+ const statuses = fileRows.map((f) => f.status);
21211
+ const folded = foldGroupStatus(statuses);
21212
+ return {
21213
+ repo,
21214
+ instanceCount: rollup.instanceCount,
21215
+ maxSeverity: rollup.maxSeverity,
21216
+ latestDetectedAt: rollup.latestDetectedAt,
21217
+ ...folded === void 0 ? {} : { status: folded },
21218
+ files: fileRows
21219
+ };
21220
+ });
21221
+ repos.sort(compareLocationOrder);
21222
+ return Promise.resolve({
21223
+ totals: { findings: total, repos: repos.length, files: fileCount },
21224
+ items: repos.slice(0, limit),
21225
+ hasMore: repos.length > limit
21226
+ });
21227
+ }
21228
+ /**
21229
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21230
+ *
21231
+ * A generator so a caller streams the scope without it ever being an array:
21232
+ * the flat list counts and facets the whole filtered scope, which on a large
21233
+ * store is far more rows than any page. Each batch advances the same keyset
21234
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21235
+ * rather than one unbounded result set.
21236
+ *
21237
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21238
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21239
+ * makes it a point lookup per row, and the derived table would re-materialize
21240
+ * a window over the whole resolution table once per batch.
21241
+ *
21242
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21243
+ * would be missing from its own facet, which is computed by excluding that
21244
+ * dimension — see listFindingInstances.
21245
+ */
21246
+ *scanFindingRows(scope) {
21247
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21248
+ const params = [];
21249
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21250
+ conditions.push("e.root_session_id = ?");
21251
+ params.push(scope.sessionId);
21252
+ }
21253
+ if (scope.from !== void 0) {
21254
+ conditions.push("e.started_at >= ?");
21255
+ params.push(isoToEpochMillis(scope.from));
21256
+ }
21257
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21258
+ d.severity AS severity, f.masked_match AS masked_match,
21259
+ f.action_taken AS action_taken, f.confidence AS confidence,
21260
+ e.started_at AS occurred_at,
21261
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21262
+ json_extract(e.attributes, '$.repo') AS repo,
21263
+ json_extract(e.attributes, '$.file_path') AS file,
21264
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21265
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21266
+ e.event_type AS kind, f.finding_key AS finding_key,
21267
+ ${latestResolutionStatusSql("f")} AS latest_status
21268
+ FROM inspection_findings f
21269
+ JOIN audit_events e ON e.id = f.audit_event_id
21270
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21271
+ WHERE ${conditions.join(" AND ")}
21272
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21273
+ ORDER BY e.started_at DESC, f.id DESC
21274
+ LIMIT ?`;
21275
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21276
+ for (; ; ) {
21277
+ const rows = allRows(this.db.prepare(sql), [
21278
+ ...params,
21279
+ after.startedAtMs,
21280
+ after.startedAtMs,
21281
+ after.id,
21282
+ SCAN_BATCH_ROWS
21283
+ ]);
21284
+ for (const r of rows) {
21285
+ yield {
21286
+ id: r.id,
21287
+ ruleId: r.rule_id,
21288
+ category: r.category,
21289
+ severity: r.severity,
21290
+ maskedMatch: r.masked_match,
21291
+ actionTaken: r.action_taken,
21292
+ confidence: r.confidence,
21293
+ occurredAt: epochMillisToIso(r.occurred_at),
21294
+ sourceTool: r.source_tool,
21295
+ repo: r.repo ?? "",
21296
+ file: r.file ?? "",
21297
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21298
+ eventId: r.event_id,
21299
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21300
+ status: deriveInstanceStatus(r)
21301
+ };
21302
+ }
21303
+ if (rows.length < SCAN_BATCH_ROWS) return;
21304
+ const lastRow = rows[rows.length - 1];
21305
+ if (lastRow === void 0) return;
21306
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21307
+ }
21308
+ }
20338
21309
  groupAggregates(withSearchText, scope) {
20339
21310
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20340
21311
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20595,7 +21566,7 @@ var SqliteInspectionFindingsRepository = class {
20595
21566
  };
20596
21567
 
20597
21568
  // ../../packages/persistence/src/repositories/installed-packs.ts
20598
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21569
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20599
21570
 
20600
21571
  // ../../packages/persistence/src/semver.ts
20601
21572
  function parse3(version2) {
@@ -20746,7 +21717,7 @@ var SqliteInstalledPacksRepository = class {
20746
21717
  let behind = false;
20747
21718
  for (const row of rows) {
20748
21719
  const params = {
20749
- id: randomUUID2(),
21720
+ id: randomUUID3(),
20750
21721
  namespace: row.namespace,
20751
21722
  packId: row.packId,
20752
21723
  version: row.version,
@@ -20758,7 +21729,7 @@ var SqliteInstalledPacksRepository = class {
20758
21729
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20759
21730
  this.upsertAvailableStmt.run({
20760
21731
  ...params,
20761
- id: randomUUID2(),
21732
+ id: randomUUID3(),
20762
21733
  recordedBy: meta3?.recordedBy ?? null
20763
21734
  });
20764
21735
  } else {
@@ -21081,14 +22052,15 @@ var SqliteInventoryRepository = class {
21081
22052
  };
21082
22053
 
21083
22054
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21084
- import { randomUUID as randomUUID3 } from "crypto";
22055
+ import { randomUUID as randomUUID4 } from "crypto";
21085
22056
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21086
22057
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21087
22058
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21088
22059
  var HARNESS_LABELS = {
21089
22060
  claudecode: "Claude Code",
21090
22061
  cursor: "Cursor",
21091
- codex: "Codex"
22062
+ codex: "Codex",
22063
+ antigravity: "Antigravity"
21092
22064
  };
21093
22065
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21094
22066
  var EMPTY_PROJECT_AGG = {
@@ -21103,6 +22075,7 @@ function resolveHarnessId(attrs, row) {
21103
22075
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21104
22076
  if (t.includes("cursor")) return "cursor";
21105
22077
  if (t.includes("codex")) return "codex";
22078
+ if (t.includes("antigravity")) return "antigravity";
21106
22079
  return null;
21107
22080
  }
21108
22081
  function isLiveRealClaudeCode(rows) {
@@ -21561,7 +22534,7 @@ var SqliteInventoryAssetsRepository = class {
21561
22534
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21562
22535
  VALUES (:id, :projectId, :path, :access, :now, :now)
21563
22536
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21564
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22537
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21565
22538
  }
21566
22539
  return true;
21567
22540
  }
@@ -21582,7 +22555,7 @@ var SqliteInventoryAssetsRepository = class {
21582
22555
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21583
22556
  VALUES (:id, :assetId, :trust, :now, :now)
21584
22557
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21585
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22558
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21586
22559
  }
21587
22560
  this.configRowsCache = void 0;
21588
22561
  return "ok";
@@ -21879,7 +22852,7 @@ var SqliteInventoryAssetsRepository = class {
21879
22852
  };
21880
22853
 
21881
22854
  // ../../packages/persistence/src/repositories/policies.ts
21882
- import { randomUUID as randomUUID4 } from "crypto";
22855
+ import { randomUUID as randomUUID5 } from "crypto";
21883
22856
  var SqlitePoliciesRepository = class {
21884
22857
  constructor(db) {
21885
22858
  this.db = db;
@@ -21914,7 +22887,7 @@ var SqlitePoliciesRepository = class {
21914
22887
  failOpenTransaction(this.db, () => {
21915
22888
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
21916
22889
  stmt.run({
21917
- id: randomUUID4(),
22890
+ id: randomUUID5(),
21918
22891
  target: JSON.stringify({ category }),
21919
22892
  action,
21920
22893
  now: Date.now()
@@ -21934,7 +22907,7 @@ var SqlitePoliciesRepository = class {
21934
22907
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21935
22908
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
21936
22909
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21937
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22910
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
21938
22911
  }
21939
22912
  // Caps every global per-category policy currently set to block/redact down
21940
22913
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22002,7 +22975,7 @@ var SqlitePolicyCatalogRepository = class {
22002
22975
  };
22003
22976
 
22004
22977
  // ../../packages/persistence/src/repositories/project-files.ts
22005
- import { randomUUID as randomUUID5 } from "crypto";
22978
+ import { randomUUID as randomUUID6 } from "crypto";
22006
22979
  var SqliteProjectFilesRepository = class {
22007
22980
  constructor(db) {
22008
22981
  this.db = db;
@@ -22034,7 +23007,7 @@ var SqliteProjectFilesRepository = class {
22034
23007
  const stamp = Math.max(now, maxStamp + 1);
22035
23008
  for (const file2 of scan2.files) {
22036
23009
  this.upsertStmt.run({
22037
- id: randomUUID5(),
23010
+ id: randomUUID6(),
22038
23011
  projectId,
22039
23012
  path: file2.path,
22040
23013
  name: file2.name,
@@ -22048,7 +23021,7 @@ var SqliteProjectFilesRepository = class {
22048
23021
  };
22049
23022
 
22050
23023
  // ../../packages/persistence/src/repositories/resolutions.ts
22051
- import { randomUUID as randomUUID6 } from "crypto";
23024
+ import { randomUUID as randomUUID7 } from "crypto";
22052
23025
  var SqliteResolutionsRepository = class {
22053
23026
  constructor(db, now = () => Date.now()) {
22054
23027
  this.db = db;
@@ -22102,7 +23075,7 @@ var SqliteResolutionsRepository = class {
22102
23075
  */
22103
23076
  insertResolution(r) {
22104
23077
  this.insertStmt.run({
22105
- id: randomUUID6(),
23078
+ id: randomUUID7(),
22106
23079
  findingKey: r.findingKey,
22107
23080
  status: FindingStatus.parse(r.status),
22108
23081
  method: ResolutionMethod.parse(r.method),
@@ -22161,13 +23134,51 @@ var SqliteRuleProbeCacheRepository = class {
22161
23134
  this.readStmt = db.prepare(
22162
23135
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22163
23136
  );
23137
+ this.countQuarantinedStmt = db.prepare(
23138
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23139
+ );
23140
+ this.clearQuarantinedStmt = db.prepare(
23141
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23142
+ );
22164
23143
  }
22165
23144
  db;
22166
23145
  upsertStmt;
22167
23146
  readStmt;
23147
+ countQuarantinedStmt;
23148
+ clearQuarantinedStmt;
22168
23149
  getVerdict(ruleKey) {
22169
23150
  return getRow(this.readStmt, { ruleKey });
22170
23151
  }
23152
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23153
+ countQuarantined() {
23154
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23155
+ }
23156
+ /**
23157
+ * Forgets every quarantine verdict, so the rules behind them are measured
23158
+ * again on the next load. This is the undo for a verdict the machine reached
23159
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23160
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23161
+ * loaded or slow machine can reach about a rule that is in fact fine.
23162
+ *
23163
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23164
+ * keeping, and dropping it would make every rule pay the battery again.
23165
+ *
23166
+ * Reports `refused` from the write's own result rather than inferring it from
23167
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23168
+ * swallows a contended DELETE (another writer holding the lock past
23169
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23170
+ * leaves the count unchanged, which is indistinguishable from "there was
23171
+ * nothing to clear". An undo that reports success while the quarantines are
23172
+ * still in place is worse than one that fails, because the rules it claimed
23173
+ * to restore are silently still disabled.
23174
+ */
23175
+ clearQuarantined() {
23176
+ const before = this.countQuarantined();
23177
+ const committed = failOpenTransaction(this.db, () => {
23178
+ this.clearQuarantinedStmt.run();
23179
+ });
23180
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23181
+ }
22171
23182
  setVerdict(ruleKey, verdict, worstProbeMs) {
22172
23183
  failOpenTransaction(this.db, () => {
22173
23184
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -22221,6 +23232,419 @@ var SqliteScanLedgerRepository = class {
22221
23232
  }
22222
23233
  };
22223
23234
 
23235
+ // ../../packages/persistence/src/repositories/secret-vault.ts
23236
+ import { randomUUID as randomUUID8 } from "crypto";
23237
+ function pageLimit(requested, fallback) {
23238
+ if (requested === void 0) return fallback;
23239
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23240
+ }
23241
+ function encodeReuseCursor(payload) {
23242
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23243
+ }
23244
+ function decodeReuseCursor(cursor) {
23245
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23246
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23247
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23248
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23249
+ // malformed cursor must never produce, since restarting from the top is the
23250
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23251
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23252
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23253
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23254
+ }
23255
+ return null;
23256
+ }
23257
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23258
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23259
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23260
+ v.occurrence_count, v.first_seen, v.last_seen`;
23261
+ function toSighting(row) {
23262
+ return {
23263
+ location: row.location,
23264
+ kind: row.kind,
23265
+ firstSeen: new Date(row.first_seen).toISOString(),
23266
+ lastSeen: new Date(row.last_seen).toISOString()
23267
+ };
23268
+ }
23269
+ var SELECT_COLUMNS = `
23270
+ pointer_id AS pointerId,
23271
+ value_fingerprint AS valueFingerprint,
23272
+ fingerprint_key_version AS fingerprintKeyVersion,
23273
+ key_version AS keyVersion,
23274
+ format_version AS formatVersion,
23275
+ category,
23276
+ rule_id AS ruleId,
23277
+ masked_match AS maskedMatch,
23278
+ provider,
23279
+ ciphertext,
23280
+ nonce,
23281
+ auth_tag AS authTag,
23282
+ occurrence_count AS occurrenceCount,
23283
+ first_seen AS firstSeen,
23284
+ last_seen AS lastSeen`;
23285
+ function toRow(raw) {
23286
+ const { provider, ...rest } = raw;
23287
+ return provider === null ? rest : { ...rest, provider };
23288
+ }
23289
+ var SqliteSecretVaultRepository = class {
23290
+ constructor(db) {
23291
+ this.db = db;
23292
+ this.insertStmt = db.prepare(
23293
+ `INSERT INTO secret_vault (
23294
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
23295
+ format_version, category, rule_id, masked_match, provider,
23296
+ ciphertext, nonce, auth_tag,
23297
+ occurrence_count, first_seen, last_seen
23298
+ ) VALUES (
23299
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
23300
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
23301
+ :ciphertext, :nonce, :authTag,
23302
+ 1, :now, :now
23303
+ )`
23304
+ );
23305
+ this.bumpStmt = db.prepare(
23306
+ `UPDATE secret_vault
23307
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
23308
+ WHERE value_fingerprint = :valueFingerprint`
23309
+ );
23310
+ this.byPointerStmt = db.prepare(
23311
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
23312
+ );
23313
+ this.byFingerprintStmt = db.prepare(
23314
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
23315
+ );
23316
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
23317
+ this.replaceCiphertextStmt = db.prepare(
23318
+ `UPDATE secret_vault
23319
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
23320
+ WHERE pointer_id = :pointerId`
23321
+ );
23322
+ this.refreshFingerprintStmt = db.prepare(
23323
+ `UPDATE secret_vault
23324
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
23325
+ WHERE pointer_id = :pointerId`
23326
+ );
23327
+ this.derefStmt = db.prepare(
23328
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
23329
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
23330
+ );
23331
+ }
23332
+ db;
23333
+ insertStmt;
23334
+ bumpStmt;
23335
+ byPointerStmt;
23336
+ byFingerprintStmt;
23337
+ listStmt;
23338
+ replaceCiphertextStmt;
23339
+ refreshFingerprintStmt;
23340
+ derefStmt;
23341
+ /**
23342
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
23343
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
23344
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
23345
+ * pointer, category and ciphertext, so the same secret always resolves to one
23346
+ * wire token. `minted` is true only when this call created the row.
23347
+ *
23348
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
23349
+ * writers cannot both decide they are minting.
23350
+ */
23351
+ upsert(input, now) {
23352
+ let minted = false;
23353
+ withTransaction(
23354
+ this.db,
23355
+ () => {
23356
+ const existing = getRow(this.byFingerprintStmt, {
23357
+ valueFingerprint: input.valueFingerprint
23358
+ });
23359
+ if (existing === void 0) {
23360
+ this.insertStmt.run(
23361
+ bindParams({
23362
+ pointerId: input.pointerId,
23363
+ valueFingerprint: input.valueFingerprint,
23364
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
23365
+ keyVersion: input.keyVersion,
23366
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
23367
+ category: input.category,
23368
+ ruleId: input.ruleId,
23369
+ maskedMatch: input.maskedMatch,
23370
+ provider: input.provider,
23371
+ ciphertext: input.ciphertext,
23372
+ nonce: input.nonce,
23373
+ authTag: input.authTag,
23374
+ now
23375
+ })
23376
+ );
23377
+ minted = true;
23378
+ return;
23379
+ }
23380
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
23381
+ },
23382
+ "IMMEDIATE"
23383
+ );
23384
+ const row = getRow(this.byFingerprintStmt, {
23385
+ valueFingerprint: input.valueFingerprint
23386
+ });
23387
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
23388
+ return { row: toRow(row), minted };
23389
+ }
23390
+ byPointerId(pointerId) {
23391
+ const raw = getRow(this.byPointerStmt, { pointerId });
23392
+ return raw === void 0 ? null : toRow(raw);
23393
+ }
23394
+ byValueFingerprint(fingerprint) {
23395
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
23396
+ return raw === void 0 ? null : toRow(raw);
23397
+ }
23398
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
23399
+ recordDeref(entry) {
23400
+ this.derefStmt.run(
23401
+ bindParams({
23402
+ id: entry.id,
23403
+ pointerId: entry.pointerId,
23404
+ at: entry.at,
23405
+ target: entry.target,
23406
+ reason: entry.reason,
23407
+ outcome: entry.outcome,
23408
+ grantId: entry.grantId,
23409
+ pointerCount: entry.pointerCount ?? 1
23410
+ })
23411
+ );
23412
+ }
23413
+ listAll() {
23414
+ return allRows(this.listStmt).map(toRow);
23415
+ }
23416
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
23417
+ replaceCiphertext(pointerId, next) {
23418
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
23419
+ }
23420
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
23421
+ refreshFingerprint(pointerId, next) {
23422
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
23423
+ }
23424
+ /**
23425
+ * Destroy every vaulted value and report how many were destroyed. The deref
23426
+ * audit is left alone on purpose — see the table note above.
23427
+ */
23428
+ purgeAll() {
23429
+ let destroyed = 0;
23430
+ withTransaction(
23431
+ this.db,
23432
+ () => {
23433
+ destroyed = this.countEntries();
23434
+ this.db.exec("DELETE FROM secret_vault");
23435
+ },
23436
+ "IMMEDIATE"
23437
+ );
23438
+ return destroyed;
23439
+ }
23440
+ /**
23441
+ * Record (or re-stamp) one place a pointer has been written. One row per
23442
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
23443
+ * on hook paths — a failure must never affect the rewrite that triggered it,
23444
+ * so callers wrap this, not the other way around.
23445
+ */
23446
+ recordSighting(entry, now) {
23447
+ this.db.prepare(
23448
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
23449
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
23450
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
23451
+ ).run({
23452
+ id: randomUUID8(),
23453
+ pointerId: entry.pointerId,
23454
+ location: entry.location,
23455
+ kind: entry.kind,
23456
+ now
23457
+ });
23458
+ }
23459
+ /**
23460
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23461
+ * than one query per row. A pointer with no sightings still gets an entry, so
23462
+ * the caller never has to distinguish "none" from "missing".
23463
+ *
23464
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23465
+ * the instance the way the fixed-shape ones in the constructor are.
23466
+ */
23467
+ sightingsFor(pointerIds) {
23468
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23469
+ if (pointerIds.length === 0) return byPointer;
23470
+ const rows = allRows(
23471
+ this.db.prepare(
23472
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23473
+ FROM secret_vault_sighting
23474
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23475
+ ORDER BY last_seen DESC`
23476
+ ),
23477
+ pointerIds
23478
+ );
23479
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23480
+ return byPointer;
23481
+ }
23482
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23483
+ toInventoryEntries(rows) {
23484
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
23485
+ return rows.map((r) => ({
23486
+ pointerId: r.pointer_id,
23487
+ category: r.category,
23488
+ ...r.provider === null ? {} : { provider: r.provider },
23489
+ maskedMatch: r.masked_match,
23490
+ occurrences: r.occurrence_count,
23491
+ firstSeen: new Date(r.first_seen).toISOString(),
23492
+ lastSeen: new Date(r.last_seen).toISOString(),
23493
+ revealGrantId: r.grant_id,
23494
+ sightings: sightings.get(r.pointer_id) ?? []
23495
+ }));
23496
+ }
23497
+ /**
23498
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23499
+ * value's descriptor data joined with its sightings and the active
23500
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23501
+ * the fingerprint nor the ciphertext columns are selected.
23502
+ *
23503
+ * `totals.values` counts the whole store, not the page, so the count a reader
23504
+ * sees never depends on how far they have paged.
23505
+ */
23506
+ listInventory(query = {}, now = Date.now()) {
23507
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23508
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23509
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
23510
+ const rows = allRows(
23511
+ this.db.prepare(
23512
+ `SELECT ${INVENTORY_COLUMNS},
23513
+ (SELECT e.id FROM exceptions e
23514
+ WHERE e.rule_id = v.rule_id
23515
+ AND e.value_fingerprint = v.value_fingerprint
23516
+ AND e.key_version = v.fingerprint_key_version
23517
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23518
+ LIMIT 1) AS grant_id
23519
+ FROM secret_vault v
23520
+ ${where}
23521
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23522
+ LIMIT :limit`
23523
+ ),
23524
+ bindParams({
23525
+ now,
23526
+ limit: limit + 1,
23527
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23528
+ })
23529
+ );
23530
+ const hasMore = rows.length > limit;
23531
+ const page = hasMore ? rows.slice(0, limit) : rows;
23532
+ const last = page[page.length - 1];
23533
+ return {
23534
+ totals: { values: this.countEntries() },
23535
+ items: this.toInventoryEntries(page),
23536
+ // Minted from the last row of the PAGE, never the extra probe row.
23537
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23538
+ };
23539
+ }
23540
+ /**
23541
+ * Values reused on this machine — detected more than once, or written to more
23542
+ * than one location — most-reused first, one page at a time.
23543
+ *
23544
+ * Its own read rather than a filter over an inventory page: reuse is a
23545
+ * property of the whole store, and deriving it from 50 newest rows would
23546
+ * under-report exactly the values a reader most needs to see.
23547
+ */
23548
+ listReuse(query = {}, now = Date.now()) {
23549
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23550
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23551
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23552
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23553
+ const rows = allRows(
23554
+ this.db.prepare(
23555
+ `SELECT ${INVENTORY_COLUMNS},
23556
+ (SELECT e.id FROM exceptions e
23557
+ WHERE e.rule_id = v.rule_id
23558
+ AND e.value_fingerprint = v.value_fingerprint
23559
+ AND e.key_version = v.fingerprint_key_version
23560
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23561
+ LIMIT 1) AS grant_id
23562
+ FROM secret_vault v
23563
+ WHERE ${REUSED_PREDICATE} ${after}
23564
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23565
+ LIMIT :limit`
23566
+ ),
23567
+ bindParams({
23568
+ now,
23569
+ limit: limit + 1,
23570
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23571
+ })
23572
+ );
23573
+ const hasMore = rows.length > limit;
23574
+ const page = hasMore ? rows.slice(0, limit) : rows;
23575
+ const last = page[page.length - 1];
23576
+ return {
23577
+ totals: { reused: this.countReused() },
23578
+ items: this.toInventoryEntries(page),
23579
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23580
+ };
23581
+ }
23582
+ /**
23583
+ * The de-reference trail, newest first, one page at a time. By default the
23584
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23585
+ * instead — the rows that matter as a signal are the model crossings, and
23586
+ * burying them under render noise would defeat the audit's purpose.
23587
+ *
23588
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23589
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23590
+ * the reader pages.
23591
+ */
23592
+ listDerefs(query = {}) {
23593
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23594
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23595
+ const conditions = [];
23596
+ if (query.includeBatched !== true) {
23597
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23598
+ }
23599
+ if (cursor !== null) {
23600
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23601
+ }
23602
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
23603
+ const rows = allRows(
23604
+ this.db.prepare(
23605
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
23606
+ FROM secret_vault_deref ${where}
23607
+ ORDER BY at DESC, id DESC LIMIT :limit`
23608
+ ),
23609
+ bindParams({
23610
+ limit: limit + 1,
23611
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23612
+ })
23613
+ );
23614
+ const hasMore = rows.length > limit;
23615
+ const page = hasMore ? rows.slice(0, limit) : rows;
23616
+ const last = page[page.length - 1];
23617
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
23618
+ this.db,
23619
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
23620
+ );
23621
+ return {
23622
+ items: page.map((r) => ({
23623
+ id: r.id,
23624
+ pointerId: r.pointer_id,
23625
+ at: new Date(r.at).toISOString(),
23626
+ target: r.target,
23627
+ reason: r.reason,
23628
+ outcome: r.outcome,
23629
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
23630
+ pointerCount: r.pointer_count
23631
+ })),
23632
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
23633
+ hiddenBatched
23634
+ };
23635
+ }
23636
+ countEntries() {
23637
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
23638
+ }
23639
+ /** Values reused on this machine — the reuse list's page-independent total. */
23640
+ countReused() {
23641
+ return countScalar(
23642
+ this.db,
23643
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23644
+ );
23645
+ }
23646
+ };
23647
+
22224
23648
  // ../../packages/persistence/src/repositories/security.ts
22225
23649
  var DAY_MS4 = 864e5;
22226
23650
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22233,7 +23657,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22233
23657
  var SCAN_COVERAGE = [
22234
23658
  { provider: "claudecode", coverage: 100, supported: true },
22235
23659
  { provider: "cursor", coverage: 0, supported: false },
22236
- { provider: "codex", coverage: 0, supported: false },
23660
+ { provider: "codex", coverage: 80, supported: true },
23661
+ { provider: "antigravity", coverage: 60, supported: true },
23662
+ { provider: "claudeai", coverage: 0, supported: false },
22237
23663
  { provider: "chatgpt", coverage: 0, supported: false },
22238
23664
  { provider: "copilot", coverage: 0, supported: false },
22239
23665
  { provider: "api", coverage: 0, supported: false }
@@ -22566,7 +23992,7 @@ var SqliteSecurityRepository = class {
22566
23992
  };
22567
23993
 
22568
23994
  // ../../packages/persistence/src/repositories/shares.ts
22569
- import { randomUUID as randomUUID7 } from "crypto";
23995
+ import { randomUUID as randomUUID9 } from "crypto";
22570
23996
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22571
23997
  var IN_CHUNK = 500;
22572
23998
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22822,7 +24248,7 @@ var SqliteSharesRepository = class {
22822
24248
  (id, destination_id, host, decision, created_at, updated_at)
22823
24249
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22824
24250
  ).run({
22825
- id: randomUUID7(),
24251
+ id: randomUUID9(),
22826
24252
  destinationId,
22827
24253
  host: dest.host,
22828
24254
  decision,
@@ -22971,7 +24397,7 @@ var SqliteSharesRepository = class {
22971
24397
  let destinationId = destIds.get(hit.host);
22972
24398
  if (destinationId === void 0) {
22973
24399
  destStmt.run({
22974
- id: randomUUID7(),
24400
+ id: randomUUID9(),
22975
24401
  kind: hit.kind,
22976
24402
  name: hit.name,
22977
24403
  host: hit.host,
@@ -22987,7 +24413,7 @@ var SqliteSharesRepository = class {
22987
24413
  let endpointId = endpointIds.get(endpointKey);
22988
24414
  if (endpointId === void 0) {
22989
24415
  endpointStmt.run({
22990
- id: randomUUID7(),
24416
+ id: randomUUID9(),
22991
24417
  destinationId,
22992
24418
  method: hit.method,
22993
24419
  transport: hit.transport,
@@ -23000,7 +24426,7 @@ var SqliteSharesRepository = class {
23000
24426
  endpointIds.set(endpointKey, endpointId);
23001
24427
  }
23002
24428
  siteStmt.run({
23003
- id: randomUUID7(),
24429
+ id: randomUUID9(),
23004
24430
  endpointId,
23005
24431
  project: input.project,
23006
24432
  projectKey: input.projectKey,
@@ -23365,6 +24791,9 @@ function purgeSampleData(db) {
23365
24791
  }
23366
24792
 
23367
24793
  // ../../packages/persistence/src/database.ts
24794
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24795
+ "aka.persistence.unsafeTestOnlyRawHandle"
24796
+ );
23368
24797
  function linkHost(input, hostId) {
23369
24798
  return hostId ? { ...input, hostId } : input;
23370
24799
  }
@@ -23386,21 +24815,34 @@ function openWithPragmas(file2) {
23386
24815
  }
23387
24816
  return db;
23388
24817
  }
23389
- function backupLegacyStore(file2) {
23390
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23391
- renameSync2(file2, backup);
23392
- tightenFile(backup);
23393
- for (const sidecar of dbSidecars(file2)) {
23394
- if (existsSync(sidecar)) rmSync2(sidecar);
24818
+ function backupLegacyStore(db, file2) {
24819
+ reapStalePartials(file2);
24820
+ const backup = backupPath(file2, "legacy");
24821
+ let snapshotted = false;
24822
+ let snapshotError;
24823
+ try {
24824
+ snapshotStore(db, backup);
24825
+ snapshotted = true;
24826
+ } catch (error51) {
24827
+ snapshotError = error51;
24828
+ } finally {
24829
+ db.close();
23395
24830
  }
24831
+ if (!snapshotted) {
24832
+ akaWarn(
24833
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24834
+ );
24835
+ moveStoreAside(file2, backup);
24836
+ return backup;
24837
+ }
24838
+ discardStore(file2, backup);
23396
24839
  return backup;
23397
24840
  }
23398
24841
  function openAndInitialize(file2) {
23399
24842
  let db = openWithPragmas(file2);
23400
24843
  try {
23401
24844
  if (isForeignSqliteLineage(db)) {
23402
- db.close();
23403
- const backup = backupLegacyStore(file2);
24845
+ const backup = backupLegacyStore(db, file2);
23404
24846
  db = openWithPragmas(file2);
23405
24847
  akaWarn(
23406
24848
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23416,6 +24858,7 @@ function openAndInitialize(file2) {
23416
24858
  policies,
23417
24859
  installedPacks,
23418
24860
  scanLedger: new SqliteScanLedgerRepository(db),
24861
+ secretVault: new SqliteSecretVaultRepository(db),
23419
24862
  exceptions: new SqliteExceptionsRepository(db),
23420
24863
  resolutions: new SqliteResolutionsRepository(db),
23421
24864
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23443,7 +24886,7 @@ function openAndInitialize(file2) {
23443
24886
  }
23444
24887
  function openLocalDatabase(dir) {
23445
24888
  ensureDataDirSync(dir);
23446
- const file2 = join(dir, DB_FILENAME);
24889
+ const file2 = join2(dir, DB_FILENAME);
23447
24890
  const {
23448
24891
  db,
23449
24892
  events,
@@ -23451,6 +24894,7 @@ function openLocalDatabase(dir) {
23451
24894
  policies,
23452
24895
  installedPacks,
23453
24896
  scanLedger,
24897
+ secretVault,
23454
24898
  exceptions,
23455
24899
  resolutions,
23456
24900
  ruleProbeCache,
@@ -23559,7 +25003,7 @@ function openLocalDatabase(dir) {
23559
25003
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23560
25004
  if (!definitionId) continue;
23561
25005
  inspectionFindings.insertFinding({
23562
- id: randomUUID8(),
25006
+ id: randomUUID10(),
23563
25007
  auditEventId: record2.scanEvent.id,
23564
25008
  inspectionDefinitionId: definitionId,
23565
25009
  span: finding.span,
@@ -23636,6 +25080,7 @@ function openLocalDatabase(dir) {
23636
25080
  policies,
23637
25081
  installedPacks,
23638
25082
  scanLedger,
25083
+ secretVault,
23639
25084
  exceptions,
23640
25085
  resolutions,
23641
25086
  ruleProbeCache,
@@ -23664,22 +25109,38 @@ function openLocalDatabase(dir) {
23664
25109
  transaction,
23665
25110
  close: () => {
23666
25111
  db.close();
23667
- }
25112
+ },
25113
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25114
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
23668
25115
  };
23669
25116
  }
23670
25117
 
25118
+ // ../../packages/persistence/src/file-lock.ts
25119
+ import { randomUUID as randomUUID11 } from "crypto";
25120
+ import {
25121
+ closeSync,
25122
+ existsSync as existsSync2,
25123
+ openSync,
25124
+ readFileSync,
25125
+ rmSync as rmSync3,
25126
+ statSync as statSync2,
25127
+ writeFileSync as writeFileSync2
25128
+ } from "fs";
25129
+ import { hostname as hostname3 } from "os";
25130
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25131
+
23671
25132
  // ../../packages/persistence/src/finding-key.ts
23672
25133
  import { createHash as createHash3 } from "crypto";
23673
25134
 
23674
25135
  // ../../packages/persistence/src/fingerprint.ts
23675
25136
  import { createHmac, randomBytes } from "crypto";
23676
- import { existsSync as existsSync2, readFileSync } from "fs";
23677
- import { join as join2 } from "path";
25137
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25138
+ import { join as join3 } from "path";
23678
25139
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23679
- var KEY_FILENAME = "exception.key";
25140
+ var EXCEPTION_KEY_FILENAME = "exception.key";
23680
25141
  var KEY_MATERIAL_BYTES = 32;
23681
25142
  function keyFilePath(dataDir2) {
23682
- return join2(dataDir2, KEY_FILENAME);
25143
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
23683
25144
  }
23684
25145
  function parseKeyFile(raw) {
23685
25146
  const parsed = JSON.parse(raw);
@@ -23702,7 +25163,7 @@ function parseKeyFile(raw) {
23702
25163
  function readFingerprintKey(dataDir2) {
23703
25164
  let raw;
23704
25165
  try {
23705
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25166
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
23706
25167
  } catch (err) {
23707
25168
  if (err.code === "ENOENT") return null;
23708
25169
  throw err instanceof Error ? err : new Error(String(err));
@@ -23714,18 +25175,18 @@ function readFingerprintKey(dataDir2) {
23714
25175
  import { renameSync as renameSync3 } from "fs";
23715
25176
  import { mkdir } from "fs/promises";
23716
25177
  import { homedir } from "os";
23717
- import { join as join3 } from "path";
25178
+ import { join as join4 } from "path";
23718
25179
  function defaultDataDir() {
23719
- return join3(homedir(), ".aka");
25180
+ return join4(homedir(), ".aka");
23720
25181
  }
23721
25182
  function settingsDir(base = defaultDataDir()) {
23722
- return join3(base, "settings");
25183
+ return join4(base, "settings");
23723
25184
  }
23724
25185
  function dataDir(base = defaultDataDir()) {
23725
- return join3(base, "data");
25186
+ return join4(base, "data");
23726
25187
  }
23727
25188
  function dbPath(base = defaultDataDir()) {
23728
- return join3(dataDir(base), "aka.db");
25189
+ return join4(dataDir(base), "aka.db");
23729
25190
  }
23730
25191
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23731
25192
  ensureDataDirSync(dir);
@@ -23738,8 +25199,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23738
25199
  for (const { name, dest } of moves) {
23739
25200
  try {
23740
25201
  ensureDataDirSync(dest);
23741
- const moved = join3(dest, name);
23742
- renameSync3(join3(base, name), moved);
25202
+ const moved = join4(dest, name);
25203
+ renameSync3(join4(base, name), moved);
23743
25204
  tightenFile(moved);
23744
25205
  } catch {
23745
25206
  }
@@ -23747,10 +25208,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23747
25208
  }
23748
25209
 
23749
25210
  // ../../packages/persistence/src/settings.ts
23750
- import { readFileSync as readFileSync2 } from "fs";
23751
- import { join as join4 } from "path";
25211
+ import { readFileSync as readFileSync3 } from "fs";
25212
+ import { join as join5 } from "path";
25213
+ var SETTINGS_FILENAME = "settings.json";
23752
25214
  function readWorkspaceSettings(base = defaultDataDir()) {
23753
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25215
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
23754
25216
  if (!record2) return defaultWorkspaceSettings();
23755
25217
  try {
23756
25218
  return WorkspaceSettings.parse(record2);
@@ -23761,30 +25223,56 @@ function readWorkspaceSettings(base = defaultDataDir()) {
23761
25223
  function readJson(file2) {
23762
25224
  let text;
23763
25225
  try {
23764
- text = readFileSync2(file2, "utf8");
25226
+ text = readFileSync3(file2, "utf8");
23765
25227
  } catch {
23766
25228
  return null;
23767
25229
  }
23768
25230
  return parseJsonObject(text) ?? null;
23769
25231
  }
23770
25232
 
25233
+ // ../../packages/persistence/src/vault/crypto.ts
25234
+ import {
25235
+ createCipheriv,
25236
+ createDecipheriv,
25237
+ createHmac as createHmac2,
25238
+ hkdfSync,
25239
+ timingSafeEqual
25240
+ } from "crypto";
25241
+
25242
+ // ../../packages/persistence/src/vault/key-provider.ts
25243
+ import { execFileSync } from "child_process";
25244
+ import { randomBytes as randomBytes2 } from "crypto";
25245
+ import {
25246
+ chmodSync as chmodSync2,
25247
+ mkdirSync as mkdirSync2,
25248
+ readFileSync as readFileSync4,
25249
+ renameSync as renameSync4,
25250
+ rmSync as rmSync4,
25251
+ statSync as statSync3,
25252
+ writeFileSync as writeFileSync3
25253
+ } from "fs";
25254
+ import { join as join6 } from "path";
25255
+
25256
+ // ../../packages/persistence/src/vault/vault.ts
25257
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
25258
+
23771
25259
  // ../../packages/persistence/src/warn-era-cap.ts
23772
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
23773
- import { join as join5 } from "path";
25260
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
25261
+ import { join as join7 } from "path";
23774
25262
  var MARKER = "warn-era-capped";
23775
25263
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23776
25264
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23777
- const marker = join5(dataDir2, MARKER);
23778
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25265
+ const marker = join7(dataDir2, MARKER);
25266
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
23779
25267
  const capped = db.policies.capCategoryActions();
23780
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
25268
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
23781
25269
  `, { mode: DATA_FILE_MODE });
23782
25270
  return { capped };
23783
25271
  }
23784
25272
 
23785
25273
  // ../../packages/plugin-sdk/src/config.ts
23786
- import { existsSync as existsSync4 } from "fs";
23787
- import { join as join6 } from "path";
25274
+ import { existsSync as existsSync5 } from "fs";
25275
+ import { join as join8 } from "path";
23788
25276
 
23789
25277
  // ../../packages/plugin-sdk/src/provider-env.ts
23790
25278
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -23835,11 +25323,11 @@ function resolveProvider() {
23835
25323
  }
23836
25324
 
23837
25325
  // ../../packages/plugin-sdk/src/config.ts
23838
- function loadConfig(base = defaultDataDir()) {
25326
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
23839
25327
  try {
23840
25328
  ensureLayoutDirSync(base);
23841
- const settingsFile = join6(settingsDir(base), "settings.json");
23842
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
25329
+ const settingsFile = join8(settingsDir(base), "settings.json");
25330
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
23843
25331
  } catch {
23844
25332
  }
23845
25333
  migrateLegacyLayout(base);
@@ -23850,21 +25338,21 @@ function loadConfig(base = defaultDataDir()) {
23850
25338
  dbPath: dbPath(base),
23851
25339
  settingsDir: settingsDir(base),
23852
25340
  onboarded: settings.onboardedAt != null,
23853
- provider: resolveProviderSafe()
25341
+ provider: resolveProviderSafe(resolveProviderFn)
23854
25342
  };
23855
25343
  }
23856
- function resolveProviderSafe() {
25344
+ function resolveProviderSafe(resolveProviderFn) {
23857
25345
  try {
23858
- return resolveProvider();
25346
+ return resolveProviderFn();
23859
25347
  } catch {
23860
25348
  return { provider: "anthropic" };
23861
25349
  }
23862
25350
  }
23863
25351
 
23864
25352
  // ../../packages/plugin-sdk/src/config-inventory.ts
23865
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
25353
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
23866
25354
  import { homedir as homedir2 } from "os";
23867
- import { basename as basename2, join as join8 } from "path";
25355
+ import { basename as basename3, join as join10 } from "path";
23868
25356
 
23869
25357
  // ../../packages/detections/src/egress/registry.ts
23870
25358
  var EXTRACTOR_VERSION = "1";
@@ -26228,7 +27716,7 @@ var gcp_service_account_default = {
26228
27716
  severity: "critical",
26229
27717
  matcher: {
26230
27718
  type: "regex",
26231
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
27719
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
26232
27720
  flags: "g"
26233
27721
  },
26234
27722
  examples: [
@@ -26600,40 +28088,71 @@ function bundledDetections() {
26600
28088
  }
26601
28089
 
26602
28090
  // ../../packages/plugin-sdk/src/repo.ts
26603
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
26604
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
28091
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
28092
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
26605
28093
 
26606
28094
  // ../../packages/plugin-sdk/src/events.ts
26607
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
28095
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28096
+
28097
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
28098
+ import { existsSync as existsSync7 } from "fs";
28099
+ import { fileURLToPath } from "url";
28100
+ import { Worker } from "worker_threads";
26608
28101
 
26609
28102
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
26610
- import { arch, hostname as hostname3, platform, release } from "os";
28103
+ import { arch, hostname as hostname4, platform, release } from "os";
26611
28104
 
26612
28105
  // ../../packages/plugin-sdk/src/nudge.ts
26613
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
26614
- import { join as join9 } from "path";
28106
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
28107
+ import { join as join11 } from "path";
26615
28108
 
26616
28109
  // ../../packages/plugin-sdk/src/paths.ts
26617
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26618
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
28110
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
28111
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
26619
28112
 
26620
28113
  // ../../packages/plugin-sdk/src/project-files.ts
26621
28114
  var import_ignore = __toESM(require_ignore(), 1);
26622
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26623
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
28115
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
28116
+ import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
28117
+
28118
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
28119
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
28120
+ if (typeof v === "string" && v.trim() === "") return void 0;
28121
+ return v;
28122
+ }, external_exports.string().optional()).catch(void 0);
28123
+ var optionalFlag = external_exports.preprocess((v) => {
28124
+ if (typeof v !== "string") return false;
28125
+ const normalized = v.trim().toLowerCase();
28126
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
28127
+ }, external_exports.boolean()).catch(false);
28128
+ var antigravityProviderEnvShape = {
28129
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
28130
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
28131
+ };
28132
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
28133
+
28134
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
28135
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
28136
+ if (typeof v === "string" && v.trim() === "") return void 0;
28137
+ return v;
28138
+ }, external_exports.string().optional()).catch(void 0);
28139
+ var codexProviderEnvShape = {
28140
+ OPENAI_BASE_URL: optionalBaseUrl3
28141
+ };
28142
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
26624
28143
 
26625
28144
  // ../../packages/plugin-sdk/src/runtime.ts
26626
- import { randomUUID as randomUUID10 } from "crypto";
28145
+ import { randomUUID as randomUUID14 } from "crypto";
26627
28146
 
26628
28147
  // ../../packages/plugin-sdk/src/suppressions.ts
26629
28148
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26630
28149
 
26631
28150
  // ../../packages/plugin-sdk/src/throttle.ts
26632
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
26633
- import { join as join11 } from "path";
28151
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
28152
+ import { join as join13 } from "path";
26634
28153
 
26635
28154
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
26636
- import { randomUUID as randomUUID11 } from "crypto";
28155
+ import { randomUUID as randomUUID15 } from "crypto";
26637
28156
 
26638
28157
  // ../../packages/plugin-runtime/src/recorder.ts
26639
28158
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -26795,7 +28314,7 @@ var StandaloneDataGateway = class {
26795
28314
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
26796
28315
  const installed = this.installedScanRules();
26797
28316
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
26798
- id: randomUUID11(),
28317
+ id: randomUUID15(),
26799
28318
  scope: "global",
26800
28319
  target: { ruleId },
26801
28320
  action,
@@ -26948,16 +28467,16 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
26948
28467
  }
26949
28468
 
26950
28469
  // ../../packages/plugin-runtime/src/handle-session-start.ts
26951
- import { randomUUID as randomUUID12 } from "crypto";
28470
+ import { randomUUID as randomUUID16 } from "crypto";
26952
28471
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
26953
28472
 
26954
28473
  // src/command-registry.ts
26955
- import { readdirSync as readdirSync4 } from "fs";
26956
- import { fileURLToPath } from "url";
28474
+ import { readdirSync as readdirSync5 } from "fs";
28475
+ import { fileURLToPath as fileURLToPath2 } from "url";
26957
28476
  var COMMAND_NAMESPACE = "aka";
26958
- var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
28477
+ var COMMANDS_DIR = fileURLToPath2(new URL("../commands", import.meta.url));
26959
28478
  function readRegisteredCommands() {
26960
- return readdirSync4(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
28479
+ return readdirSync5(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
26961
28480
  }
26962
28481
  function selectRegisteredCommands(curated, registry2) {
26963
28482
  const registered = new Set(registry2);
@@ -27063,6 +28582,54 @@ function show(body) {
27063
28582
  return showBlock(body);
27064
28583
  }
27065
28584
 
28585
+ // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
28586
+ import { writeFileSync as writeFileSync7 } from "fs";
28587
+ import { join as join14 } from "path";
28588
+
28589
+ // ../../packages/setup-wizard/src/triage/plan-file.ts
28590
+ import { mkdtempSync, readFileSync as readFileSync9, rmdirSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
28591
+ import { tmpdir } from "os";
28592
+ import { basename as basename6, dirname as dirname4, join as join15 } from "path";
28593
+ var SuppressionEntrySchema = external_exports.object({
28594
+ ruleId: external_exports.string(),
28595
+ category: DetectionCategory,
28596
+ valueFingerprint: external_exports.string(),
28597
+ keyVersion: external_exports.number(),
28598
+ maskedValue: external_exports.string(),
28599
+ justification: external_exports.string()
28600
+ });
28601
+ var ShowcaseCategorySchema = external_exports.object({
28602
+ category: DetectionCategory,
28603
+ action: BuiltinPolicyId,
28604
+ genuineCount: external_exports.number(),
28605
+ fpCount: external_exports.number(),
28606
+ reasoning: external_exports.string()
28607
+ });
28608
+ var JoinEntrySchema = external_exports.object({
28609
+ id: external_exports.string(),
28610
+ ruleId: external_exports.string(),
28611
+ category: DetectionCategory,
28612
+ valueFingerprint: external_exports.string().optional(),
28613
+ keyVersion: external_exports.number().optional(),
28614
+ maskedMatch: external_exports.string(),
28615
+ maskedContext: external_exports.string()
28616
+ });
28617
+ var PLAN_FILE_VERSION = 3;
28618
+ var PersistedPlanSchema = external_exports.object({
28619
+ version: external_exports.literal(PLAN_FILE_VERSION),
28620
+ // partialRecord (not record): a posture only covers the categories present in
28621
+ // the evidence, so an exhaustive-key record would reject every real plan.
28622
+ posture: external_exports.partialRecord(DetectionCategory, BuiltinPolicyId),
28623
+ entries: external_exports.array(SuppressionEntrySchema),
28624
+ showcase: external_exports.array(ShowcaseCategorySchema),
28625
+ join: external_exports.array(JoinEntrySchema),
28626
+ notes: external_exports.string(),
28627
+ // The store's per-category action at preview time. The downgrade view is
28628
+ // rendered from it at PREVIEW (renderPosturePlan); confirm reads it back ONLY to
28629
+ // compare against the live store and reject a stale plan (runConfirm's drift gate).
28630
+ current: external_exports.partialRecord(DetectionCategory, ActionTaken)
28631
+ });
28632
+
27066
28633
  // src/render.ts
27067
28634
  var STORE_UNAVAILABLE_NOTE = "I couldn't check my records just now \u2014 we can check again soon. Your Claude session keeps going, and I'll fill in as you work.";
27068
28635
  var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
@@ -27272,10 +28839,10 @@ async function runFirstRunFailOpen(deps) {
27272
28839
  }
27273
28840
 
27274
28841
  // src/posture.ts
27275
- async function readPostureBlock(open) {
28842
+ async function readPostureBlock(open2) {
27276
28843
  let db;
27277
28844
  try {
27278
- db = open();
28845
+ db = open2();
27279
28846
  const policies = await db.policies.readPolicies();
27280
28847
  return renderPosture(
27281
28848
  policies.map((p) => ({