@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,13 +492,12 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync4 } from "fs";
496
- import { join as join6 } from "path";
495
+ import { existsSync as existsSync5 } from "fs";
496
+ import { join as join8 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/database.ts
499
- import { randomUUID as randomUUID8 } from "crypto";
500
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
501
- import { join, sep } from "path";
499
+ import { randomUUID as randomUUID10 } from "crypto";
500
+ import { join as join2, sep } from "path";
502
501
  import { DatabaseSync } from "node:sqlite";
503
502
 
504
503
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -562,6 +561,30 @@ var SQLITE_MIGRATIONS = [
562
561
  {
563
562
  tag: "0014_drop_legacy_events_findings",
564
563
  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"
564
+ },
565
+ {
566
+ tag: "0015_busy_vengeance",
567
+ 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`);"
568
+ },
569
+ {
570
+ tag: "0016_breezy_zodiak",
571
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
572
+ },
573
+ {
574
+ tag: "0017_rainy_kat_farrell",
575
+ 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`);"
576
+ },
577
+ {
578
+ tag: "0018_serious_tana_nile",
579
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
580
+ },
581
+ {
582
+ tag: "0019_audit_started_at_index",
583
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
584
+ },
585
+ {
586
+ tag: "0020_secret_vault_pagination_indexes",
587
+ sql: "CREATE INDEX `idx_secret_vault_last_seen` ON `secret_vault` (`last_seen`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_reuse` ON `secret_vault` (`occurrence_count`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_at` ON `secret_vault_deref` (`at`,`id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_signal` ON `secret_vault_deref` (`at`,`id`) WHERE `reason` NOT IN ('display', 'view-render');"
565
588
  }
566
589
  ];
567
590
 
@@ -15299,7 +15322,17 @@ var Finding = external_exports.object({
15299
15322
  }).meta({ id: "Finding" });
15300
15323
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15301
15324
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15302
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15325
+ var FindingProvider = external_exports.enum([
15326
+ "claudecode",
15327
+ "claudedesktop",
15328
+ "cursor",
15329
+ "copilot",
15330
+ "chatgpt",
15331
+ "claudeai",
15332
+ "codex",
15333
+ "antigravity",
15334
+ "api"
15335
+ ]).meta({ id: "FindingProvider" });
15303
15336
  var FindingCategory = external_exports.enum([
15304
15337
  "secret",
15305
15338
  "pii",
@@ -15353,7 +15386,16 @@ var FindingInstance = external_exports.object({
15353
15386
  confidence: external_exports.number().min(0).max(1),
15354
15387
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15355
15388
  // that predate the resolution feature stay valid.
15356
- status: FindingStatus.optional()
15389
+ status: FindingStatus.optional(),
15390
+ // The audit event this finding was captured from. Optional so callers that
15391
+ // do not project it stay valid. An at-rest finding is content-addressed by
15392
+ // finding_key and its row is upserted on re-detection, so this names the
15393
+ // MOST RECENT detection event, not the first.
15394
+ eventId: external_exports.string().optional(),
15395
+ // The session that event belongs to, when it has one — the seam a
15396
+ // per-instance "view session" link needs. Absent for events captured
15397
+ // outside a session.
15398
+ sessionId: external_exports.string().optional()
15357
15399
  }).meta({ id: "FindingInstance" });
15358
15400
  var FindingGroup = external_exports.object({
15359
15401
  id: external_exports.string(),
@@ -15397,7 +15439,11 @@ var FindingFacets = external_exports.object({
15397
15439
  // for every instance, so every group lands in a bucket; a status-less
15398
15440
  // group (possible only for callers whose rows carry no statuses) is
15399
15441
  // counted under no value.
15400
- status: external_exports.array(FindingFacetItem)
15442
+ status: external_exports.array(FindingFacetItem),
15443
+ // Host tool (attributes.tool_name). Present only on the instance-level
15444
+ // reads, which can filter by it; the grouped read omits the dimension
15445
+ // because a group spans tools.
15446
+ tool: external_exports.array(FindingFacetItem).optional()
15401
15447
  }).meta({ id: "FindingFacets" });
15402
15448
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15403
15449
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15415,6 +15461,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15415
15461
  // Scope to findings whose event carries this session id (the Activity page's
15416
15462
  // session → findings drilldown). Findings without a session never match.
15417
15463
  sessionId: external_exports.string().optional(),
15464
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15465
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15466
+ // means all time — this list has no default window.
15467
+ from: external_exports.iso.datetime().optional(),
15468
+ // A group or instance id that must appear in the page even when the cursor
15469
+ // has already advanced past its sort position. This is what keeps the
15470
+ // Findings page's one-shot ?finding= deep link resolving once the list
15471
+ // paginates: the target group is appended out of sort order rather than
15472
+ // scanning forward for it. Never affects totals, facets or the cursor.
15473
+ includeId: external_exports.string().optional(),
15418
15474
  groupBy: external_exports.literal("type").optional(),
15419
15475
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15420
15476
  cursor: external_exports.string().optional()
@@ -15459,15 +15515,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15459
15515
  detection: FindingDetectionRef,
15460
15516
  policy: FindingPolicyRef
15461
15517
  }).meta({ id: "FindingInstanceDetail" });
15518
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15519
+ var ListFindingInstancesQuery = external_exports.object({
15520
+ severity: external_exports.array(Severity).optional(),
15521
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15522
+ subtype: external_exports.array(external_exports.string()).optional(),
15523
+ provider: external_exports.array(FindingProvider).optional(),
15524
+ action: external_exports.array(FindingAction).optional(),
15525
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15526
+ // the grouped query's group-level fold.
15527
+ status: external_exports.array(FindingStatus).optional(),
15528
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15529
+ // where the free-text `q` can only match the rendered "via Bash" label.
15530
+ tool: external_exports.array(external_exports.string()).optional(),
15531
+ // Exact repository / file-path matches, for the drill-down out of the
15532
+ // locations view. A row whose event carries no repo/file matches neither.
15533
+ repo: external_exports.string().optional(),
15534
+ file: external_exports.string().optional(),
15535
+ q: external_exports.string().optional(),
15536
+ sessionId: external_exports.string().optional(),
15537
+ from: external_exports.iso.datetime().optional(),
15538
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15539
+ cursor: external_exports.string().optional()
15540
+ });
15541
+ var ListFindingInstancesResponse = external_exports.object({
15542
+ // Instances matching the filters across the whole scope, not just this
15543
+ // page — cursor-independent, like the grouped list's totals.
15544
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15545
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15546
+ // dimension still excludes its own filter.
15547
+ facets: FindingFacets,
15548
+ items: external_exports.array(FindingInstanceDetail),
15549
+ nextCursor: external_exports.string().nullable()
15550
+ }).meta({ id: "ListFindingInstancesResponse" });
15551
+ var FindingLocationFile = external_exports.object({
15552
+ // Empty when the instances carried no file path (a prompt or a tool call
15553
+ // with no file attribution).
15554
+ file: external_exports.string(),
15555
+ instanceCount: external_exports.number().int().nonnegative(),
15556
+ maxSeverity: Severity,
15557
+ latestDetectedAt: external_exports.iso.datetime(),
15558
+ // Folded from the instances' derived statuses with the same
15559
+ // open-dominates precedence a group uses.
15560
+ status: FindingStatus.optional(),
15561
+ // Distinct rules seen at this location, capped — the row shows them as
15562
+ // chips, and the count is what conveys scale.
15563
+ ruleIds: external_exports.array(external_exports.string())
15564
+ }).meta({ id: "FindingLocationFile" });
15565
+ var FindingLocationRepo = external_exports.object({
15566
+ /** Empty when the instances carried no repo attribute. */
15567
+ repo: external_exports.string(),
15568
+ instanceCount: external_exports.number().int().nonnegative(),
15569
+ maxSeverity: Severity,
15570
+ latestDetectedAt: external_exports.iso.datetime(),
15571
+ status: FindingStatus.optional(),
15572
+ files: external_exports.array(FindingLocationFile)
15573
+ }).meta({ id: "FindingLocationRepo" });
15574
+ var ListFindingLocationsQuery = external_exports.object({
15575
+ severity: external_exports.array(Severity).optional(),
15576
+ subtype: external_exports.array(external_exports.string()).optional(),
15577
+ provider: external_exports.array(FindingProvider).optional(),
15578
+ action: external_exports.array(FindingAction).optional(),
15579
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15580
+ // instances that match, and folds its status from those.
15581
+ status: external_exports.array(FindingStatus).optional(),
15582
+ tool: external_exports.array(external_exports.string()).optional(),
15583
+ q: external_exports.string().optional(),
15584
+ sessionId: external_exports.string().optional(),
15585
+ from: external_exports.iso.datetime().optional(),
15586
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15587
+ });
15588
+ var ListFindingLocationsResponse = external_exports.object({
15589
+ totals: external_exports.object({
15590
+ findings: external_exports.number().int().nonnegative(),
15591
+ repos: external_exports.number().int().nonnegative(),
15592
+ files: external_exports.number().int().nonnegative()
15593
+ }),
15594
+ /** Sorted by max severity, then most recent. */
15595
+ items: external_exports.array(FindingLocationRepo),
15596
+ /** Whether `limit` truncated the repo list. */
15597
+ hasMore: external_exports.boolean()
15598
+ }).meta({ id: "ListFindingLocationsResponse" });
15462
15599
 
15463
15600
  // ../../packages/schema/src/zod/harness-map.ts
15464
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15601
+ var Harness = external_exports.enum([
15602
+ "claudecode",
15603
+ "cursor",
15604
+ "copilot",
15605
+ "codex",
15606
+ "antigravity",
15607
+ "windsurf",
15608
+ "claudedesktop",
15609
+ "chatgpt",
15610
+ "claudeai",
15611
+ "api"
15612
+ ]).meta({ id: "Harness" });
15465
15613
  var TOOL_TO_HARNESS = {
15466
15614
  "claude-code": "claudecode",
15467
15615
  "claude-desktop": "claudedesktop",
15468
15616
  "github-copilot": "copilot",
15469
15617
  cursor: "cursor",
15470
- chatgpt: "chatgpt"
15618
+ chatgpt: "chatgpt",
15619
+ codex: "codex",
15620
+ antigravity: "antigravity",
15621
+ "claude-ai": "claudeai"
15471
15622
  };
15472
15623
 
15473
15624
  // ../../packages/schema/src/zod/meta.ts
@@ -15925,7 +16076,18 @@ var ActivityOverviewResponse = external_exports.object({
15925
16076
  // ../../packages/schema/src/zod/event.ts
15926
16077
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15927
16078
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15928
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16079
+ var SourceTool = external_exports.enum([
16080
+ "claude-code",
16081
+ "claude-desktop",
16082
+ "cursor",
16083
+ "chatgpt",
16084
+ "claude-ai",
16085
+ "github-copilot",
16086
+ "codex",
16087
+ "antigravity",
16088
+ "cli",
16089
+ "unknown"
16090
+ ]).meta({ id: "SourceTool" });
15929
16091
  var EventMetadata = external_exports.object({
15930
16092
  sessionId: external_exports.string().optional(),
15931
16093
  repo: external_exports.string().optional(),
@@ -15996,7 +16158,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
15996
16158
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
15997
16159
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
15998
16160
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
15999
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16161
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
16000
16162
  var AccessCounts = external_exports.object({
16001
16163
  open: external_exports.number().int().nonnegative(),
16002
16164
  approved: external_exports.number().int().nonnegative(),
@@ -16218,6 +16380,7 @@ var ExceptionConditions = external_exports.object({
16218
16380
  sourceTool: external_exports.string().optional(),
16219
16381
  provider: external_exports.string().optional()
16220
16382
  }).strict();
16383
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16221
16384
  var DetectionException = external_exports.object({
16222
16385
  id: external_exports.guid(),
16223
16386
  ruleId: external_exports.string(),
@@ -16234,6 +16397,7 @@ var DetectionException = external_exports.object({
16234
16397
  keyVersion: external_exports.number().int().positive(),
16235
16398
  // maskMatch() preview of the approved value — never the raw value.
16236
16399
  maskedValue: external_exports.string(),
16400
+ capability: ExceptionCapability.default("suppress"),
16237
16401
  scope: ExceptionScope,
16238
16402
  expiresAt: external_exports.iso.datetime().nullable(),
16239
16403
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16257,11 +16421,13 @@ var ExceptionBundleEntry = DetectionException.pick({
16257
16421
  ruleId: true,
16258
16422
  valueFingerprint: true,
16259
16423
  keyVersion: true,
16424
+ capability: true,
16260
16425
  expiresAt: true,
16261
16426
  maxUses: true,
16262
16427
  useCount: true,
16263
16428
  conditions: true
16264
16429
  });
16430
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16265
16431
 
16266
16432
  // ../../packages/schema/src/zod/rule.ts
16267
16433
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17072,6 +17238,35 @@ var EgressWriteSummary = external_exports.object({
17072
17238
  droppedFiles: external_exports.array(external_exports.string()).default([])
17073
17239
  }).meta({ id: "EgressWriteSummary" });
17074
17240
 
17241
+ // ../../packages/schema/src/zod/exception-action.ts
17242
+ var confirmation = external_exports.string().optional();
17243
+ var ApproveBlockedInput = external_exports.object({
17244
+ reference: external_exports.string(),
17245
+ scope: external_exports.string(),
17246
+ reason: external_exports.string(),
17247
+ confirmation
17248
+ });
17249
+ var AddExceptionInput = external_exports.object({
17250
+ ruleId: external_exports.string(),
17251
+ value: external_exports.string(),
17252
+ scope: external_exports.string(),
17253
+ reason: external_exports.string(),
17254
+ confirmation
17255
+ });
17256
+ var GrantRevealInput = external_exports.object({
17257
+ pointer: external_exports.string(),
17258
+ scope: external_exports.string(),
17259
+ justification: external_exports.string(),
17260
+ confirmation
17261
+ });
17262
+ var RevokeExceptionInput = external_exports.object({
17263
+ id: external_exports.string(),
17264
+ reason: external_exports.string()
17265
+ });
17266
+ var RotateKeyInput = external_exports.object({
17267
+ confirmation: external_exports.string()
17268
+ });
17269
+
17075
17270
  // ../../packages/schema/src/zod/findings-group-build.ts
17076
17271
  function toApiAction(dbVal) {
17077
17272
  const map2 = {
@@ -17127,6 +17322,8 @@ function buildFindingGroups(rows, opts = {}) {
17127
17322
  repo: r.repo,
17128
17323
  file: r.file,
17129
17324
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17325
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17326
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17130
17327
  action: toApiAction(effectiveDbAction),
17131
17328
  detectedAt: r.occurredAt,
17132
17329
  confidence: r.confidence,
@@ -17258,14 +17455,17 @@ function applyFindingFilters(groups, opts) {
17258
17455
  }
17259
17456
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17260
17457
  var SEVERITY_RANK = SEVERITY_ORDER;
17458
+ function compareFindingGroupOrder(a, b) {
17459
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17460
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17461
+ const severityDiff = rankA - rankB;
17462
+ if (severityDiff !== 0) return severityDiff;
17463
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17464
+ if (recencyDiff !== 0) return recencyDiff;
17465
+ return a.id.localeCompare(b.id);
17466
+ }
17261
17467
  function sortFindingGroups(groups) {
17262
- return [...groups].sort((a, b) => {
17263
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17264
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17265
- const severityDiff = rankA - rankB;
17266
- if (severityDiff !== 0) return severityDiff;
17267
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17268
- });
17468
+ return [...groups].sort(compareFindingGroupOrder);
17269
17469
  }
17270
17470
  function computeFindingFacets(allGroups, opts) {
17271
17471
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17321,15 +17521,158 @@ function computeFindingFacets(allGroups, opts) {
17321
17521
  for (const g of forStatus) {
17322
17522
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17323
17523
  }
17324
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17524
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17525
+ return {
17526
+ severity: toItems2(severityMap),
17527
+ provider: toItems2(providerMap),
17528
+ action: toItems2(actionMap),
17529
+ subtype: toItems2(subtypeMap),
17530
+ status: toItems2(statusMap)
17531
+ };
17532
+ }
17533
+
17534
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17535
+ function rowHaystack(row) {
17536
+ return [
17537
+ row.ruleId,
17538
+ row.category,
17539
+ row.maskedMatch,
17540
+ row.repo,
17541
+ row.file,
17542
+ row.toolName ? `via ${row.toolName}` : "",
17543
+ row.id
17544
+ ].join(" ").toLowerCase();
17545
+ }
17546
+ function matchesDimension(row, opts, dimension) {
17547
+ switch (dimension) {
17548
+ case "severity":
17549
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17550
+ case "subtype":
17551
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17552
+ case "providers":
17553
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17554
+ case "actions":
17555
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17556
+ case "statuses":
17557
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17558
+ case "tools":
17559
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17560
+ case "repo":
17561
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17562
+ case "file":
17563
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17564
+ case "q":
17565
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17566
+ }
17567
+ }
17568
+ var DIMENSIONS = [
17569
+ "severity",
17570
+ "subtype",
17571
+ "providers",
17572
+ "actions",
17573
+ "statuses",
17574
+ "tools",
17575
+ "repo",
17576
+ "file",
17577
+ "q"
17578
+ ];
17579
+ function matchesInstanceFilters(row, opts, except) {
17580
+ for (const dimension of DIMENSIONS) {
17581
+ if (dimension === except) continue;
17582
+ if (!matchesDimension(row, opts, dimension)) return false;
17583
+ }
17584
+ return true;
17585
+ }
17586
+ function toItems(counts) {
17587
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17588
+ }
17589
+ function bump(counts, value) {
17590
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17591
+ }
17592
+ function createInstanceFacetAccumulator(opts) {
17593
+ const severity = /* @__PURE__ */ new Map();
17594
+ const subtype = /* @__PURE__ */ new Map();
17595
+ const provider = /* @__PURE__ */ new Map();
17596
+ const action = /* @__PURE__ */ new Map();
17597
+ const status = /* @__PURE__ */ new Map();
17598
+ const tool = /* @__PURE__ */ new Map();
17599
+ return {
17600
+ add(row) {
17601
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17602
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17603
+ if (matchesInstanceFilters(row, opts, "providers")) {
17604
+ bump(provider, toApiProvider(row.sourceTool));
17605
+ }
17606
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17607
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17608
+ bump(status, row.status);
17609
+ }
17610
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17611
+ bump(tool, row.toolName);
17612
+ }
17613
+ },
17614
+ facets: () => ({
17615
+ severity: toItems(severity),
17616
+ subtype: toItems(subtype),
17617
+ provider: toItems(provider),
17618
+ action: toItems(action),
17619
+ status: toItems(status),
17620
+ tool: toItems(tool)
17621
+ })
17622
+ };
17623
+ }
17624
+ function toInstanceDetail(row) {
17625
+ const category = toApiCategory(row.category);
17325
17626
  return {
17326
- severity: toItems(severityMap),
17327
- provider: toItems(providerMap),
17328
- action: toItems(actionMap),
17329
- subtype: toItems(subtypeMap),
17330
- status: toItems(statusMap)
17627
+ id: row.id,
17628
+ provider: toApiProvider(row.sourceTool),
17629
+ repo: row.repo,
17630
+ file: row.file,
17631
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17632
+ eventId: row.eventId,
17633
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17634
+ action: toApiAction(row.actionTaken),
17635
+ detectedAt: row.occurredAt,
17636
+ confidence: row.confidence,
17637
+ ...row.status === void 0 ? {} : { status: row.status },
17638
+ groupId: row.ruleId,
17639
+ category,
17640
+ subtype: row.ruleId,
17641
+ severity: row.severity,
17642
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17643
+ detection: { id: row.ruleId, name: null },
17644
+ policy: { id: `category:${category}`, name: category }
17645
+ };
17646
+ }
17647
+ var SEVERITY_ORDER2 = {
17648
+ critical: 0,
17649
+ high: 1,
17650
+ medium: 2,
17651
+ low: 3
17652
+ };
17653
+ function newLocationAccumulator() {
17654
+ return {
17655
+ instanceCount: 0,
17656
+ // Sorts after every known severity, so the first row always wins the
17657
+ // comparison below rather than an unknown value pinning the location.
17658
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17659
+ maxSeverity: "low",
17660
+ latestDetectedAt: "",
17661
+ statuses: [],
17662
+ ruleIds: /* @__PURE__ */ new Set()
17331
17663
  };
17332
17664
  }
17665
+ function addToLocation(acc, row) {
17666
+ acc.instanceCount += 1;
17667
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17668
+ if (rank < acc.maxSeverityRank) {
17669
+ acc.maxSeverityRank = rank;
17670
+ acc.maxSeverity = row.severity;
17671
+ }
17672
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17673
+ acc.statuses.push(row.status);
17674
+ acc.ruleIds.add(row.ruleId);
17675
+ }
17333
17676
 
17334
17677
  // ../../packages/schema/src/zod/installed-pack.ts
17335
17678
  var InstalledPack = external_exports.object({
@@ -17361,8 +17704,161 @@ var PatchInstalledPackRequest = external_exports.object({
17361
17704
  message: "At least one field must be provided"
17362
17705
  }).meta({ id: "PatchInstalledPackRequest" });
17363
17706
 
17707
+ // ../../packages/schema/src/zod/vault.ts
17708
+ var POINTER_FORMAT_VERSION = 2;
17709
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17710
+ var POINTER_TOKEN_PATTERN = new RegExp(
17711
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17712
+ );
17713
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17714
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17715
+ var ParsedPointer = external_exports.object({
17716
+ category: DetectionCategory,
17717
+ keyVersion: external_exports.number().int().positive(),
17718
+ pointerId: external_exports.string(),
17719
+ tag: external_exports.string()
17720
+ });
17721
+ var VaultEntry = external_exports.object({
17722
+ pointerId: external_exports.string(),
17723
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17724
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17725
+ // independently of the vault encryption key below.
17726
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17727
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17728
+ // The vault-key epoch this row's ciphertext was sealed under.
17729
+ keyVersion: external_exports.number().int().positive(),
17730
+ // Fixed at first mint and never updated: the same value detected later under a
17731
+ // different rule's category keeps the category it was minted with, so one
17732
+ // value always produces exactly one wire token.
17733
+ category: DetectionCategory,
17734
+ ruleId: external_exports.string(),
17735
+ // Partial-reveal preview for badges and listings. Never the raw value.
17736
+ maskedMatch: external_exports.string(),
17737
+ provider: external_exports.string().optional(),
17738
+ ciphertext: external_exports.string(),
17739
+ nonce: external_exports.string(),
17740
+ authTag: external_exports.string(),
17741
+ // How many times this value has been detected on this machine — the reuse
17742
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17743
+ occurrenceCount: external_exports.number().int().nonnegative(),
17744
+ firstSeen: external_exports.string(),
17745
+ lastSeen: external_exports.string()
17746
+ });
17747
+ var PointerDescriptor = external_exports.object({
17748
+ category: DetectionCategory,
17749
+ provider: external_exports.string().optional(),
17750
+ maskedMatch: external_exports.string(),
17751
+ occurrences: external_exports.number().int().nonnegative(),
17752
+ firstSeen: external_exports.string(),
17753
+ lastSeen: external_exports.string()
17754
+ });
17755
+ var PointerIdentity = external_exports.object({
17756
+ ruleId: external_exports.string(),
17757
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17758
+ fingerprintKeyVersion: external_exports.number().int().positive()
17759
+ });
17760
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17761
+ var VaultDerefReason = external_exports.enum([
17762
+ "display",
17763
+ "explicit-reveal",
17764
+ "view-render",
17765
+ "model-input",
17766
+ "remediation",
17767
+ "purge"
17768
+ ]);
17769
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17770
+ var VaultDeref = external_exports.object({
17771
+ id: external_exports.guid(),
17772
+ pointerId: external_exports.string(),
17773
+ at: external_exports.string(),
17774
+ target: DetokenizeTarget,
17775
+ reason: VaultDerefReason,
17776
+ outcome: VaultDerefOutcome,
17777
+ // Present only on a model-target crossing that a reveal grant authorized.
17778
+ grantId: external_exports.string().optional(),
17779
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17780
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17781
+ pointerCount: external_exports.number().int().positive().default(1)
17782
+ });
17783
+ var VaultSightingKind = external_exports.enum([
17784
+ "prompt",
17785
+ "tool-input",
17786
+ "tool-output",
17787
+ "file",
17788
+ "transcript"
17789
+ ]);
17790
+ var VaultSighting = external_exports.object({
17791
+ location: external_exports.string(),
17792
+ kind: VaultSightingKind,
17793
+ firstSeen: external_exports.string(),
17794
+ lastSeen: external_exports.string()
17795
+ });
17796
+ var VaultInventoryEntry = external_exports.object({
17797
+ pointerId: external_exports.string(),
17798
+ category: DetectionCategory,
17799
+ provider: external_exports.string().optional(),
17800
+ maskedMatch: external_exports.string(),
17801
+ occurrences: external_exports.number().int().nonnegative(),
17802
+ firstSeen: external_exports.string(),
17803
+ lastSeen: external_exports.string(),
17804
+ // The active reveal-to-model grant covering this value, when one exists —
17805
+ // the inventory badges it, the row links to revocation.
17806
+ revealGrantId: external_exports.string().nullable(),
17807
+ sightings: external_exports.array(VaultSighting)
17808
+ });
17809
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17810
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17811
+ var MAX_VAULT_PAGE_LIMIT = 200;
17812
+ var ListVaultInventoryQuery = external_exports.object({
17813
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17814
+ // Opaque; names the last row of the page just served.
17815
+ cursor: external_exports.string().optional()
17816
+ });
17817
+ var ListVaultInventoryResponse = external_exports.object({
17818
+ // Vaulted values across the whole store, not just this page — cursor-
17819
+ // independent, so paging never changes what the count claims.
17820
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17821
+ items: external_exports.array(VaultInventoryEntry),
17822
+ // `null` once the last page is reached.
17823
+ nextCursor: external_exports.string().nullable()
17824
+ });
17825
+ var ListVaultReuseQuery = external_exports.object({
17826
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17827
+ cursor: external_exports.string().optional()
17828
+ });
17829
+ var ListVaultReuseResponse = external_exports.object({
17830
+ // Reused values across the whole store — the number the section's claim
17831
+ // ("values detected in more than one place") is about.
17832
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17833
+ items: external_exports.array(VaultInventoryEntry),
17834
+ nextCursor: external_exports.string().nullable()
17835
+ });
17836
+ var ListVaultDerefsQuery = external_exports.object({
17837
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17838
+ // hides them and counts them into `hiddenBatched` instead, so the model
17839
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17840
+ // over a Server Action, which preserves the type, never as a URL param.
17841
+ includeBatched: external_exports.boolean().optional(),
17842
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17843
+ cursor: external_exports.string().optional()
17844
+ });
17845
+ var ListVaultDerefsResponse = external_exports.object({
17846
+ items: external_exports.array(VaultDeref),
17847
+ nextCursor: external_exports.string().nullable(),
17848
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17849
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17850
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17851
+ hiddenBatched: external_exports.number().int().nonnegative()
17852
+ });
17853
+ var VaultKeyCustody = external_exports.string();
17854
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17855
+ var VaultConsent = external_exports.object({
17856
+ acknowledgedAt: external_exports.iso.datetime(),
17857
+ version: external_exports.number().int().positive()
17858
+ });
17859
+
17364
17860
  // ../../packages/schema/src/zod/local.ts
17365
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17861
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17366
17862
  var RunMode = external_exports.enum(["standalone"]);
17367
17863
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17368
17864
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17384,6 +17880,16 @@ var WorkspaceSettings = external_exports.object({
17384
17880
  // In-place egress extraction on the scan paths; disable to stop all Data
17385
17881
  // Shares writes.
17386
17882
  dataSharesInPlace: external_exports.boolean().default(true),
17883
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17884
+ // vault, instead of destroying them. Absent by default: this is a custody
17885
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17886
+ // Revoking stops future vaulting; it does not erase what is already stored —
17887
+ // purging the vault is the eraser.
17888
+ vaultConsent: VaultConsent.optional(),
17889
+ // Where the vault master key lives.
17890
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17891
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17892
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17387
17893
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17388
17894
  onboardedAt: external_exports.iso.datetime().optional(),
17389
17895
  // Records that the user consented to sending findings to the model API for
@@ -17716,7 +18222,7 @@ var TopSourcesQuery = external_exports.object({
17716
18222
  // Omit for both kinds.
17717
18223
  kind: external_exports.enum(SOURCE_KINDS).optional()
17718
18224
  });
17719
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18225
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17720
18226
  var ScanCoverageProvider = external_exports.object({
17721
18227
  provider: Provider,
17722
18228
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -17969,6 +18475,138 @@ function captureId(sessionId, contentHash, filePath = null) {
17969
18475
  );
17970
18476
  }
17971
18477
 
18478
+ // ../../packages/persistence/src/internal/snapshot.ts
18479
+ import { randomUUID } from "crypto";
18480
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18481
+ import { basename, dirname, join } from "path";
18482
+
18483
+ // ../../packages/persistence/src/paths.ts
18484
+ import {
18485
+ chmodSync,
18486
+ linkSync,
18487
+ lstatSync,
18488
+ mkdirSync,
18489
+ renameSync,
18490
+ rmSync,
18491
+ writeFileSync
18492
+ } from "fs";
18493
+ import { threadId } from "worker_threads";
18494
+ var DATA_DIR_MODE = 448;
18495
+ var DATA_FILE_MODE = 384;
18496
+ var DB_FILENAME = "aka.db";
18497
+ function isSymlink(path) {
18498
+ try {
18499
+ return lstatSync(path).isSymbolicLink();
18500
+ } catch {
18501
+ return false;
18502
+ }
18503
+ }
18504
+ function chmodBestEffort(path, mode) {
18505
+ if (isSymlink(path)) return;
18506
+ try {
18507
+ chmodSync(path, mode);
18508
+ } catch {
18509
+ }
18510
+ }
18511
+ function tightenDir(dir) {
18512
+ chmodBestEffort(dir, DATA_DIR_MODE);
18513
+ }
18514
+ function ensureDataDirSync(dir) {
18515
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18516
+ tightenDir(dir);
18517
+ }
18518
+ function dbSidecars(file2) {
18519
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18520
+ }
18521
+ function tightenFile(file2) {
18522
+ chmodBestEffort(file2, DATA_FILE_MODE);
18523
+ }
18524
+ function tightenPerms(file2) {
18525
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18526
+ }
18527
+
18528
+ // ../../packages/persistence/src/internal/snapshot.ts
18529
+ function backupPath(file2, tag) {
18530
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18531
+ }
18532
+ var STALE_PARTIAL_MS = 5 * 6e4;
18533
+ function reapStalePartials(file2) {
18534
+ const dir = dirname(file2);
18535
+ const prefix = `${basename(file2)}.`;
18536
+ let entries;
18537
+ try {
18538
+ entries = readdirSync(dir);
18539
+ } catch {
18540
+ return;
18541
+ }
18542
+ for (const name of entries) {
18543
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18544
+ const partial2 = join(dir, name);
18545
+ try {
18546
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18547
+ rmSync2(partial2, { force: true });
18548
+ }
18549
+ } catch {
18550
+ }
18551
+ }
18552
+ }
18553
+ function snapshotStore(db, backup) {
18554
+ const partial2 = `${backup}.partial`;
18555
+ try {
18556
+ rmSync2(partial2, { force: true });
18557
+ db.prepare("VACUUM INTO ?").run(partial2);
18558
+ tightenFile(partial2);
18559
+ renameSync2(partial2, backup);
18560
+ } catch (error51) {
18561
+ try {
18562
+ rmSync2(partial2, { force: true });
18563
+ } catch {
18564
+ }
18565
+ throw error51;
18566
+ }
18567
+ }
18568
+ function moveStoreAside(file2, backup) {
18569
+ const undo = [];
18570
+ renameSync2(file2, backup);
18571
+ undo.push([backup, file2]);
18572
+ try {
18573
+ for (const sidecar of dbSidecars(file2)) {
18574
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18575
+ try {
18576
+ renameSync2(sidecar, moved);
18577
+ undo.push([moved, sidecar]);
18578
+ } catch {
18579
+ rmSync2(sidecar, { force: true });
18580
+ }
18581
+ }
18582
+ } catch (error51) {
18583
+ for (const [from, to] of undo.reverse()) {
18584
+ try {
18585
+ renameSync2(from, to);
18586
+ } catch {
18587
+ }
18588
+ }
18589
+ throw error51;
18590
+ }
18591
+ tightenPerms(backup);
18592
+ }
18593
+ function discardStore(file2, backup) {
18594
+ try {
18595
+ rmSync2(file2, { force: true });
18596
+ for (const sidecar of dbSidecars(file2)) {
18597
+ rmSync2(sidecar, { force: true });
18598
+ }
18599
+ } catch (error51) {
18600
+ if (existsSync(file2)) {
18601
+ try {
18602
+ rmSync2(backup, { force: true });
18603
+ } catch {
18604
+ }
18605
+ }
18606
+ throw error51;
18607
+ }
18608
+ }
18609
+
17972
18610
  // ../../packages/persistence/src/internal/sql-text.ts
17973
18611
  function escapeLikePattern(s) {
17974
18612
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18110,38 +18748,6 @@ function mapRowsTolerant(rows, map2) {
18110
18748
  return out;
18111
18749
  }
18112
18750
 
18113
- // ../../packages/persistence/src/paths.ts
18114
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18115
- var DATA_DIR_MODE = 448;
18116
- var DATA_FILE_MODE = 384;
18117
- var DB_FILENAME = "aka.db";
18118
- function chmodBestEffort(path, mode) {
18119
- try {
18120
- chmodSync(path, mode);
18121
- } catch {
18122
- }
18123
- }
18124
- function tightenDir(dir) {
18125
- chmodBestEffort(dir, DATA_DIR_MODE);
18126
- }
18127
- function ensureDataDirSync(dir) {
18128
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18129
- tightenDir(dir);
18130
- }
18131
- function dbSidecars(file2) {
18132
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18133
- }
18134
- function tightenFile(file2) {
18135
- try {
18136
- if (lstatSync(file2).isSymbolicLink()) return;
18137
- } catch {
18138
- }
18139
- chmodBestEffort(file2, DATA_FILE_MODE);
18140
- }
18141
- function tightenPerms(file2) {
18142
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18143
- }
18144
-
18145
18751
  // ../../packages/persistence/src/migrations.ts
18146
18752
  function describeObject(object2) {
18147
18753
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18257,9 +18863,9 @@ function applyLegacyDropMigration(db, file2) {
18257
18863
  }
18258
18864
  }
18259
18865
  function backupBeforeLegacyDrop(db, file2) {
18260
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18261
- db.prepare("VACUUM INTO ?").run(backup);
18262
- tightenFile(backup);
18866
+ reapStalePartials(file2);
18867
+ const backup = backupPath(file2, "pre-drop");
18868
+ snapshotStore(db, backup);
18263
18869
  return backup;
18264
18870
  }
18265
18871
  var TOKEN_USAGE_COLUMNS = [
@@ -18603,6 +19209,25 @@ function parseJsonObject(s) {
18603
19209
  return void 0;
18604
19210
  }
18605
19211
 
19212
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19213
+ function encodeKeysetCursor(payload) {
19214
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19215
+ }
19216
+ function decodeKeysetCursor(cursor) {
19217
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19218
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19219
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19220
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19221
+ // a null cursor, which a caller reads as "end of list". That is the one
19222
+ // outcome a cursor that does not decode must never produce, since the
19223
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19224
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19225
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19226
+ return parsed;
19227
+ }
19228
+ return null;
19229
+ }
19230
+
18606
19231
  // ../../packages/persistence/src/repositories/activity.ts
18607
19232
  var DAY_MS = 864e5;
18608
19233
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18648,16 +19273,6 @@ function utcWindow(nowMs) {
18648
19273
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18649
19274
  return { startMs, endMs: startMs + DAY_MS };
18650
19275
  }
18651
- function encodeCursor(payload) {
18652
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18653
- }
18654
- function decodeCursor(cursor) {
18655
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18656
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18657
- return parsed;
18658
- }
18659
- return null;
18660
- }
18661
19276
  var DB_EVENT_TYPE_TO_KIND = {
18662
19277
  session: "session",
18663
19278
  prompt: "prompt",
@@ -18802,7 +19417,7 @@ var SqliteActivityRepository = class {
18802
19417
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18803
19418
  }
18804
19419
  listSessions(query) {
18805
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19420
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18806
19421
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18807
19422
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18808
19423
  const conditions = [SESSION_ROOT];
@@ -18876,7 +19491,7 @@ var SqliteActivityRepository = class {
18876
19491
  )
18877
19492
  );
18878
19493
  const last = page[page.length - 1];
18879
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19494
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18880
19495
  return Promise.resolve({ items, nextCursor, emptyCount });
18881
19496
  }
18882
19497
  getSession(sessionId) {
@@ -19749,7 +20364,7 @@ var SqliteEventsRepository = class {
19749
20364
  };
19750
20365
 
19751
20366
  // ../../packages/persistence/src/repositories/exceptions.ts
19752
- import { randomUUID } from "crypto";
20367
+ import { randomUUID as randomUUID2 } from "crypto";
19753
20368
 
19754
20369
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19755
20370
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19781,9 +20396,13 @@ var AmbiguousExceptionIdError = class extends Error {
19781
20396
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19782
20397
  AND (expires_at IS NULL OR expires_at > :now)
19783
20398
  AND (max_uses IS NULL OR use_count < max_uses)`;
20399
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
20400
+ AND conditions IS NULL
20401
+ AND ${ACTIVE_PREDICATE}`;
19784
20402
  var SqliteExceptionsRepository = class {
19785
- constructor(db) {
20403
+ constructor(db, now = () => Date.now()) {
19786
20404
  this.db = db;
20405
+ this.now = now;
19787
20406
  this.consumeStmt = db.prepare(
19788
20407
  `UPDATE exceptions
19789
20408
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19801,6 +20420,7 @@ var SqliteExceptionsRepository = class {
19801
20420
  );
19802
20421
  }
19803
20422
  db;
20423
+ now;
19804
20424
  consumeStmt;
19805
20425
  insertBlockedStmt;
19806
20426
  sweepBlockedStmt;
@@ -19827,8 +20447,8 @@ var SqliteExceptionsRepository = class {
19827
20447
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19828
20448
  );
19829
20449
  }
19830
- const id = randomUUID();
19831
- const now = Date.now();
20450
+ const id = randomUUID2();
20451
+ const now = this.now();
19832
20452
  try {
19833
20453
  this.insertExceptionRow(id, input, now);
19834
20454
  } catch (err) {
@@ -19872,11 +20492,11 @@ var SqliteExceptionsRepository = class {
19872
20492
  this.db.prepare(
19873
20493
  `INSERT INTO exceptions (
19874
20494
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19875
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19876
- conditions, created_by, created_via, created_at, updated_at
20495
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20496
+ justification, conditions, created_by, created_via, created_at, updated_at
19877
20497
  ) VALUES (
19878
20498
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19879
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20499
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19880
20500
  :conditions, :createdBy, :createdVia, :now, :now
19881
20501
  )`
19882
20502
  ).run({
@@ -19886,6 +20506,7 @@ var SqliteExceptionsRepository = class {
19886
20506
  valueFingerprint: input.valueFingerprint,
19887
20507
  keyVersion: input.keyVersion,
19888
20508
  maskedValue: input.maskedValue,
20509
+ capability: input.capability ?? "suppress",
19889
20510
  scope: input.scope,
19890
20511
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19891
20512
  maxUses: input.maxUses,
@@ -19905,7 +20526,7 @@ var SqliteExceptionsRepository = class {
19905
20526
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19906
20527
  const rows = allRows(
19907
20528
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19908
- opts?.includeTerminal ? {} : { now: Date.now() }
20529
+ opts?.includeTerminal ? {} : { now: this.now() }
19909
20530
  );
19910
20531
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19911
20532
  return Promise.resolve(exceptions);
@@ -19940,7 +20561,7 @@ var SqliteExceptionsRepository = class {
19940
20561
  * already revoked.
19941
20562
  */
19942
20563
  revoke(id, revokedBy, reason) {
19943
- const now = Date.now();
20564
+ const now = this.now();
19944
20565
  const result = this.db.prepare(
19945
20566
  `UPDATE exceptions
19946
20567
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -19954,7 +20575,7 @@ var SqliteExceptionsRepository = class {
19954
20575
  * callers must treat identically — means it does not and the detection is
19955
20576
  * enforced as usual. Deliberately NOT wrapped in try/catch.
19956
20577
  */
19957
- consume(id, now = Date.now()) {
20578
+ consume(id, now = this.now()) {
19958
20579
  const result = this.consumeStmt.run({ id, now });
19959
20580
  return Promise.resolve(Number(result.changes) === 1);
19960
20581
  }
@@ -19963,7 +20584,7 @@ var SqliteExceptionsRepository = class {
19963
20584
  * version — what rides the policy bundle to the hook. Grants written under
19964
20585
  * a different (rotated-away) key never match, so they are excluded at read.
19965
20586
  */
19966
- activeBundleEntries(keyVersion, now = Date.now()) {
20587
+ activeBundleEntries(keyVersion, now = this.now()) {
19967
20588
  const rows = allRows(
19968
20589
  this.db.prepare(
19969
20590
  `SELECT * FROM exceptions
@@ -19979,6 +20600,7 @@ var SqliteExceptionsRepository = class {
19979
20600
  ruleId: row.rule_id,
19980
20601
  valueFingerprint: row.value_fingerprint,
19981
20602
  keyVersion: row.key_version,
20603
+ capability: row.capability,
19982
20604
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19983
20605
  maxUses: row.max_uses,
19984
20606
  useCount: row.use_count,
@@ -19994,7 +20616,7 @@ var SqliteExceptionsRepository = class {
19994
20616
  * than the retention window on every write, so the ledger self-limits.
19995
20617
  */
19996
20618
  recordBlocked(entry) {
19997
- const now = Date.now();
20619
+ const now = this.now();
19998
20620
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
19999
20621
  this.insertBlockedStmt.run({
20000
20622
  reference: entry.reference,
@@ -20017,7 +20639,7 @@ var SqliteExceptionsRepository = class {
20017
20639
  WHERE blocked_at > :cutoff
20018
20640
  ORDER BY blocked_at DESC, rowid DESC`
20019
20641
  ),
20020
- { cutoff: Date.now() - windowMs }
20642
+ { cutoff: this.now() - windowMs }
20021
20643
  );
20022
20644
  return Promise.resolve(
20023
20645
  rows.map((row) => ({
@@ -20033,6 +20655,36 @@ var SqliteExceptionsRepository = class {
20033
20655
  }))
20034
20656
  );
20035
20657
  }
20658
+ /**
20659
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20660
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20661
+ * suppression uses — plus the capability: a suppression grant must never
20662
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20663
+ * revealed value re-enters the detection scan immediately afterward and the
20664
+ * suppression match there claims the use — one crossing, one use.
20665
+ *
20666
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20667
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20668
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20669
+ */
20670
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20671
+ try {
20672
+ const at = now ?? this.now();
20673
+ const row = getRow(
20674
+ this.db.prepare(
20675
+ `SELECT id FROM exceptions
20676
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20677
+ AND key_version = :keyVersion
20678
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20679
+ LIMIT 1`
20680
+ ),
20681
+ { ruleId, valueFingerprint, keyVersion, now: at }
20682
+ );
20683
+ return Promise.resolve(row ?? null);
20684
+ } catch (err) {
20685
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20686
+ }
20687
+ }
20036
20688
  /**
20037
20689
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20038
20690
  * exhausted) whose last transition is older than the retention window.
@@ -20040,7 +20692,7 @@ var SqliteExceptionsRepository = class {
20040
20692
  * predicate, so correctness never depends on this sweep; it only bounds how
20041
20693
  * long the audit evidence is kept locally. Returns the deleted count.
20042
20694
  */
20043
- sweepTerminal(retentionMs, now = Date.now()) {
20695
+ sweepTerminal(retentionMs, now = this.now()) {
20044
20696
  const result = this.db.prepare(
20045
20697
  `DELETE FROM exceptions
20046
20698
  WHERE updated_at < :cutoff
@@ -20060,6 +20712,7 @@ function parseExceptionRow(row) {
20060
20712
  valueFingerprint: row.value_fingerprint,
20061
20713
  keyVersion: row.key_version,
20062
20714
  maskedValue: row.masked_value,
20715
+ capability: row.capability,
20063
20716
  scope: row.scope,
20064
20717
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20065
20718
  maxUses: row.max_uses,
@@ -20102,6 +20755,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20102
20755
 
20103
20756
  // ../../packages/persistence/src/repositories/findings.ts
20104
20757
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20758
+ var SCAN_BATCH_ROWS = 1e3;
20759
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20760
+ var LOCATION_RULE_IDS_CAP = 20;
20761
+ function compareLocationOrder(a, b) {
20762
+ return compareFindingGroupOrder(
20763
+ {
20764
+ severity: a.maxSeverity,
20765
+ latestDetectedAt: a.latestDetectedAt,
20766
+ id: ""
20767
+ },
20768
+ {
20769
+ severity: b.maxSeverity,
20770
+ latestDetectedAt: b.latestDetectedAt,
20771
+ id: ""
20772
+ }
20773
+ );
20774
+ }
20105
20775
  var CONCAT_SEP = ",";
20106
20776
  var TUPLE_SEP = "|";
20107
20777
  function splitConcat(value) {
@@ -20114,6 +20784,33 @@ function deriveInstanceStatus(row) {
20114
20784
  latestResolutionStatus: row.latest_status
20115
20785
  });
20116
20786
  }
20787
+ function encodeGroupCursor(group) {
20788
+ const payload = {
20789
+ sev: group.severity,
20790
+ t: group.latestDetectedAt,
20791
+ id: group.id
20792
+ };
20793
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20794
+ }
20795
+ function decodeGroupCursor(cursor) {
20796
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20797
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20798
+ return {
20799
+ severity: parsed.sev,
20800
+ latestDetectedAt: parsed.t,
20801
+ id: parsed.id
20802
+ };
20803
+ }
20804
+ return null;
20805
+ }
20806
+ function firstAfter(sorted, cursor) {
20807
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20808
+ return index === -1 ? sorted.length : index;
20809
+ }
20810
+ function findDeepLinked(sorted, page, id) {
20811
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20812
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20813
+ }
20117
20814
  var DAY_MS3 = 864e5;
20118
20815
  var SqliteFindingsRepository = class {
20119
20816
  constructor(db) {
@@ -20223,8 +20920,13 @@ var SqliteFindingsRepository = class {
20223
20920
  */
20224
20921
  listGroupedFindings(query) {
20225
20922
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20226
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20227
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20923
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20924
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20925
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20926
+ const sessionParams = {
20927
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20928
+ ...fromMs === void 0 ? {} : { fromMs }
20929
+ };
20228
20930
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20229
20931
  predicate,
20230
20932
  params: sessionParams
@@ -20232,7 +20934,8 @@ var SqliteFindingsRepository = class {
20232
20934
  const rows = allRows(
20233
20935
  this.db.prepare(
20234
20936
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20235
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
20937
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
20938
+ kind, finding_key, latest_status
20236
20939
  FROM (
20237
20940
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20238
20941
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20242,6 +20945,7 @@ var SqliteFindingsRepository = class {
20242
20945
  json_extract(e.attributes, '$.repo') AS repo,
20243
20946
  json_extract(e.attributes, '$.file_path') AS file,
20244
20947
  json_extract(e.attributes, '$.tool_name') AS tool_name,
20948
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20245
20949
  e.event_type AS kind, f.finding_key AS finding_key,
20246
20950
  latest.status AS latest_status,
20247
20951
  ROW_NUMBER() OVER (
@@ -20273,6 +20977,8 @@ var SqliteFindingsRepository = class {
20273
20977
  repo: r.repo ?? "",
20274
20978
  file: r.file ?? "",
20275
20979
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
20980
+ eventId: r.event_id,
20981
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20276
20982
  status: deriveInstanceStatus(r)
20277
20983
  }));
20278
20984
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20296,18 +21002,23 @@ var SqliteFindingsRepository = class {
20296
21002
  groups: sorted.length
20297
21003
  };
20298
21004
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21005
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21006
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21007
+ const page = sorted.slice(start, start + limit);
21008
+ const lastOnPage = page.at(-1);
21009
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21010
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20299
21011
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20300
- const items = sorted.slice(0, limit).map(
20301
- (g) => statusSet ? {
20302
- ...g,
20303
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20304
- } : g
20305
- );
21012
+ const narrow = (g) => statusSet ? {
21013
+ ...g,
21014
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21015
+ } : g;
21016
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20306
21017
  return Promise.resolve({
20307
21018
  totals,
20308
21019
  facets,
20309
21020
  items,
20310
- nextCursor: null,
21021
+ nextCursor,
20311
21022
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20312
21023
  });
20313
21024
  }
@@ -20339,6 +21050,266 @@ var SqliteFindingsRepository = class {
20339
21050
  * request actually carries a `q`. (Substring matching is unaffected by a
20340
21051
  * path repeating across tuples.)
20341
21052
  */
21053
+ /**
21054
+ * The instance-level (flat) findings list: one row per finding, newest first,
21055
+ * paged by keyset.
21056
+ *
21057
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21058
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21059
+ * them changes no reported number. Severity, subtype, provider, action,
21060
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21061
+ * facet excludes its own filter, so a row the filter rejects still has to be
21062
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21063
+ * Several could not be expressed there anyway: status comes from the one
21064
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21065
+ * none of the mappers names", which no IN-list can say.
21066
+ *
21067
+ * The scan runs from the top of the scope on every request, not from the
21068
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21069
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21070
+ * while the counting runs, and only the page itself is retained.
21071
+ */
21072
+ listFindingInstances(query) {
21073
+ const opts = {
21074
+ severity: query.severity,
21075
+ subtype: query.subtype,
21076
+ providers: query.provider,
21077
+ actions: query.action,
21078
+ statuses: query.status,
21079
+ tools: query.tool,
21080
+ repo: query.repo,
21081
+ file: query.file,
21082
+ q: query.q
21083
+ };
21084
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21085
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21086
+ const accumulator = createInstanceFacetAccumulator(opts);
21087
+ const items = [];
21088
+ let total = 0;
21089
+ let last;
21090
+ let hasMore = false;
21091
+ for (const row of this.scanFindingRows({
21092
+ sessionId: query.sessionId,
21093
+ from: query.from
21094
+ })) {
21095
+ accumulator.add(row);
21096
+ if (!matchesInstanceFilters(row, opts)) continue;
21097
+ total += 1;
21098
+ if (items.length < limit) {
21099
+ items.push(toInstanceDetail(row));
21100
+ last = row;
21101
+ } else {
21102
+ hasMore = true;
21103
+ }
21104
+ }
21105
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21106
+ if (cursor !== null) {
21107
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21108
+ return Promise.resolve({
21109
+ totals: { findings: total },
21110
+ facets: accumulator.facets(),
21111
+ items: resumed.items,
21112
+ nextCursor: resumed.nextCursor
21113
+ });
21114
+ }
21115
+ return Promise.resolve({
21116
+ totals: { findings: total },
21117
+ facets: accumulator.facets(),
21118
+ items,
21119
+ nextCursor
21120
+ });
21121
+ }
21122
+ /**
21123
+ * The page of matching rows strictly after `cursor`. Separate from the
21124
+ * counting pass because that one starts at the top of the scope by design;
21125
+ * this one narrows the scan with the same keyset predicate the activity list
21126
+ * uses, so a later page costs less than the first rather than more.
21127
+ */
21128
+ pageAfter(cursor, opts, limit, query) {
21129
+ const items = [];
21130
+ let last;
21131
+ let hasMore = false;
21132
+ for (const row of this.scanFindingRows({
21133
+ sessionId: query.sessionId,
21134
+ from: query.from,
21135
+ after: cursor
21136
+ })) {
21137
+ if (!matchesInstanceFilters(row, opts)) continue;
21138
+ if (items.length < limit) {
21139
+ items.push(toInstanceDetail(row));
21140
+ last = row;
21141
+ } else {
21142
+ hasMore = true;
21143
+ break;
21144
+ }
21145
+ }
21146
+ return {
21147
+ items,
21148
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21149
+ };
21150
+ }
21151
+ /**
21152
+ * The same findings folded by location: repository, then file within it.
21153
+ *
21154
+ * The grouping keys come from the capturing event's attributes, which is what
21155
+ * the local store relates a finding to — there is no finding↔asset row to
21156
+ * group by instead. A repo or file the event did not record folds into the
21157
+ * empty-string bucket, which the view renders but does not link, since no
21158
+ * filter can name it.
21159
+ */
21160
+ listFindingLocations(query) {
21161
+ const opts = {
21162
+ severity: query.severity,
21163
+ subtype: query.subtype,
21164
+ providers: query.provider,
21165
+ actions: query.action,
21166
+ statuses: query.status,
21167
+ tools: query.tool,
21168
+ q: query.q
21169
+ };
21170
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21171
+ const byRepo = /* @__PURE__ */ new Map();
21172
+ let total = 0;
21173
+ for (const row of this.scanFindingRows({
21174
+ sessionId: query.sessionId,
21175
+ from: query.from
21176
+ })) {
21177
+ if (!matchesInstanceFilters(row, opts)) continue;
21178
+ total += 1;
21179
+ let files = byRepo.get(row.repo);
21180
+ if (files === void 0) {
21181
+ files = /* @__PURE__ */ new Map();
21182
+ byRepo.set(row.repo, files);
21183
+ }
21184
+ let acc = files.get(row.file);
21185
+ if (acc === void 0) {
21186
+ acc = newLocationAccumulator();
21187
+ files.set(row.file, acc);
21188
+ }
21189
+ addToLocation(acc, row);
21190
+ }
21191
+ let fileCount = 0;
21192
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21193
+ fileCount += files.size;
21194
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21195
+ file: file2,
21196
+ instanceCount: acc.instanceCount,
21197
+ maxSeverity: acc.maxSeverity,
21198
+ latestDetectedAt: acc.latestDetectedAt,
21199
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21200
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21201
+ })).sort(compareLocationOrder);
21202
+ const rollup = fileRows.reduce(
21203
+ (a, f) => ({
21204
+ instanceCount: a.instanceCount + f.instanceCount,
21205
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21206
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21207
+ }),
21208
+ {
21209
+ instanceCount: 0,
21210
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21211
+ latestDetectedAt: ""
21212
+ }
21213
+ );
21214
+ const statuses = fileRows.map((f) => f.status);
21215
+ const folded = foldGroupStatus(statuses);
21216
+ return {
21217
+ repo,
21218
+ instanceCount: rollup.instanceCount,
21219
+ maxSeverity: rollup.maxSeverity,
21220
+ latestDetectedAt: rollup.latestDetectedAt,
21221
+ ...folded === void 0 ? {} : { status: folded },
21222
+ files: fileRows
21223
+ };
21224
+ });
21225
+ repos.sort(compareLocationOrder);
21226
+ return Promise.resolve({
21227
+ totals: { findings: total, repos: repos.length, files: fileCount },
21228
+ items: repos.slice(0, limit),
21229
+ hasMore: repos.length > limit
21230
+ });
21231
+ }
21232
+ /**
21233
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21234
+ *
21235
+ * A generator so a caller streams the scope without it ever being an array:
21236
+ * the flat list counts and facets the whole filtered scope, which on a large
21237
+ * store is far more rows than any page. Each batch advances the same keyset
21238
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21239
+ * rather than one unbounded result set.
21240
+ *
21241
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21242
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21243
+ * makes it a point lookup per row, and the derived table would re-materialize
21244
+ * a window over the whole resolution table once per batch.
21245
+ *
21246
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21247
+ * would be missing from its own facet, which is computed by excluding that
21248
+ * dimension — see listFindingInstances.
21249
+ */
21250
+ *scanFindingRows(scope) {
21251
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21252
+ const params = [];
21253
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21254
+ conditions.push("e.root_session_id = ?");
21255
+ params.push(scope.sessionId);
21256
+ }
21257
+ if (scope.from !== void 0) {
21258
+ conditions.push("e.started_at >= ?");
21259
+ params.push(isoToEpochMillis(scope.from));
21260
+ }
21261
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21262
+ d.severity AS severity, f.masked_match AS masked_match,
21263
+ f.action_taken AS action_taken, f.confidence AS confidence,
21264
+ e.started_at AS occurred_at,
21265
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21266
+ json_extract(e.attributes, '$.repo') AS repo,
21267
+ json_extract(e.attributes, '$.file_path') AS file,
21268
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21269
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21270
+ e.event_type AS kind, f.finding_key AS finding_key,
21271
+ ${latestResolutionStatusSql("f")} AS latest_status
21272
+ FROM inspection_findings f
21273
+ JOIN audit_events e ON e.id = f.audit_event_id
21274
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21275
+ WHERE ${conditions.join(" AND ")}
21276
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21277
+ ORDER BY e.started_at DESC, f.id DESC
21278
+ LIMIT ?`;
21279
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21280
+ for (; ; ) {
21281
+ const rows = allRows(this.db.prepare(sql), [
21282
+ ...params,
21283
+ after.startedAtMs,
21284
+ after.startedAtMs,
21285
+ after.id,
21286
+ SCAN_BATCH_ROWS
21287
+ ]);
21288
+ for (const r of rows) {
21289
+ yield {
21290
+ id: r.id,
21291
+ ruleId: r.rule_id,
21292
+ category: r.category,
21293
+ severity: r.severity,
21294
+ maskedMatch: r.masked_match,
21295
+ actionTaken: r.action_taken,
21296
+ confidence: r.confidence,
21297
+ occurredAt: epochMillisToIso(r.occurred_at),
21298
+ sourceTool: r.source_tool,
21299
+ repo: r.repo ?? "",
21300
+ file: r.file ?? "",
21301
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21302
+ eventId: r.event_id,
21303
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21304
+ status: deriveInstanceStatus(r)
21305
+ };
21306
+ }
21307
+ if (rows.length < SCAN_BATCH_ROWS) return;
21308
+ const lastRow = rows[rows.length - 1];
21309
+ if (lastRow === void 0) return;
21310
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21311
+ }
21312
+ }
20342
21313
  groupAggregates(withSearchText, scope) {
20343
21314
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20344
21315
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20599,7 +21570,7 @@ var SqliteInspectionFindingsRepository = class {
20599
21570
  };
20600
21571
 
20601
21572
  // ../../packages/persistence/src/repositories/installed-packs.ts
20602
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21573
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20603
21574
 
20604
21575
  // ../../packages/persistence/src/semver.ts
20605
21576
  function parse3(version2) {
@@ -20750,7 +21721,7 @@ var SqliteInstalledPacksRepository = class {
20750
21721
  let behind = false;
20751
21722
  for (const row of rows) {
20752
21723
  const params = {
20753
- id: randomUUID2(),
21724
+ id: randomUUID3(),
20754
21725
  namespace: row.namespace,
20755
21726
  packId: row.packId,
20756
21727
  version: row.version,
@@ -20762,7 +21733,7 @@ var SqliteInstalledPacksRepository = class {
20762
21733
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20763
21734
  this.upsertAvailableStmt.run({
20764
21735
  ...params,
20765
- id: randomUUID2(),
21736
+ id: randomUUID3(),
20766
21737
  recordedBy: meta3?.recordedBy ?? null
20767
21738
  });
20768
21739
  } else {
@@ -21085,14 +22056,15 @@ var SqliteInventoryRepository = class {
21085
22056
  };
21086
22057
 
21087
22058
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21088
- import { randomUUID as randomUUID3 } from "crypto";
22059
+ import { randomUUID as randomUUID4 } from "crypto";
21089
22060
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21090
22061
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21091
22062
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21092
22063
  var HARNESS_LABELS = {
21093
22064
  claudecode: "Claude Code",
21094
22065
  cursor: "Cursor",
21095
- codex: "Codex"
22066
+ codex: "Codex",
22067
+ antigravity: "Antigravity"
21096
22068
  };
21097
22069
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21098
22070
  var EMPTY_PROJECT_AGG = {
@@ -21107,6 +22079,7 @@ function resolveHarnessId(attrs, row) {
21107
22079
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21108
22080
  if (t.includes("cursor")) return "cursor";
21109
22081
  if (t.includes("codex")) return "codex";
22082
+ if (t.includes("antigravity")) return "antigravity";
21110
22083
  return null;
21111
22084
  }
21112
22085
  function isLiveRealClaudeCode(rows) {
@@ -21565,7 +22538,7 @@ var SqliteInventoryAssetsRepository = class {
21565
22538
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21566
22539
  VALUES (:id, :projectId, :path, :access, :now, :now)
21567
22540
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21568
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22541
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21569
22542
  }
21570
22543
  return true;
21571
22544
  }
@@ -21586,7 +22559,7 @@ var SqliteInventoryAssetsRepository = class {
21586
22559
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21587
22560
  VALUES (:id, :assetId, :trust, :now, :now)
21588
22561
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21589
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22562
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21590
22563
  }
21591
22564
  this.configRowsCache = void 0;
21592
22565
  return "ok";
@@ -21883,7 +22856,7 @@ var SqliteInventoryAssetsRepository = class {
21883
22856
  };
21884
22857
 
21885
22858
  // ../../packages/persistence/src/repositories/policies.ts
21886
- import { randomUUID as randomUUID4 } from "crypto";
22859
+ import { randomUUID as randomUUID5 } from "crypto";
21887
22860
  var SqlitePoliciesRepository = class {
21888
22861
  constructor(db) {
21889
22862
  this.db = db;
@@ -21918,7 +22891,7 @@ var SqlitePoliciesRepository = class {
21918
22891
  failOpenTransaction(this.db, () => {
21919
22892
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
21920
22893
  stmt.run({
21921
- id: randomUUID4(),
22894
+ id: randomUUID5(),
21922
22895
  target: JSON.stringify({ category }),
21923
22896
  action,
21924
22897
  now: Date.now()
@@ -21938,7 +22911,7 @@ var SqlitePoliciesRepository = class {
21938
22911
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21939
22912
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
21940
22913
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21941
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22914
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
21942
22915
  }
21943
22916
  // Caps every global per-category policy currently set to block/redact down
21944
22917
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22006,7 +22979,7 @@ var SqlitePolicyCatalogRepository = class {
22006
22979
  };
22007
22980
 
22008
22981
  // ../../packages/persistence/src/repositories/project-files.ts
22009
- import { randomUUID as randomUUID5 } from "crypto";
22982
+ import { randomUUID as randomUUID6 } from "crypto";
22010
22983
  var SqliteProjectFilesRepository = class {
22011
22984
  constructor(db) {
22012
22985
  this.db = db;
@@ -22038,7 +23011,7 @@ var SqliteProjectFilesRepository = class {
22038
23011
  const stamp = Math.max(now, maxStamp + 1);
22039
23012
  for (const file2 of scan2.files) {
22040
23013
  this.upsertStmt.run({
22041
- id: randomUUID5(),
23014
+ id: randomUUID6(),
22042
23015
  projectId,
22043
23016
  path: file2.path,
22044
23017
  name: file2.name,
@@ -22052,7 +23025,7 @@ var SqliteProjectFilesRepository = class {
22052
23025
  };
22053
23026
 
22054
23027
  // ../../packages/persistence/src/repositories/resolutions.ts
22055
- import { randomUUID as randomUUID6 } from "crypto";
23028
+ import { randomUUID as randomUUID7 } from "crypto";
22056
23029
  var SqliteResolutionsRepository = class {
22057
23030
  constructor(db, now = () => Date.now()) {
22058
23031
  this.db = db;
@@ -22106,7 +23079,7 @@ var SqliteResolutionsRepository = class {
22106
23079
  */
22107
23080
  insertResolution(r) {
22108
23081
  this.insertStmt.run({
22109
- id: randomUUID6(),
23082
+ id: randomUUID7(),
22110
23083
  findingKey: r.findingKey,
22111
23084
  status: FindingStatus.parse(r.status),
22112
23085
  method: ResolutionMethod.parse(r.method),
@@ -22165,13 +23138,51 @@ var SqliteRuleProbeCacheRepository = class {
22165
23138
  this.readStmt = db.prepare(
22166
23139
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22167
23140
  );
23141
+ this.countQuarantinedStmt = db.prepare(
23142
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23143
+ );
23144
+ this.clearQuarantinedStmt = db.prepare(
23145
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23146
+ );
22168
23147
  }
22169
23148
  db;
22170
23149
  upsertStmt;
22171
23150
  readStmt;
23151
+ countQuarantinedStmt;
23152
+ clearQuarantinedStmt;
22172
23153
  getVerdict(ruleKey) {
22173
23154
  return getRow(this.readStmt, { ruleKey });
22174
23155
  }
23156
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23157
+ countQuarantined() {
23158
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23159
+ }
23160
+ /**
23161
+ * Forgets every quarantine verdict, so the rules behind them are measured
23162
+ * again on the next load. This is the undo for a verdict the machine reached
23163
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23164
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23165
+ * loaded or slow machine can reach about a rule that is in fact fine.
23166
+ *
23167
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23168
+ * keeping, and dropping it would make every rule pay the battery again.
23169
+ *
23170
+ * Reports `refused` from the write's own result rather than inferring it from
23171
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23172
+ * swallows a contended DELETE (another writer holding the lock past
23173
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23174
+ * leaves the count unchanged, which is indistinguishable from "there was
23175
+ * nothing to clear". An undo that reports success while the quarantines are
23176
+ * still in place is worse than one that fails, because the rules it claimed
23177
+ * to restore are silently still disabled.
23178
+ */
23179
+ clearQuarantined() {
23180
+ const before = this.countQuarantined();
23181
+ const committed = failOpenTransaction(this.db, () => {
23182
+ this.clearQuarantinedStmt.run();
23183
+ });
23184
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23185
+ }
22175
23186
  setVerdict(ruleKey, verdict, worstProbeMs) {
22176
23187
  failOpenTransaction(this.db, () => {
22177
23188
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -22225,6 +23236,419 @@ var SqliteScanLedgerRepository = class {
22225
23236
  }
22226
23237
  };
22227
23238
 
23239
+ // ../../packages/persistence/src/repositories/secret-vault.ts
23240
+ import { randomUUID as randomUUID8 } from "crypto";
23241
+ function pageLimit(requested, fallback) {
23242
+ if (requested === void 0) return fallback;
23243
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23244
+ }
23245
+ function encodeReuseCursor(payload) {
23246
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23247
+ }
23248
+ function decodeReuseCursor(cursor) {
23249
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23250
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23251
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23252
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23253
+ // malformed cursor must never produce, since restarting from the top is the
23254
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23255
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23256
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23257
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23258
+ }
23259
+ return null;
23260
+ }
23261
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23262
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23263
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23264
+ v.occurrence_count, v.first_seen, v.last_seen`;
23265
+ function toSighting(row) {
23266
+ return {
23267
+ location: row.location,
23268
+ kind: row.kind,
23269
+ firstSeen: new Date(row.first_seen).toISOString(),
23270
+ lastSeen: new Date(row.last_seen).toISOString()
23271
+ };
23272
+ }
23273
+ var SELECT_COLUMNS = `
23274
+ pointer_id AS pointerId,
23275
+ value_fingerprint AS valueFingerprint,
23276
+ fingerprint_key_version AS fingerprintKeyVersion,
23277
+ key_version AS keyVersion,
23278
+ format_version AS formatVersion,
23279
+ category,
23280
+ rule_id AS ruleId,
23281
+ masked_match AS maskedMatch,
23282
+ provider,
23283
+ ciphertext,
23284
+ nonce,
23285
+ auth_tag AS authTag,
23286
+ occurrence_count AS occurrenceCount,
23287
+ first_seen AS firstSeen,
23288
+ last_seen AS lastSeen`;
23289
+ function toRow(raw) {
23290
+ const { provider, ...rest } = raw;
23291
+ return provider === null ? rest : { ...rest, provider };
23292
+ }
23293
+ var SqliteSecretVaultRepository = class {
23294
+ constructor(db) {
23295
+ this.db = db;
23296
+ this.insertStmt = db.prepare(
23297
+ `INSERT INTO secret_vault (
23298
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
23299
+ format_version, category, rule_id, masked_match, provider,
23300
+ ciphertext, nonce, auth_tag,
23301
+ occurrence_count, first_seen, last_seen
23302
+ ) VALUES (
23303
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
23304
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
23305
+ :ciphertext, :nonce, :authTag,
23306
+ 1, :now, :now
23307
+ )`
23308
+ );
23309
+ this.bumpStmt = db.prepare(
23310
+ `UPDATE secret_vault
23311
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
23312
+ WHERE value_fingerprint = :valueFingerprint`
23313
+ );
23314
+ this.byPointerStmt = db.prepare(
23315
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
23316
+ );
23317
+ this.byFingerprintStmt = db.prepare(
23318
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
23319
+ );
23320
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
23321
+ this.replaceCiphertextStmt = db.prepare(
23322
+ `UPDATE secret_vault
23323
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
23324
+ WHERE pointer_id = :pointerId`
23325
+ );
23326
+ this.refreshFingerprintStmt = db.prepare(
23327
+ `UPDATE secret_vault
23328
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
23329
+ WHERE pointer_id = :pointerId`
23330
+ );
23331
+ this.derefStmt = db.prepare(
23332
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
23333
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
23334
+ );
23335
+ }
23336
+ db;
23337
+ insertStmt;
23338
+ bumpStmt;
23339
+ byPointerStmt;
23340
+ byFingerprintStmt;
23341
+ listStmt;
23342
+ replaceCiphertextStmt;
23343
+ refreshFingerprintStmt;
23344
+ derefStmt;
23345
+ /**
23346
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
23347
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
23348
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
23349
+ * pointer, category and ciphertext, so the same secret always resolves to one
23350
+ * wire token. `minted` is true only when this call created the row.
23351
+ *
23352
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
23353
+ * writers cannot both decide they are minting.
23354
+ */
23355
+ upsert(input, now) {
23356
+ let minted = false;
23357
+ withTransaction(
23358
+ this.db,
23359
+ () => {
23360
+ const existing = getRow(this.byFingerprintStmt, {
23361
+ valueFingerprint: input.valueFingerprint
23362
+ });
23363
+ if (existing === void 0) {
23364
+ this.insertStmt.run(
23365
+ bindParams({
23366
+ pointerId: input.pointerId,
23367
+ valueFingerprint: input.valueFingerprint,
23368
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
23369
+ keyVersion: input.keyVersion,
23370
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
23371
+ category: input.category,
23372
+ ruleId: input.ruleId,
23373
+ maskedMatch: input.maskedMatch,
23374
+ provider: input.provider,
23375
+ ciphertext: input.ciphertext,
23376
+ nonce: input.nonce,
23377
+ authTag: input.authTag,
23378
+ now
23379
+ })
23380
+ );
23381
+ minted = true;
23382
+ return;
23383
+ }
23384
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
23385
+ },
23386
+ "IMMEDIATE"
23387
+ );
23388
+ const row = getRow(this.byFingerprintStmt, {
23389
+ valueFingerprint: input.valueFingerprint
23390
+ });
23391
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
23392
+ return { row: toRow(row), minted };
23393
+ }
23394
+ byPointerId(pointerId) {
23395
+ const raw = getRow(this.byPointerStmt, { pointerId });
23396
+ return raw === void 0 ? null : toRow(raw);
23397
+ }
23398
+ byValueFingerprint(fingerprint) {
23399
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
23400
+ return raw === void 0 ? null : toRow(raw);
23401
+ }
23402
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
23403
+ recordDeref(entry) {
23404
+ this.derefStmt.run(
23405
+ bindParams({
23406
+ id: entry.id,
23407
+ pointerId: entry.pointerId,
23408
+ at: entry.at,
23409
+ target: entry.target,
23410
+ reason: entry.reason,
23411
+ outcome: entry.outcome,
23412
+ grantId: entry.grantId,
23413
+ pointerCount: entry.pointerCount ?? 1
23414
+ })
23415
+ );
23416
+ }
23417
+ listAll() {
23418
+ return allRows(this.listStmt).map(toRow);
23419
+ }
23420
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
23421
+ replaceCiphertext(pointerId, next) {
23422
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
23423
+ }
23424
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
23425
+ refreshFingerprint(pointerId, next) {
23426
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
23427
+ }
23428
+ /**
23429
+ * Destroy every vaulted value and report how many were destroyed. The deref
23430
+ * audit is left alone on purpose — see the table note above.
23431
+ */
23432
+ purgeAll() {
23433
+ let destroyed = 0;
23434
+ withTransaction(
23435
+ this.db,
23436
+ () => {
23437
+ destroyed = this.countEntries();
23438
+ this.db.exec("DELETE FROM secret_vault");
23439
+ },
23440
+ "IMMEDIATE"
23441
+ );
23442
+ return destroyed;
23443
+ }
23444
+ /**
23445
+ * Record (or re-stamp) one place a pointer has been written. One row per
23446
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
23447
+ * on hook paths — a failure must never affect the rewrite that triggered it,
23448
+ * so callers wrap this, not the other way around.
23449
+ */
23450
+ recordSighting(entry, now) {
23451
+ this.db.prepare(
23452
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
23453
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
23454
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
23455
+ ).run({
23456
+ id: randomUUID8(),
23457
+ pointerId: entry.pointerId,
23458
+ location: entry.location,
23459
+ kind: entry.kind,
23460
+ now
23461
+ });
23462
+ }
23463
+ /**
23464
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23465
+ * than one query per row. A pointer with no sightings still gets an entry, so
23466
+ * the caller never has to distinguish "none" from "missing".
23467
+ *
23468
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23469
+ * the instance the way the fixed-shape ones in the constructor are.
23470
+ */
23471
+ sightingsFor(pointerIds) {
23472
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23473
+ if (pointerIds.length === 0) return byPointer;
23474
+ const rows = allRows(
23475
+ this.db.prepare(
23476
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23477
+ FROM secret_vault_sighting
23478
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23479
+ ORDER BY last_seen DESC`
23480
+ ),
23481
+ pointerIds
23482
+ );
23483
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23484
+ return byPointer;
23485
+ }
23486
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23487
+ toInventoryEntries(rows) {
23488
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
23489
+ return rows.map((r) => ({
23490
+ pointerId: r.pointer_id,
23491
+ category: r.category,
23492
+ ...r.provider === null ? {} : { provider: r.provider },
23493
+ maskedMatch: r.masked_match,
23494
+ occurrences: r.occurrence_count,
23495
+ firstSeen: new Date(r.first_seen).toISOString(),
23496
+ lastSeen: new Date(r.last_seen).toISOString(),
23497
+ revealGrantId: r.grant_id,
23498
+ sightings: sightings.get(r.pointer_id) ?? []
23499
+ }));
23500
+ }
23501
+ /**
23502
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23503
+ * value's descriptor data joined with its sightings and the active
23504
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23505
+ * the fingerprint nor the ciphertext columns are selected.
23506
+ *
23507
+ * `totals.values` counts the whole store, not the page, so the count a reader
23508
+ * sees never depends on how far they have paged.
23509
+ */
23510
+ listInventory(query = {}, now = Date.now()) {
23511
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23512
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23513
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
23514
+ const rows = allRows(
23515
+ this.db.prepare(
23516
+ `SELECT ${INVENTORY_COLUMNS},
23517
+ (SELECT e.id FROM exceptions e
23518
+ WHERE e.rule_id = v.rule_id
23519
+ AND e.value_fingerprint = v.value_fingerprint
23520
+ AND e.key_version = v.fingerprint_key_version
23521
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23522
+ LIMIT 1) AS grant_id
23523
+ FROM secret_vault v
23524
+ ${where}
23525
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23526
+ LIMIT :limit`
23527
+ ),
23528
+ bindParams({
23529
+ now,
23530
+ limit: limit + 1,
23531
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23532
+ })
23533
+ );
23534
+ const hasMore = rows.length > limit;
23535
+ const page = hasMore ? rows.slice(0, limit) : rows;
23536
+ const last = page[page.length - 1];
23537
+ return {
23538
+ totals: { values: this.countEntries() },
23539
+ items: this.toInventoryEntries(page),
23540
+ // Minted from the last row of the PAGE, never the extra probe row.
23541
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23542
+ };
23543
+ }
23544
+ /**
23545
+ * Values reused on this machine — detected more than once, or written to more
23546
+ * than one location — most-reused first, one page at a time.
23547
+ *
23548
+ * Its own read rather than a filter over an inventory page: reuse is a
23549
+ * property of the whole store, and deriving it from 50 newest rows would
23550
+ * under-report exactly the values a reader most needs to see.
23551
+ */
23552
+ listReuse(query = {}, now = Date.now()) {
23553
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23554
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23555
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23556
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23557
+ const rows = allRows(
23558
+ this.db.prepare(
23559
+ `SELECT ${INVENTORY_COLUMNS},
23560
+ (SELECT e.id FROM exceptions e
23561
+ WHERE e.rule_id = v.rule_id
23562
+ AND e.value_fingerprint = v.value_fingerprint
23563
+ AND e.key_version = v.fingerprint_key_version
23564
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23565
+ LIMIT 1) AS grant_id
23566
+ FROM secret_vault v
23567
+ WHERE ${REUSED_PREDICATE} ${after}
23568
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23569
+ LIMIT :limit`
23570
+ ),
23571
+ bindParams({
23572
+ now,
23573
+ limit: limit + 1,
23574
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23575
+ })
23576
+ );
23577
+ const hasMore = rows.length > limit;
23578
+ const page = hasMore ? rows.slice(0, limit) : rows;
23579
+ const last = page[page.length - 1];
23580
+ return {
23581
+ totals: { reused: this.countReused() },
23582
+ items: this.toInventoryEntries(page),
23583
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23584
+ };
23585
+ }
23586
+ /**
23587
+ * The de-reference trail, newest first, one page at a time. By default the
23588
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23589
+ * instead — the rows that matter as a signal are the model crossings, and
23590
+ * burying them under render noise would defeat the audit's purpose.
23591
+ *
23592
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23593
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23594
+ * the reader pages.
23595
+ */
23596
+ listDerefs(query = {}) {
23597
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23598
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23599
+ const conditions = [];
23600
+ if (query.includeBatched !== true) {
23601
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23602
+ }
23603
+ if (cursor !== null) {
23604
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23605
+ }
23606
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
23607
+ const rows = allRows(
23608
+ this.db.prepare(
23609
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
23610
+ FROM secret_vault_deref ${where}
23611
+ ORDER BY at DESC, id DESC LIMIT :limit`
23612
+ ),
23613
+ bindParams({
23614
+ limit: limit + 1,
23615
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23616
+ })
23617
+ );
23618
+ const hasMore = rows.length > limit;
23619
+ const page = hasMore ? rows.slice(0, limit) : rows;
23620
+ const last = page[page.length - 1];
23621
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
23622
+ this.db,
23623
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
23624
+ );
23625
+ return {
23626
+ items: page.map((r) => ({
23627
+ id: r.id,
23628
+ pointerId: r.pointer_id,
23629
+ at: new Date(r.at).toISOString(),
23630
+ target: r.target,
23631
+ reason: r.reason,
23632
+ outcome: r.outcome,
23633
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
23634
+ pointerCount: r.pointer_count
23635
+ })),
23636
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
23637
+ hiddenBatched
23638
+ };
23639
+ }
23640
+ countEntries() {
23641
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
23642
+ }
23643
+ /** Values reused on this machine — the reuse list's page-independent total. */
23644
+ countReused() {
23645
+ return countScalar(
23646
+ this.db,
23647
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23648
+ );
23649
+ }
23650
+ };
23651
+
22228
23652
  // ../../packages/persistence/src/repositories/security.ts
22229
23653
  var DAY_MS4 = 864e5;
22230
23654
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22237,7 +23661,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22237
23661
  var SCAN_COVERAGE = [
22238
23662
  { provider: "claudecode", coverage: 100, supported: true },
22239
23663
  { provider: "cursor", coverage: 0, supported: false },
22240
- { provider: "codex", coverage: 0, supported: false },
23664
+ { provider: "codex", coverage: 80, supported: true },
23665
+ { provider: "antigravity", coverage: 60, supported: true },
23666
+ { provider: "claudeai", coverage: 0, supported: false },
22241
23667
  { provider: "chatgpt", coverage: 0, supported: false },
22242
23668
  { provider: "copilot", coverage: 0, supported: false },
22243
23669
  { provider: "api", coverage: 0, supported: false }
@@ -22570,7 +23996,7 @@ var SqliteSecurityRepository = class {
22570
23996
  };
22571
23997
 
22572
23998
  // ../../packages/persistence/src/repositories/shares.ts
22573
- import { randomUUID as randomUUID7 } from "crypto";
23999
+ import { randomUUID as randomUUID9 } from "crypto";
22574
24000
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22575
24001
  var IN_CHUNK = 500;
22576
24002
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22826,7 +24252,7 @@ var SqliteSharesRepository = class {
22826
24252
  (id, destination_id, host, decision, created_at, updated_at)
22827
24253
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22828
24254
  ).run({
22829
- id: randomUUID7(),
24255
+ id: randomUUID9(),
22830
24256
  destinationId,
22831
24257
  host: dest.host,
22832
24258
  decision,
@@ -22975,7 +24401,7 @@ var SqliteSharesRepository = class {
22975
24401
  let destinationId = destIds.get(hit.host);
22976
24402
  if (destinationId === void 0) {
22977
24403
  destStmt.run({
22978
- id: randomUUID7(),
24404
+ id: randomUUID9(),
22979
24405
  kind: hit.kind,
22980
24406
  name: hit.name,
22981
24407
  host: hit.host,
@@ -22991,7 +24417,7 @@ var SqliteSharesRepository = class {
22991
24417
  let endpointId = endpointIds.get(endpointKey);
22992
24418
  if (endpointId === void 0) {
22993
24419
  endpointStmt.run({
22994
- id: randomUUID7(),
24420
+ id: randomUUID9(),
22995
24421
  destinationId,
22996
24422
  method: hit.method,
22997
24423
  transport: hit.transport,
@@ -23004,7 +24430,7 @@ var SqliteSharesRepository = class {
23004
24430
  endpointIds.set(endpointKey, endpointId);
23005
24431
  }
23006
24432
  siteStmt.run({
23007
- id: randomUUID7(),
24433
+ id: randomUUID9(),
23008
24434
  endpointId,
23009
24435
  project: input.project,
23010
24436
  projectKey: input.projectKey,
@@ -23369,6 +24795,9 @@ function purgeSampleData(db) {
23369
24795
  }
23370
24796
 
23371
24797
  // ../../packages/persistence/src/database.ts
24798
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24799
+ "aka.persistence.unsafeTestOnlyRawHandle"
24800
+ );
23372
24801
  function linkHost(input, hostId) {
23373
24802
  return hostId ? { ...input, hostId } : input;
23374
24803
  }
@@ -23390,21 +24819,34 @@ function openWithPragmas(file2) {
23390
24819
  }
23391
24820
  return db;
23392
24821
  }
23393
- function backupLegacyStore(file2) {
23394
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23395
- renameSync2(file2, backup);
23396
- tightenFile(backup);
23397
- for (const sidecar of dbSidecars(file2)) {
23398
- if (existsSync(sidecar)) rmSync2(sidecar);
24822
+ function backupLegacyStore(db, file2) {
24823
+ reapStalePartials(file2);
24824
+ const backup = backupPath(file2, "legacy");
24825
+ let snapshotted = false;
24826
+ let snapshotError;
24827
+ try {
24828
+ snapshotStore(db, backup);
24829
+ snapshotted = true;
24830
+ } catch (error51) {
24831
+ snapshotError = error51;
24832
+ } finally {
24833
+ db.close();
24834
+ }
24835
+ if (!snapshotted) {
24836
+ akaWarn(
24837
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24838
+ );
24839
+ moveStoreAside(file2, backup);
24840
+ return backup;
23399
24841
  }
24842
+ discardStore(file2, backup);
23400
24843
  return backup;
23401
24844
  }
23402
24845
  function openAndInitialize(file2) {
23403
24846
  let db = openWithPragmas(file2);
23404
24847
  try {
23405
24848
  if (isForeignSqliteLineage(db)) {
23406
- db.close();
23407
- const backup = backupLegacyStore(file2);
24849
+ const backup = backupLegacyStore(db, file2);
23408
24850
  db = openWithPragmas(file2);
23409
24851
  akaWarn(
23410
24852
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23420,6 +24862,7 @@ function openAndInitialize(file2) {
23420
24862
  policies,
23421
24863
  installedPacks,
23422
24864
  scanLedger: new SqliteScanLedgerRepository(db),
24865
+ secretVault: new SqliteSecretVaultRepository(db),
23423
24866
  exceptions: new SqliteExceptionsRepository(db),
23424
24867
  resolutions: new SqliteResolutionsRepository(db),
23425
24868
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23447,7 +24890,7 @@ function openAndInitialize(file2) {
23447
24890
  }
23448
24891
  function openLocalDatabase(dir) {
23449
24892
  ensureDataDirSync(dir);
23450
- const file2 = join(dir, DB_FILENAME);
24893
+ const file2 = join2(dir, DB_FILENAME);
23451
24894
  const {
23452
24895
  db,
23453
24896
  events,
@@ -23455,6 +24898,7 @@ function openLocalDatabase(dir) {
23455
24898
  policies,
23456
24899
  installedPacks,
23457
24900
  scanLedger,
24901
+ secretVault,
23458
24902
  exceptions,
23459
24903
  resolutions,
23460
24904
  ruleProbeCache,
@@ -23563,7 +25007,7 @@ function openLocalDatabase(dir) {
23563
25007
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23564
25008
  if (!definitionId) continue;
23565
25009
  inspectionFindings.insertFinding({
23566
- id: randomUUID8(),
25010
+ id: randomUUID10(),
23567
25011
  auditEventId: record2.scanEvent.id,
23568
25012
  inspectionDefinitionId: definitionId,
23569
25013
  span: finding.span,
@@ -23640,6 +25084,7 @@ function openLocalDatabase(dir) {
23640
25084
  policies,
23641
25085
  installedPacks,
23642
25086
  scanLedger,
25087
+ secretVault,
23643
25088
  exceptions,
23644
25089
  resolutions,
23645
25090
  ruleProbeCache,
@@ -23668,22 +25113,38 @@ function openLocalDatabase(dir) {
23668
25113
  transaction,
23669
25114
  close: () => {
23670
25115
  db.close();
23671
- }
25116
+ },
25117
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25118
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
23672
25119
  };
23673
25120
  }
23674
25121
 
25122
+ // ../../packages/persistence/src/file-lock.ts
25123
+ import { randomUUID as randomUUID11 } from "crypto";
25124
+ import {
25125
+ closeSync,
25126
+ existsSync as existsSync2,
25127
+ openSync,
25128
+ readFileSync,
25129
+ rmSync as rmSync3,
25130
+ statSync as statSync2,
25131
+ writeFileSync as writeFileSync2
25132
+ } from "fs";
25133
+ import { hostname as hostname3 } from "os";
25134
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25135
+
23675
25136
  // ../../packages/persistence/src/finding-key.ts
23676
25137
  import { createHash as createHash3 } from "crypto";
23677
25138
 
23678
25139
  // ../../packages/persistence/src/fingerprint.ts
23679
25140
  import { createHmac, randomBytes } from "crypto";
23680
- import { existsSync as existsSync2, readFileSync } from "fs";
23681
- import { join as join2 } from "path";
25141
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25142
+ import { join as join3 } from "path";
23682
25143
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23683
- var KEY_FILENAME = "exception.key";
25144
+ var EXCEPTION_KEY_FILENAME = "exception.key";
23684
25145
  var KEY_MATERIAL_BYTES = 32;
23685
25146
  function keyFilePath(dataDir2) {
23686
- return join2(dataDir2, KEY_FILENAME);
25147
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
23687
25148
  }
23688
25149
  function parseKeyFile(raw) {
23689
25150
  const parsed = JSON.parse(raw);
@@ -23706,7 +25167,7 @@ function parseKeyFile(raw) {
23706
25167
  function readFingerprintKey(dataDir2) {
23707
25168
  let raw;
23708
25169
  try {
23709
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25170
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
23710
25171
  } catch (err) {
23711
25172
  if (err.code === "ENOENT") return null;
23712
25173
  throw err instanceof Error ? err : new Error(String(err));
@@ -23718,18 +25179,18 @@ function readFingerprintKey(dataDir2) {
23718
25179
  import { renameSync as renameSync3 } from "fs";
23719
25180
  import { mkdir } from "fs/promises";
23720
25181
  import { homedir } from "os";
23721
- import { join as join3 } from "path";
25182
+ import { join as join4 } from "path";
23722
25183
  function defaultDataDir() {
23723
- return join3(homedir(), ".aka");
25184
+ return join4(homedir(), ".aka");
23724
25185
  }
23725
25186
  function settingsDir(base = defaultDataDir()) {
23726
- return join3(base, "settings");
25187
+ return join4(base, "settings");
23727
25188
  }
23728
25189
  function dataDir(base = defaultDataDir()) {
23729
- return join3(base, "data");
25190
+ return join4(base, "data");
23730
25191
  }
23731
25192
  function dbPath(base = defaultDataDir()) {
23732
- return join3(dataDir(base), "aka.db");
25193
+ return join4(dataDir(base), "aka.db");
23733
25194
  }
23734
25195
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23735
25196
  ensureDataDirSync(dir);
@@ -23742,8 +25203,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23742
25203
  for (const { name, dest } of moves) {
23743
25204
  try {
23744
25205
  ensureDataDirSync(dest);
23745
- const moved = join3(dest, name);
23746
- renameSync3(join3(base, name), moved);
25206
+ const moved = join4(dest, name);
25207
+ renameSync3(join4(base, name), moved);
23747
25208
  tightenFile(moved);
23748
25209
  } catch {
23749
25210
  }
@@ -23751,10 +25212,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23751
25212
  }
23752
25213
 
23753
25214
  // ../../packages/persistence/src/settings.ts
23754
- import { readFileSync as readFileSync2 } from "fs";
23755
- import { join as join4 } from "path";
25215
+ import { readFileSync as readFileSync3 } from "fs";
25216
+ import { join as join5 } from "path";
25217
+ var SETTINGS_FILENAME = "settings.json";
23756
25218
  function readWorkspaceSettings(base = defaultDataDir()) {
23757
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25219
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
23758
25220
  if (!record2) return defaultWorkspaceSettings();
23759
25221
  try {
23760
25222
  return WorkspaceSettings.parse(record2);
@@ -23765,23 +25227,49 @@ function readWorkspaceSettings(base = defaultDataDir()) {
23765
25227
  function readJson(file2) {
23766
25228
  let text;
23767
25229
  try {
23768
- text = readFileSync2(file2, "utf8");
25230
+ text = readFileSync3(file2, "utf8");
23769
25231
  } catch {
23770
25232
  return null;
23771
25233
  }
23772
25234
  return parseJsonObject(text) ?? null;
23773
25235
  }
23774
25236
 
25237
+ // ../../packages/persistence/src/vault/crypto.ts
25238
+ import {
25239
+ createCipheriv,
25240
+ createDecipheriv,
25241
+ createHmac as createHmac2,
25242
+ hkdfSync,
25243
+ timingSafeEqual
25244
+ } from "crypto";
25245
+
25246
+ // ../../packages/persistence/src/vault/key-provider.ts
25247
+ import { execFileSync } from "child_process";
25248
+ import { randomBytes as randomBytes2 } from "crypto";
25249
+ import {
25250
+ chmodSync as chmodSync2,
25251
+ mkdirSync as mkdirSync2,
25252
+ readFileSync as readFileSync4,
25253
+ renameSync as renameSync4,
25254
+ rmSync as rmSync4,
25255
+ statSync as statSync3,
25256
+ writeFileSync as writeFileSync3
25257
+ } from "fs";
25258
+ import { join as join6 } from "path";
25259
+
25260
+ // ../../packages/persistence/src/vault/vault.ts
25261
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
25262
+
23775
25263
  // ../../packages/persistence/src/warn-era-cap.ts
23776
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
23777
- import { join as join5 } from "path";
25264
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
25265
+ import { join as join7 } from "path";
23778
25266
  var MARKER = "warn-era-capped";
23779
25267
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23780
25268
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23781
- const marker = join5(dataDir2, MARKER);
23782
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25269
+ const marker = join7(dataDir2, MARKER);
25270
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
23783
25271
  const capped = db.policies.capCategoryActions();
23784
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
25272
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
23785
25273
  `, { mode: DATA_FILE_MODE });
23786
25274
  return { capped };
23787
25275
  }
@@ -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,7 +28467,7 @@ 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/hooks/shared.ts
@@ -26975,10 +28494,58 @@ async function readStdin() {
26975
28494
  });
26976
28495
  }
26977
28496
 
28497
+ // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
28498
+ import { writeFileSync as writeFileSync7 } from "fs";
28499
+ import { join as join14 } from "path";
28500
+
28501
+ // ../../packages/setup-wizard/src/triage/plan-file.ts
28502
+ import { mkdtempSync, readFileSync as readFileSync9, rmdirSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
28503
+ import { tmpdir } from "os";
28504
+ import { basename as basename6, dirname as dirname4, join as join15 } from "path";
28505
+ var SuppressionEntrySchema = external_exports.object({
28506
+ ruleId: external_exports.string(),
28507
+ category: DetectionCategory,
28508
+ valueFingerprint: external_exports.string(),
28509
+ keyVersion: external_exports.number(),
28510
+ maskedValue: external_exports.string(),
28511
+ justification: external_exports.string()
28512
+ });
28513
+ var ShowcaseCategorySchema = external_exports.object({
28514
+ category: DetectionCategory,
28515
+ action: BuiltinPolicyId,
28516
+ genuineCount: external_exports.number(),
28517
+ fpCount: external_exports.number(),
28518
+ reasoning: external_exports.string()
28519
+ });
28520
+ var JoinEntrySchema = external_exports.object({
28521
+ id: external_exports.string(),
28522
+ ruleId: external_exports.string(),
28523
+ category: DetectionCategory,
28524
+ valueFingerprint: external_exports.string().optional(),
28525
+ keyVersion: external_exports.number().optional(),
28526
+ maskedMatch: external_exports.string(),
28527
+ maskedContext: external_exports.string()
28528
+ });
28529
+ var PLAN_FILE_VERSION = 3;
28530
+ var PersistedPlanSchema = external_exports.object({
28531
+ version: external_exports.literal(PLAN_FILE_VERSION),
28532
+ // partialRecord (not record): a posture only covers the categories present in
28533
+ // the evidence, so an exhaustive-key record would reject every real plan.
28534
+ posture: external_exports.partialRecord(DetectionCategory, BuiltinPolicyId),
28535
+ entries: external_exports.array(SuppressionEntrySchema),
28536
+ showcase: external_exports.array(ShowcaseCategorySchema),
28537
+ join: external_exports.array(JoinEntrySchema),
28538
+ notes: external_exports.string(),
28539
+ // The store's per-category action at preview time. The downgrade view is
28540
+ // rendered from it at PREVIEW (renderPosturePlan); confirm reads it back ONLY to
28541
+ // compare against the live store and reject a stale plan (runConfirm's drift gate).
28542
+ current: external_exports.partialRecord(DetectionCategory, ActionTaken)
28543
+ });
28544
+
26978
28545
  // src/command-registry.ts
26979
- import { readdirSync as readdirSync4 } from "fs";
26980
- import { fileURLToPath } from "url";
26981
- var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
28546
+ import { readdirSync as readdirSync5 } from "fs";
28547
+ import { fileURLToPath as fileURLToPath2 } from "url";
28548
+ var COMMANDS_DIR = fileURLToPath2(new URL("../commands", import.meta.url));
26982
28549
 
26983
28550
  // src/present.ts
26984
28551
  var SHADE = {
@@ -27037,8 +28604,8 @@ function renderStatusBar(s, opts = {}) {
27037
28604
  const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
27038
28605
  const tally = `${paint.dim("unreviewed")} ${paint.critical(sq)}${String(u.critical)} ${paint.high(sq)}${String(u.high)} ${paint.medium(sq)}${String(u.medium)} ${paint.low(sq)}${String(u.low)}`;
27039
28606
  const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
27040
- const open = `${flag} ${String(s.openFindings)} open findings`;
27041
- return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
28607
+ const open2 = `${flag} ${String(s.openFindings)} open findings`;
28608
+ return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open2}`;
27042
28609
  }
27043
28610
  function renderStatusLine(summary) {
27044
28611
  return renderStatusBar(findingStatus(summary), { color: true });