@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.
package/scripts/query.js CHANGED
@@ -492,9 +492,8 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/persistence/src/database.ts
495
- import { randomUUID as randomUUID8 } from "crypto";
496
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
497
- import { join, sep } from "path";
495
+ import { randomUUID as randomUUID10 } from "crypto";
496
+ import { join as join2, sep } from "path";
498
497
  import { DatabaseSync } from "node:sqlite";
499
498
 
500
499
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -558,6 +557,30 @@ var SQLITE_MIGRATIONS = [
558
557
  {
559
558
  tag: "0014_drop_legacy_events_findings",
560
559
  sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
560
+ },
561
+ {
562
+ tag: "0015_busy_vengeance",
563
+ sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
564
+ },
565
+ {
566
+ tag: "0016_breezy_zodiak",
567
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
568
+ },
569
+ {
570
+ tag: "0017_rainy_kat_farrell",
571
+ sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
572
+ },
573
+ {
574
+ tag: "0018_serious_tana_nile",
575
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
576
+ },
577
+ {
578
+ tag: "0019_audit_started_at_index",
579
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
580
+ },
581
+ {
582
+ tag: "0020_secret_vault_pagination_indexes",
583
+ sql: "CREATE INDEX `idx_secret_vault_last_seen` ON `secret_vault` (`last_seen`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_reuse` ON `secret_vault` (`occurrence_count`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_at` ON `secret_vault_deref` (`at`,`id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_signal` ON `secret_vault_deref` (`at`,`id`) WHERE `reason` NOT IN ('display', 'view-render');"
561
584
  }
562
585
  ];
563
586
 
@@ -15355,7 +15378,17 @@ var Finding = external_exports.object({
15355
15378
  }).meta({ id: "Finding" });
15356
15379
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15357
15380
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15358
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15381
+ var FindingProvider = external_exports.enum([
15382
+ "claudecode",
15383
+ "claudedesktop",
15384
+ "cursor",
15385
+ "copilot",
15386
+ "chatgpt",
15387
+ "claudeai",
15388
+ "codex",
15389
+ "antigravity",
15390
+ "api"
15391
+ ]).meta({ id: "FindingProvider" });
15359
15392
  var FindingCategory = external_exports.enum([
15360
15393
  "secret",
15361
15394
  "pii",
@@ -15409,7 +15442,16 @@ var FindingInstance = external_exports.object({
15409
15442
  confidence: external_exports.number().min(0).max(1),
15410
15443
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15411
15444
  // that predate the resolution feature stay valid.
15412
- status: FindingStatus.optional()
15445
+ status: FindingStatus.optional(),
15446
+ // The audit event this finding was captured from. Optional so callers that
15447
+ // do not project it stay valid. An at-rest finding is content-addressed by
15448
+ // finding_key and its row is upserted on re-detection, so this names the
15449
+ // MOST RECENT detection event, not the first.
15450
+ eventId: external_exports.string().optional(),
15451
+ // The session that event belongs to, when it has one — the seam a
15452
+ // per-instance "view session" link needs. Absent for events captured
15453
+ // outside a session.
15454
+ sessionId: external_exports.string().optional()
15413
15455
  }).meta({ id: "FindingInstance" });
15414
15456
  var FindingGroup = external_exports.object({
15415
15457
  id: external_exports.string(),
@@ -15453,7 +15495,11 @@ var FindingFacets = external_exports.object({
15453
15495
  // for every instance, so every group lands in a bucket; a status-less
15454
15496
  // group (possible only for callers whose rows carry no statuses) is
15455
15497
  // counted under no value.
15456
- status: external_exports.array(FindingFacetItem)
15498
+ status: external_exports.array(FindingFacetItem),
15499
+ // Host tool (attributes.tool_name). Present only on the instance-level
15500
+ // reads, which can filter by it; the grouped read omits the dimension
15501
+ // because a group spans tools.
15502
+ tool: external_exports.array(FindingFacetItem).optional()
15457
15503
  }).meta({ id: "FindingFacets" });
15458
15504
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15459
15505
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15471,6 +15517,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15471
15517
  // Scope to findings whose event carries this session id (the Activity page's
15472
15518
  // session → findings drilldown). Findings without a session never match.
15473
15519
  sessionId: external_exports.string().optional(),
15520
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15521
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15522
+ // means all time — this list has no default window.
15523
+ from: external_exports.iso.datetime().optional(),
15524
+ // A group or instance id that must appear in the page even when the cursor
15525
+ // has already advanced past its sort position. This is what keeps the
15526
+ // Findings page's one-shot ?finding= deep link resolving once the list
15527
+ // paginates: the target group is appended out of sort order rather than
15528
+ // scanning forward for it. Never affects totals, facets or the cursor.
15529
+ includeId: external_exports.string().optional(),
15474
15530
  groupBy: external_exports.literal("type").optional(),
15475
15531
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15476
15532
  cursor: external_exports.string().optional()
@@ -15515,15 +15571,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15515
15571
  detection: FindingDetectionRef,
15516
15572
  policy: FindingPolicyRef
15517
15573
  }).meta({ id: "FindingInstanceDetail" });
15574
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15575
+ var ListFindingInstancesQuery = external_exports.object({
15576
+ severity: external_exports.array(Severity).optional(),
15577
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15578
+ subtype: external_exports.array(external_exports.string()).optional(),
15579
+ provider: external_exports.array(FindingProvider).optional(),
15580
+ action: external_exports.array(FindingAction).optional(),
15581
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15582
+ // the grouped query's group-level fold.
15583
+ status: external_exports.array(FindingStatus).optional(),
15584
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15585
+ // where the free-text `q` can only match the rendered "via Bash" label.
15586
+ tool: external_exports.array(external_exports.string()).optional(),
15587
+ // Exact repository / file-path matches, for the drill-down out of the
15588
+ // locations view. A row whose event carries no repo/file matches neither.
15589
+ repo: external_exports.string().optional(),
15590
+ file: external_exports.string().optional(),
15591
+ q: external_exports.string().optional(),
15592
+ sessionId: external_exports.string().optional(),
15593
+ from: external_exports.iso.datetime().optional(),
15594
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15595
+ cursor: external_exports.string().optional()
15596
+ });
15597
+ var ListFindingInstancesResponse = external_exports.object({
15598
+ // Instances matching the filters across the whole scope, not just this
15599
+ // page — cursor-independent, like the grouped list's totals.
15600
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15601
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15602
+ // dimension still excludes its own filter.
15603
+ facets: FindingFacets,
15604
+ items: external_exports.array(FindingInstanceDetail),
15605
+ nextCursor: external_exports.string().nullable()
15606
+ }).meta({ id: "ListFindingInstancesResponse" });
15607
+ var FindingLocationFile = external_exports.object({
15608
+ // Empty when the instances carried no file path (a prompt or a tool call
15609
+ // with no file attribution).
15610
+ file: external_exports.string(),
15611
+ instanceCount: external_exports.number().int().nonnegative(),
15612
+ maxSeverity: Severity,
15613
+ latestDetectedAt: external_exports.iso.datetime(),
15614
+ // Folded from the instances' derived statuses with the same
15615
+ // open-dominates precedence a group uses.
15616
+ status: FindingStatus.optional(),
15617
+ // Distinct rules seen at this location, capped — the row shows them as
15618
+ // chips, and the count is what conveys scale.
15619
+ ruleIds: external_exports.array(external_exports.string())
15620
+ }).meta({ id: "FindingLocationFile" });
15621
+ var FindingLocationRepo = external_exports.object({
15622
+ /** Empty when the instances carried no repo attribute. */
15623
+ repo: external_exports.string(),
15624
+ instanceCount: external_exports.number().int().nonnegative(),
15625
+ maxSeverity: Severity,
15626
+ latestDetectedAt: external_exports.iso.datetime(),
15627
+ status: FindingStatus.optional(),
15628
+ files: external_exports.array(FindingLocationFile)
15629
+ }).meta({ id: "FindingLocationRepo" });
15630
+ var ListFindingLocationsQuery = external_exports.object({
15631
+ severity: external_exports.array(Severity).optional(),
15632
+ subtype: external_exports.array(external_exports.string()).optional(),
15633
+ provider: external_exports.array(FindingProvider).optional(),
15634
+ action: external_exports.array(FindingAction).optional(),
15635
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15636
+ // instances that match, and folds its status from those.
15637
+ status: external_exports.array(FindingStatus).optional(),
15638
+ tool: external_exports.array(external_exports.string()).optional(),
15639
+ q: external_exports.string().optional(),
15640
+ sessionId: external_exports.string().optional(),
15641
+ from: external_exports.iso.datetime().optional(),
15642
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15643
+ });
15644
+ var ListFindingLocationsResponse = external_exports.object({
15645
+ totals: external_exports.object({
15646
+ findings: external_exports.number().int().nonnegative(),
15647
+ repos: external_exports.number().int().nonnegative(),
15648
+ files: external_exports.number().int().nonnegative()
15649
+ }),
15650
+ /** Sorted by max severity, then most recent. */
15651
+ items: external_exports.array(FindingLocationRepo),
15652
+ /** Whether `limit` truncated the repo list. */
15653
+ hasMore: external_exports.boolean()
15654
+ }).meta({ id: "ListFindingLocationsResponse" });
15518
15655
 
15519
15656
  // ../../packages/schema/src/zod/harness-map.ts
15520
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15657
+ var Harness = external_exports.enum([
15658
+ "claudecode",
15659
+ "cursor",
15660
+ "copilot",
15661
+ "codex",
15662
+ "antigravity",
15663
+ "windsurf",
15664
+ "claudedesktop",
15665
+ "chatgpt",
15666
+ "claudeai",
15667
+ "api"
15668
+ ]).meta({ id: "Harness" });
15521
15669
  var TOOL_TO_HARNESS = {
15522
15670
  "claude-code": "claudecode",
15523
15671
  "claude-desktop": "claudedesktop",
15524
15672
  "github-copilot": "copilot",
15525
15673
  cursor: "cursor",
15526
- chatgpt: "chatgpt"
15674
+ chatgpt: "chatgpt",
15675
+ codex: "codex",
15676
+ antigravity: "antigravity",
15677
+ "claude-ai": "claudeai"
15527
15678
  };
15528
15679
 
15529
15680
  // ../../packages/schema/src/zod/meta.ts
@@ -15981,7 +16132,18 @@ var ActivityOverviewResponse = external_exports.object({
15981
16132
  // ../../packages/schema/src/zod/event.ts
15982
16133
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15983
16134
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15984
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16135
+ var SourceTool = external_exports.enum([
16136
+ "claude-code",
16137
+ "claude-desktop",
16138
+ "cursor",
16139
+ "chatgpt",
16140
+ "claude-ai",
16141
+ "github-copilot",
16142
+ "codex",
16143
+ "antigravity",
16144
+ "cli",
16145
+ "unknown"
16146
+ ]).meta({ id: "SourceTool" });
15985
16147
  var EventMetadata = external_exports.object({
15986
16148
  sessionId: external_exports.string().optional(),
15987
16149
  repo: external_exports.string().optional(),
@@ -16052,7 +16214,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
16052
16214
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
16053
16215
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
16054
16216
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
16055
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16217
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
16056
16218
  var AccessCounts = external_exports.object({
16057
16219
  open: external_exports.number().int().nonnegative(),
16058
16220
  approved: external_exports.number().int().nonnegative(),
@@ -16274,6 +16436,7 @@ var ExceptionConditions = external_exports.object({
16274
16436
  sourceTool: external_exports.string().optional(),
16275
16437
  provider: external_exports.string().optional()
16276
16438
  }).strict();
16439
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16277
16440
  var DetectionException = external_exports.object({
16278
16441
  id: external_exports.guid(),
16279
16442
  ruleId: external_exports.string(),
@@ -16290,6 +16453,7 @@ var DetectionException = external_exports.object({
16290
16453
  keyVersion: external_exports.number().int().positive(),
16291
16454
  // maskMatch() preview of the approved value — never the raw value.
16292
16455
  maskedValue: external_exports.string(),
16456
+ capability: ExceptionCapability.default("suppress"),
16293
16457
  scope: ExceptionScope,
16294
16458
  expiresAt: external_exports.iso.datetime().nullable(),
16295
16459
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16313,11 +16477,13 @@ var ExceptionBundleEntry = DetectionException.pick({
16313
16477
  ruleId: true,
16314
16478
  valueFingerprint: true,
16315
16479
  keyVersion: true,
16480
+ capability: true,
16316
16481
  expiresAt: true,
16317
16482
  maxUses: true,
16318
16483
  useCount: true,
16319
16484
  conditions: true
16320
16485
  });
16486
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16321
16487
 
16322
16488
  // ../../packages/schema/src/zod/rule.ts
16323
16489
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17128,6 +17294,35 @@ var EgressWriteSummary = external_exports.object({
17128
17294
  droppedFiles: external_exports.array(external_exports.string()).default([])
17129
17295
  }).meta({ id: "EgressWriteSummary" });
17130
17296
 
17297
+ // ../../packages/schema/src/zod/exception-action.ts
17298
+ var confirmation = external_exports.string().optional();
17299
+ var ApproveBlockedInput = external_exports.object({
17300
+ reference: external_exports.string(),
17301
+ scope: external_exports.string(),
17302
+ reason: external_exports.string(),
17303
+ confirmation
17304
+ });
17305
+ var AddExceptionInput = external_exports.object({
17306
+ ruleId: external_exports.string(),
17307
+ value: external_exports.string(),
17308
+ scope: external_exports.string(),
17309
+ reason: external_exports.string(),
17310
+ confirmation
17311
+ });
17312
+ var GrantRevealInput = external_exports.object({
17313
+ pointer: external_exports.string(),
17314
+ scope: external_exports.string(),
17315
+ justification: external_exports.string(),
17316
+ confirmation
17317
+ });
17318
+ var RevokeExceptionInput = external_exports.object({
17319
+ id: external_exports.string(),
17320
+ reason: external_exports.string()
17321
+ });
17322
+ var RotateKeyInput = external_exports.object({
17323
+ confirmation: external_exports.string()
17324
+ });
17325
+
17131
17326
  // ../../packages/schema/src/zod/findings-group-build.ts
17132
17327
  function toApiAction(dbVal) {
17133
17328
  const map2 = {
@@ -17183,6 +17378,8 @@ function buildFindingGroups(rows, opts = {}) {
17183
17378
  repo: r.repo,
17184
17379
  file: r.file,
17185
17380
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17381
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17382
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17186
17383
  action: toApiAction(effectiveDbAction),
17187
17384
  detectedAt: r.occurredAt,
17188
17385
  confidence: r.confidence,
@@ -17314,14 +17511,17 @@ function applyFindingFilters(groups, opts) {
17314
17511
  }
17315
17512
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17316
17513
  var SEVERITY_RANK = SEVERITY_ORDER;
17514
+ function compareFindingGroupOrder(a, b) {
17515
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17516
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17517
+ const severityDiff = rankA - rankB;
17518
+ if (severityDiff !== 0) return severityDiff;
17519
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17520
+ if (recencyDiff !== 0) return recencyDiff;
17521
+ return a.id.localeCompare(b.id);
17522
+ }
17317
17523
  function sortFindingGroups(groups) {
17318
- return [...groups].sort((a, b) => {
17319
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17320
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17321
- const severityDiff = rankA - rankB;
17322
- if (severityDiff !== 0) return severityDiff;
17323
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17324
- });
17524
+ return [...groups].sort(compareFindingGroupOrder);
17325
17525
  }
17326
17526
  function computeFindingFacets(allGroups, opts) {
17327
17527
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17377,15 +17577,158 @@ function computeFindingFacets(allGroups, opts) {
17377
17577
  for (const g of forStatus) {
17378
17578
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17379
17579
  }
17380
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17580
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17581
+ return {
17582
+ severity: toItems2(severityMap),
17583
+ provider: toItems2(providerMap),
17584
+ action: toItems2(actionMap),
17585
+ subtype: toItems2(subtypeMap),
17586
+ status: toItems2(statusMap)
17587
+ };
17588
+ }
17589
+
17590
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17591
+ function rowHaystack(row) {
17592
+ return [
17593
+ row.ruleId,
17594
+ row.category,
17595
+ row.maskedMatch,
17596
+ row.repo,
17597
+ row.file,
17598
+ row.toolName ? `via ${row.toolName}` : "",
17599
+ row.id
17600
+ ].join(" ").toLowerCase();
17601
+ }
17602
+ function matchesDimension(row, opts, dimension) {
17603
+ switch (dimension) {
17604
+ case "severity":
17605
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17606
+ case "subtype":
17607
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17608
+ case "providers":
17609
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17610
+ case "actions":
17611
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17612
+ case "statuses":
17613
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17614
+ case "tools":
17615
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17616
+ case "repo":
17617
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17618
+ case "file":
17619
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17620
+ case "q":
17621
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17622
+ }
17623
+ }
17624
+ var DIMENSIONS = [
17625
+ "severity",
17626
+ "subtype",
17627
+ "providers",
17628
+ "actions",
17629
+ "statuses",
17630
+ "tools",
17631
+ "repo",
17632
+ "file",
17633
+ "q"
17634
+ ];
17635
+ function matchesInstanceFilters(row, opts, except) {
17636
+ for (const dimension of DIMENSIONS) {
17637
+ if (dimension === except) continue;
17638
+ if (!matchesDimension(row, opts, dimension)) return false;
17639
+ }
17640
+ return true;
17641
+ }
17642
+ function toItems(counts) {
17643
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17644
+ }
17645
+ function bump(counts, value) {
17646
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17647
+ }
17648
+ function createInstanceFacetAccumulator(opts) {
17649
+ const severity = /* @__PURE__ */ new Map();
17650
+ const subtype = /* @__PURE__ */ new Map();
17651
+ const provider = /* @__PURE__ */ new Map();
17652
+ const action = /* @__PURE__ */ new Map();
17653
+ const status = /* @__PURE__ */ new Map();
17654
+ const tool = /* @__PURE__ */ new Map();
17655
+ return {
17656
+ add(row) {
17657
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17658
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17659
+ if (matchesInstanceFilters(row, opts, "providers")) {
17660
+ bump(provider, toApiProvider(row.sourceTool));
17661
+ }
17662
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17663
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17664
+ bump(status, row.status);
17665
+ }
17666
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17667
+ bump(tool, row.toolName);
17668
+ }
17669
+ },
17670
+ facets: () => ({
17671
+ severity: toItems(severity),
17672
+ subtype: toItems(subtype),
17673
+ provider: toItems(provider),
17674
+ action: toItems(action),
17675
+ status: toItems(status),
17676
+ tool: toItems(tool)
17677
+ })
17678
+ };
17679
+ }
17680
+ function toInstanceDetail(row) {
17681
+ const category = toApiCategory(row.category);
17682
+ return {
17683
+ id: row.id,
17684
+ provider: toApiProvider(row.sourceTool),
17685
+ repo: row.repo,
17686
+ file: row.file,
17687
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17688
+ eventId: row.eventId,
17689
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17690
+ action: toApiAction(row.actionTaken),
17691
+ detectedAt: row.occurredAt,
17692
+ confidence: row.confidence,
17693
+ ...row.status === void 0 ? {} : { status: row.status },
17694
+ groupId: row.ruleId,
17695
+ category,
17696
+ subtype: row.ruleId,
17697
+ severity: row.severity,
17698
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17699
+ detection: { id: row.ruleId, name: null },
17700
+ policy: { id: `category:${category}`, name: category }
17701
+ };
17702
+ }
17703
+ var SEVERITY_ORDER2 = {
17704
+ critical: 0,
17705
+ high: 1,
17706
+ medium: 2,
17707
+ low: 3
17708
+ };
17709
+ function newLocationAccumulator() {
17381
17710
  return {
17382
- severity: toItems(severityMap),
17383
- provider: toItems(providerMap),
17384
- action: toItems(actionMap),
17385
- subtype: toItems(subtypeMap),
17386
- status: toItems(statusMap)
17711
+ instanceCount: 0,
17712
+ // Sorts after every known severity, so the first row always wins the
17713
+ // comparison below rather than an unknown value pinning the location.
17714
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17715
+ maxSeverity: "low",
17716
+ latestDetectedAt: "",
17717
+ statuses: [],
17718
+ ruleIds: /* @__PURE__ */ new Set()
17387
17719
  };
17388
17720
  }
17721
+ function addToLocation(acc, row) {
17722
+ acc.instanceCount += 1;
17723
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17724
+ if (rank < acc.maxSeverityRank) {
17725
+ acc.maxSeverityRank = rank;
17726
+ acc.maxSeverity = row.severity;
17727
+ }
17728
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17729
+ acc.statuses.push(row.status);
17730
+ acc.ruleIds.add(row.ruleId);
17731
+ }
17389
17732
 
17390
17733
  // ../../packages/schema/src/zod/installed-pack.ts
17391
17734
  var InstalledPack = external_exports.object({
@@ -17417,8 +17760,161 @@ var PatchInstalledPackRequest = external_exports.object({
17417
17760
  message: "At least one field must be provided"
17418
17761
  }).meta({ id: "PatchInstalledPackRequest" });
17419
17762
 
17763
+ // ../../packages/schema/src/zod/vault.ts
17764
+ var POINTER_FORMAT_VERSION = 2;
17765
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17766
+ var POINTER_TOKEN_PATTERN = new RegExp(
17767
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17768
+ );
17769
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17770
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17771
+ var ParsedPointer = external_exports.object({
17772
+ category: DetectionCategory,
17773
+ keyVersion: external_exports.number().int().positive(),
17774
+ pointerId: external_exports.string(),
17775
+ tag: external_exports.string()
17776
+ });
17777
+ var VaultEntry = external_exports.object({
17778
+ pointerId: external_exports.string(),
17779
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17780
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17781
+ // independently of the vault encryption key below.
17782
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17783
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17784
+ // The vault-key epoch this row's ciphertext was sealed under.
17785
+ keyVersion: external_exports.number().int().positive(),
17786
+ // Fixed at first mint and never updated: the same value detected later under a
17787
+ // different rule's category keeps the category it was minted with, so one
17788
+ // value always produces exactly one wire token.
17789
+ category: DetectionCategory,
17790
+ ruleId: external_exports.string(),
17791
+ // Partial-reveal preview for badges and listings. Never the raw value.
17792
+ maskedMatch: external_exports.string(),
17793
+ provider: external_exports.string().optional(),
17794
+ ciphertext: external_exports.string(),
17795
+ nonce: external_exports.string(),
17796
+ authTag: external_exports.string(),
17797
+ // How many times this value has been detected on this machine — the reuse
17798
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17799
+ occurrenceCount: external_exports.number().int().nonnegative(),
17800
+ firstSeen: external_exports.string(),
17801
+ lastSeen: external_exports.string()
17802
+ });
17803
+ var PointerDescriptor = external_exports.object({
17804
+ category: DetectionCategory,
17805
+ provider: external_exports.string().optional(),
17806
+ maskedMatch: external_exports.string(),
17807
+ occurrences: external_exports.number().int().nonnegative(),
17808
+ firstSeen: external_exports.string(),
17809
+ lastSeen: external_exports.string()
17810
+ });
17811
+ var PointerIdentity = external_exports.object({
17812
+ ruleId: external_exports.string(),
17813
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17814
+ fingerprintKeyVersion: external_exports.number().int().positive()
17815
+ });
17816
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17817
+ var VaultDerefReason = external_exports.enum([
17818
+ "display",
17819
+ "explicit-reveal",
17820
+ "view-render",
17821
+ "model-input",
17822
+ "remediation",
17823
+ "purge"
17824
+ ]);
17825
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17826
+ var VaultDeref = external_exports.object({
17827
+ id: external_exports.guid(),
17828
+ pointerId: external_exports.string(),
17829
+ at: external_exports.string(),
17830
+ target: DetokenizeTarget,
17831
+ reason: VaultDerefReason,
17832
+ outcome: VaultDerefOutcome,
17833
+ // Present only on a model-target crossing that a reveal grant authorized.
17834
+ grantId: external_exports.string().optional(),
17835
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17836
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17837
+ pointerCount: external_exports.number().int().positive().default(1)
17838
+ });
17839
+ var VaultSightingKind = external_exports.enum([
17840
+ "prompt",
17841
+ "tool-input",
17842
+ "tool-output",
17843
+ "file",
17844
+ "transcript"
17845
+ ]);
17846
+ var VaultSighting = external_exports.object({
17847
+ location: external_exports.string(),
17848
+ kind: VaultSightingKind,
17849
+ firstSeen: external_exports.string(),
17850
+ lastSeen: external_exports.string()
17851
+ });
17852
+ var VaultInventoryEntry = external_exports.object({
17853
+ pointerId: external_exports.string(),
17854
+ category: DetectionCategory,
17855
+ provider: external_exports.string().optional(),
17856
+ maskedMatch: external_exports.string(),
17857
+ occurrences: external_exports.number().int().nonnegative(),
17858
+ firstSeen: external_exports.string(),
17859
+ lastSeen: external_exports.string(),
17860
+ // The active reveal-to-model grant covering this value, when one exists —
17861
+ // the inventory badges it, the row links to revocation.
17862
+ revealGrantId: external_exports.string().nullable(),
17863
+ sightings: external_exports.array(VaultSighting)
17864
+ });
17865
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17866
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17867
+ var MAX_VAULT_PAGE_LIMIT = 200;
17868
+ var ListVaultInventoryQuery = external_exports.object({
17869
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17870
+ // Opaque; names the last row of the page just served.
17871
+ cursor: external_exports.string().optional()
17872
+ });
17873
+ var ListVaultInventoryResponse = external_exports.object({
17874
+ // Vaulted values across the whole store, not just this page — cursor-
17875
+ // independent, so paging never changes what the count claims.
17876
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17877
+ items: external_exports.array(VaultInventoryEntry),
17878
+ // `null` once the last page is reached.
17879
+ nextCursor: external_exports.string().nullable()
17880
+ });
17881
+ var ListVaultReuseQuery = external_exports.object({
17882
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17883
+ cursor: external_exports.string().optional()
17884
+ });
17885
+ var ListVaultReuseResponse = external_exports.object({
17886
+ // Reused values across the whole store — the number the section's claim
17887
+ // ("values detected in more than one place") is about.
17888
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17889
+ items: external_exports.array(VaultInventoryEntry),
17890
+ nextCursor: external_exports.string().nullable()
17891
+ });
17892
+ var ListVaultDerefsQuery = external_exports.object({
17893
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17894
+ // hides them and counts them into `hiddenBatched` instead, so the model
17895
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17896
+ // over a Server Action, which preserves the type, never as a URL param.
17897
+ includeBatched: external_exports.boolean().optional(),
17898
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17899
+ cursor: external_exports.string().optional()
17900
+ });
17901
+ var ListVaultDerefsResponse = external_exports.object({
17902
+ items: external_exports.array(VaultDeref),
17903
+ nextCursor: external_exports.string().nullable(),
17904
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17905
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17906
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17907
+ hiddenBatched: external_exports.number().int().nonnegative()
17908
+ });
17909
+ var VaultKeyCustody = external_exports.string();
17910
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17911
+ var VaultConsent = external_exports.object({
17912
+ acknowledgedAt: external_exports.iso.datetime(),
17913
+ version: external_exports.number().int().positive()
17914
+ });
17915
+
17420
17916
  // ../../packages/schema/src/zod/local.ts
17421
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17917
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17422
17918
  var RunMode = external_exports.enum(["standalone"]);
17423
17919
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17424
17920
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17440,6 +17936,16 @@ var WorkspaceSettings = external_exports.object({
17440
17936
  // In-place egress extraction on the scan paths; disable to stop all Data
17441
17937
  // Shares writes.
17442
17938
  dataSharesInPlace: external_exports.boolean().default(true),
17939
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17940
+ // vault, instead of destroying them. Absent by default: this is a custody
17941
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17942
+ // Revoking stops future vaulting; it does not erase what is already stored —
17943
+ // purging the vault is the eraser.
17944
+ vaultConsent: VaultConsent.optional(),
17945
+ // Where the vault master key lives.
17946
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17947
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17948
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17443
17949
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17444
17950
  onboardedAt: external_exports.iso.datetime().optional(),
17445
17951
  // Records that the user consented to sending findings to the model API for
@@ -17772,7 +18278,7 @@ var TopSourcesQuery = external_exports.object({
17772
18278
  // Omit for both kinds.
17773
18279
  kind: external_exports.enum(SOURCE_KINDS).optional()
17774
18280
  });
17775
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18281
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17776
18282
  var ScanCoverageProvider = external_exports.object({
17777
18283
  provider: Provider,
17778
18284
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -18025,6 +18531,138 @@ function captureId(sessionId, contentHash, filePath = null) {
18025
18531
  );
18026
18532
  }
18027
18533
 
18534
+ // ../../packages/persistence/src/internal/snapshot.ts
18535
+ import { randomUUID } from "crypto";
18536
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18537
+ import { basename, dirname, join } from "path";
18538
+
18539
+ // ../../packages/persistence/src/paths.ts
18540
+ import {
18541
+ chmodSync,
18542
+ linkSync,
18543
+ lstatSync,
18544
+ mkdirSync,
18545
+ renameSync,
18546
+ rmSync,
18547
+ writeFileSync
18548
+ } from "fs";
18549
+ import { threadId } from "worker_threads";
18550
+ var DATA_DIR_MODE = 448;
18551
+ var DATA_FILE_MODE = 384;
18552
+ var DB_FILENAME = "aka.db";
18553
+ function isSymlink(path) {
18554
+ try {
18555
+ return lstatSync(path).isSymbolicLink();
18556
+ } catch {
18557
+ return false;
18558
+ }
18559
+ }
18560
+ function chmodBestEffort(path, mode) {
18561
+ if (isSymlink(path)) return;
18562
+ try {
18563
+ chmodSync(path, mode);
18564
+ } catch {
18565
+ }
18566
+ }
18567
+ function tightenDir(dir) {
18568
+ chmodBestEffort(dir, DATA_DIR_MODE);
18569
+ }
18570
+ function ensureDataDirSync(dir) {
18571
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18572
+ tightenDir(dir);
18573
+ }
18574
+ function dbSidecars(file2) {
18575
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18576
+ }
18577
+ function tightenFile(file2) {
18578
+ chmodBestEffort(file2, DATA_FILE_MODE);
18579
+ }
18580
+ function tightenPerms(file2) {
18581
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18582
+ }
18583
+
18584
+ // ../../packages/persistence/src/internal/snapshot.ts
18585
+ function backupPath(file2, tag) {
18586
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18587
+ }
18588
+ var STALE_PARTIAL_MS = 5 * 6e4;
18589
+ function reapStalePartials(file2) {
18590
+ const dir = dirname(file2);
18591
+ const prefix = `${basename(file2)}.`;
18592
+ let entries;
18593
+ try {
18594
+ entries = readdirSync(dir);
18595
+ } catch {
18596
+ return;
18597
+ }
18598
+ for (const name of entries) {
18599
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18600
+ const partial2 = join(dir, name);
18601
+ try {
18602
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18603
+ rmSync2(partial2, { force: true });
18604
+ }
18605
+ } catch {
18606
+ }
18607
+ }
18608
+ }
18609
+ function snapshotStore(db, backup) {
18610
+ const partial2 = `${backup}.partial`;
18611
+ try {
18612
+ rmSync2(partial2, { force: true });
18613
+ db.prepare("VACUUM INTO ?").run(partial2);
18614
+ tightenFile(partial2);
18615
+ renameSync2(partial2, backup);
18616
+ } catch (error51) {
18617
+ try {
18618
+ rmSync2(partial2, { force: true });
18619
+ } catch {
18620
+ }
18621
+ throw error51;
18622
+ }
18623
+ }
18624
+ function moveStoreAside(file2, backup) {
18625
+ const undo = [];
18626
+ renameSync2(file2, backup);
18627
+ undo.push([backup, file2]);
18628
+ try {
18629
+ for (const sidecar of dbSidecars(file2)) {
18630
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18631
+ try {
18632
+ renameSync2(sidecar, moved);
18633
+ undo.push([moved, sidecar]);
18634
+ } catch {
18635
+ rmSync2(sidecar, { force: true });
18636
+ }
18637
+ }
18638
+ } catch (error51) {
18639
+ for (const [from, to] of undo.reverse()) {
18640
+ try {
18641
+ renameSync2(from, to);
18642
+ } catch {
18643
+ }
18644
+ }
18645
+ throw error51;
18646
+ }
18647
+ tightenPerms(backup);
18648
+ }
18649
+ function discardStore(file2, backup) {
18650
+ try {
18651
+ rmSync2(file2, { force: true });
18652
+ for (const sidecar of dbSidecars(file2)) {
18653
+ rmSync2(sidecar, { force: true });
18654
+ }
18655
+ } catch (error51) {
18656
+ if (existsSync(file2)) {
18657
+ try {
18658
+ rmSync2(backup, { force: true });
18659
+ } catch {
18660
+ }
18661
+ }
18662
+ throw error51;
18663
+ }
18664
+ }
18665
+
18028
18666
  // ../../packages/persistence/src/internal/sql-text.ts
18029
18667
  function escapeLikePattern(s) {
18030
18668
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18166,38 +18804,6 @@ function mapRowsTolerant(rows, map2) {
18166
18804
  return out;
18167
18805
  }
18168
18806
 
18169
- // ../../packages/persistence/src/paths.ts
18170
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18171
- var DATA_DIR_MODE = 448;
18172
- var DATA_FILE_MODE = 384;
18173
- var DB_FILENAME = "aka.db";
18174
- function chmodBestEffort(path, mode) {
18175
- try {
18176
- chmodSync(path, mode);
18177
- } catch {
18178
- }
18179
- }
18180
- function tightenDir(dir) {
18181
- chmodBestEffort(dir, DATA_DIR_MODE);
18182
- }
18183
- function ensureDataDirSync(dir) {
18184
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18185
- tightenDir(dir);
18186
- }
18187
- function dbSidecars(file2) {
18188
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18189
- }
18190
- function tightenFile(file2) {
18191
- try {
18192
- if (lstatSync(file2).isSymbolicLink()) return;
18193
- } catch {
18194
- }
18195
- chmodBestEffort(file2, DATA_FILE_MODE);
18196
- }
18197
- function tightenPerms(file2) {
18198
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18199
- }
18200
-
18201
18807
  // ../../packages/persistence/src/migrations.ts
18202
18808
  function describeObject(object2) {
18203
18809
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -18313,9 +18919,9 @@ function applyLegacyDropMigration(db, file2) {
18313
18919
  }
18314
18920
  }
18315
18921
  function backupBeforeLegacyDrop(db, file2) {
18316
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18317
- db.prepare("VACUUM INTO ?").run(backup);
18318
- tightenFile(backup);
18922
+ reapStalePartials(file2);
18923
+ const backup = backupPath(file2, "pre-drop");
18924
+ snapshotStore(db, backup);
18319
18925
  return backup;
18320
18926
  }
18321
18927
  var TOKEN_USAGE_COLUMNS = [
@@ -18659,6 +19265,25 @@ function parseJsonObject(s) {
18659
19265
  return void 0;
18660
19266
  }
18661
19267
 
19268
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19269
+ function encodeKeysetCursor(payload) {
19270
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19271
+ }
19272
+ function decodeKeysetCursor(cursor) {
19273
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19274
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19275
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19276
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19277
+ // a null cursor, which a caller reads as "end of list". That is the one
19278
+ // outcome a cursor that does not decode must never produce, since the
19279
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19280
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19281
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19282
+ return parsed;
19283
+ }
19284
+ return null;
19285
+ }
19286
+
18662
19287
  // ../../packages/persistence/src/repositories/activity.ts
18663
19288
  var DAY_MS = 864e5;
18664
19289
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18704,16 +19329,6 @@ function utcWindow(nowMs) {
18704
19329
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18705
19330
  return { startMs, endMs: startMs + DAY_MS };
18706
19331
  }
18707
- function encodeCursor(payload) {
18708
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18709
- }
18710
- function decodeCursor(cursor) {
18711
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18712
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18713
- return parsed;
18714
- }
18715
- return null;
18716
- }
18717
19332
  var DB_EVENT_TYPE_TO_KIND = {
18718
19333
  session: "session",
18719
19334
  prompt: "prompt",
@@ -18858,7 +19473,7 @@ var SqliteActivityRepository = class {
18858
19473
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18859
19474
  }
18860
19475
  listSessions(query) {
18861
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19476
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18862
19477
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18863
19478
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18864
19479
  const conditions = [SESSION_ROOT];
@@ -18932,7 +19547,7 @@ var SqliteActivityRepository = class {
18932
19547
  )
18933
19548
  );
18934
19549
  const last = page[page.length - 1];
18935
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19550
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18936
19551
  return Promise.resolve({ items, nextCursor, emptyCount });
18937
19552
  }
18938
19553
  getSession(sessionId) {
@@ -19805,7 +20420,7 @@ var SqliteEventsRepository = class {
19805
20420
  };
19806
20421
 
19807
20422
  // ../../packages/persistence/src/repositories/exceptions.ts
19808
- import { randomUUID } from "crypto";
20423
+ import { randomUUID as randomUUID2 } from "crypto";
19809
20424
 
19810
20425
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19811
20426
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19837,9 +20452,13 @@ var AmbiguousExceptionIdError = class extends Error {
19837
20452
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19838
20453
  AND (expires_at IS NULL OR expires_at > :now)
19839
20454
  AND (max_uses IS NULL OR use_count < max_uses)`;
20455
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
20456
+ AND conditions IS NULL
20457
+ AND ${ACTIVE_PREDICATE}`;
19840
20458
  var SqliteExceptionsRepository = class {
19841
- constructor(db) {
20459
+ constructor(db, now = () => Date.now()) {
19842
20460
  this.db = db;
20461
+ this.now = now;
19843
20462
  this.consumeStmt = db.prepare(
19844
20463
  `UPDATE exceptions
19845
20464
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19857,6 +20476,7 @@ var SqliteExceptionsRepository = class {
19857
20476
  );
19858
20477
  }
19859
20478
  db;
20479
+ now;
19860
20480
  consumeStmt;
19861
20481
  insertBlockedStmt;
19862
20482
  sweepBlockedStmt;
@@ -19883,8 +20503,8 @@ var SqliteExceptionsRepository = class {
19883
20503
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19884
20504
  );
19885
20505
  }
19886
- const id = randomUUID();
19887
- const now = Date.now();
20506
+ const id = randomUUID2();
20507
+ const now = this.now();
19888
20508
  try {
19889
20509
  this.insertExceptionRow(id, input, now);
19890
20510
  } catch (err) {
@@ -19928,11 +20548,11 @@ var SqliteExceptionsRepository = class {
19928
20548
  this.db.prepare(
19929
20549
  `INSERT INTO exceptions (
19930
20550
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19931
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19932
- conditions, created_by, created_via, created_at, updated_at
20551
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20552
+ justification, conditions, created_by, created_via, created_at, updated_at
19933
20553
  ) VALUES (
19934
20554
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19935
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20555
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19936
20556
  :conditions, :createdBy, :createdVia, :now, :now
19937
20557
  )`
19938
20558
  ).run({
@@ -19942,6 +20562,7 @@ var SqliteExceptionsRepository = class {
19942
20562
  valueFingerprint: input.valueFingerprint,
19943
20563
  keyVersion: input.keyVersion,
19944
20564
  maskedValue: input.maskedValue,
20565
+ capability: input.capability ?? "suppress",
19945
20566
  scope: input.scope,
19946
20567
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19947
20568
  maxUses: input.maxUses,
@@ -19961,7 +20582,7 @@ var SqliteExceptionsRepository = class {
19961
20582
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19962
20583
  const rows = allRows(
19963
20584
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19964
- opts?.includeTerminal ? {} : { now: Date.now() }
20585
+ opts?.includeTerminal ? {} : { now: this.now() }
19965
20586
  );
19966
20587
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19967
20588
  return Promise.resolve(exceptions);
@@ -19996,7 +20617,7 @@ var SqliteExceptionsRepository = class {
19996
20617
  * already revoked.
19997
20618
  */
19998
20619
  revoke(id, revokedBy, reason) {
19999
- const now = Date.now();
20620
+ const now = this.now();
20000
20621
  const result = this.db.prepare(
20001
20622
  `UPDATE exceptions
20002
20623
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -20010,7 +20631,7 @@ var SqliteExceptionsRepository = class {
20010
20631
  * callers must treat identically — means it does not and the detection is
20011
20632
  * enforced as usual. Deliberately NOT wrapped in try/catch.
20012
20633
  */
20013
- consume(id, now = Date.now()) {
20634
+ consume(id, now = this.now()) {
20014
20635
  const result = this.consumeStmt.run({ id, now });
20015
20636
  return Promise.resolve(Number(result.changes) === 1);
20016
20637
  }
@@ -20019,7 +20640,7 @@ var SqliteExceptionsRepository = class {
20019
20640
  * version — what rides the policy bundle to the hook. Grants written under
20020
20641
  * a different (rotated-away) key never match, so they are excluded at read.
20021
20642
  */
20022
- activeBundleEntries(keyVersion, now = Date.now()) {
20643
+ activeBundleEntries(keyVersion, now = this.now()) {
20023
20644
  const rows = allRows(
20024
20645
  this.db.prepare(
20025
20646
  `SELECT * FROM exceptions
@@ -20035,6 +20656,7 @@ var SqliteExceptionsRepository = class {
20035
20656
  ruleId: row.rule_id,
20036
20657
  valueFingerprint: row.value_fingerprint,
20037
20658
  keyVersion: row.key_version,
20659
+ capability: row.capability,
20038
20660
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20039
20661
  maxUses: row.max_uses,
20040
20662
  useCount: row.use_count,
@@ -20050,7 +20672,7 @@ var SqliteExceptionsRepository = class {
20050
20672
  * than the retention window on every write, so the ledger self-limits.
20051
20673
  */
20052
20674
  recordBlocked(entry) {
20053
- const now = Date.now();
20675
+ const now = this.now();
20054
20676
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
20055
20677
  this.insertBlockedStmt.run({
20056
20678
  reference: entry.reference,
@@ -20073,7 +20695,7 @@ var SqliteExceptionsRepository = class {
20073
20695
  WHERE blocked_at > :cutoff
20074
20696
  ORDER BY blocked_at DESC, rowid DESC`
20075
20697
  ),
20076
- { cutoff: Date.now() - windowMs }
20698
+ { cutoff: this.now() - windowMs }
20077
20699
  );
20078
20700
  return Promise.resolve(
20079
20701
  rows.map((row) => ({
@@ -20089,6 +20711,36 @@ var SqliteExceptionsRepository = class {
20089
20711
  }))
20090
20712
  );
20091
20713
  }
20714
+ /**
20715
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20716
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20717
+ * suppression uses — plus the capability: a suppression grant must never
20718
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20719
+ * revealed value re-enters the detection scan immediately afterward and the
20720
+ * suppression match there claims the use — one crossing, one use.
20721
+ *
20722
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20723
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20724
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20725
+ */
20726
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20727
+ try {
20728
+ const at = now ?? this.now();
20729
+ const row = getRow(
20730
+ this.db.prepare(
20731
+ `SELECT id FROM exceptions
20732
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20733
+ AND key_version = :keyVersion
20734
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20735
+ LIMIT 1`
20736
+ ),
20737
+ { ruleId, valueFingerprint, keyVersion, now: at }
20738
+ );
20739
+ return Promise.resolve(row ?? null);
20740
+ } catch (err) {
20741
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20742
+ }
20743
+ }
20092
20744
  /**
20093
20745
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20094
20746
  * exhausted) whose last transition is older than the retention window.
@@ -20096,7 +20748,7 @@ var SqliteExceptionsRepository = class {
20096
20748
  * predicate, so correctness never depends on this sweep; it only bounds how
20097
20749
  * long the audit evidence is kept locally. Returns the deleted count.
20098
20750
  */
20099
- sweepTerminal(retentionMs, now = Date.now()) {
20751
+ sweepTerminal(retentionMs, now = this.now()) {
20100
20752
  const result = this.db.prepare(
20101
20753
  `DELETE FROM exceptions
20102
20754
  WHERE updated_at < :cutoff
@@ -20116,6 +20768,7 @@ function parseExceptionRow(row) {
20116
20768
  valueFingerprint: row.value_fingerprint,
20117
20769
  keyVersion: row.key_version,
20118
20770
  maskedValue: row.masked_value,
20771
+ capability: row.capability,
20119
20772
  scope: row.scope,
20120
20773
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20121
20774
  maxUses: row.max_uses,
@@ -20158,6 +20811,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20158
20811
 
20159
20812
  // ../../packages/persistence/src/repositories/findings.ts
20160
20813
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20814
+ var SCAN_BATCH_ROWS = 1e3;
20815
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20816
+ var LOCATION_RULE_IDS_CAP = 20;
20817
+ function compareLocationOrder(a, b) {
20818
+ return compareFindingGroupOrder(
20819
+ {
20820
+ severity: a.maxSeverity,
20821
+ latestDetectedAt: a.latestDetectedAt,
20822
+ id: ""
20823
+ },
20824
+ {
20825
+ severity: b.maxSeverity,
20826
+ latestDetectedAt: b.latestDetectedAt,
20827
+ id: ""
20828
+ }
20829
+ );
20830
+ }
20161
20831
  var CONCAT_SEP = ",";
20162
20832
  var TUPLE_SEP = "|";
20163
20833
  function splitConcat(value) {
@@ -20170,6 +20840,33 @@ function deriveInstanceStatus(row) {
20170
20840
  latestResolutionStatus: row.latest_status
20171
20841
  });
20172
20842
  }
20843
+ function encodeGroupCursor(group) {
20844
+ const payload = {
20845
+ sev: group.severity,
20846
+ t: group.latestDetectedAt,
20847
+ id: group.id
20848
+ };
20849
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20850
+ }
20851
+ function decodeGroupCursor(cursor) {
20852
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20853
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20854
+ return {
20855
+ severity: parsed.sev,
20856
+ latestDetectedAt: parsed.t,
20857
+ id: parsed.id
20858
+ };
20859
+ }
20860
+ return null;
20861
+ }
20862
+ function firstAfter(sorted, cursor) {
20863
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20864
+ return index === -1 ? sorted.length : index;
20865
+ }
20866
+ function findDeepLinked(sorted, page, id) {
20867
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20868
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20869
+ }
20173
20870
  var DAY_MS3 = 864e5;
20174
20871
  var SqliteFindingsRepository = class {
20175
20872
  constructor(db) {
@@ -20279,8 +20976,13 @@ var SqliteFindingsRepository = class {
20279
20976
  */
20280
20977
  listGroupedFindings(query) {
20281
20978
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20282
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20283
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20979
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20980
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20981
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20982
+ const sessionParams = {
20983
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20984
+ ...fromMs === void 0 ? {} : { fromMs }
20985
+ };
20284
20986
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20285
20987
  predicate,
20286
20988
  params: sessionParams
@@ -20288,7 +20990,8 @@ var SqliteFindingsRepository = class {
20288
20990
  const rows = allRows(
20289
20991
  this.db.prepare(
20290
20992
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20291
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
20993
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
20994
+ kind, finding_key, latest_status
20292
20995
  FROM (
20293
20996
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20294
20997
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20298,6 +21001,7 @@ var SqliteFindingsRepository = class {
20298
21001
  json_extract(e.attributes, '$.repo') AS repo,
20299
21002
  json_extract(e.attributes, '$.file_path') AS file,
20300
21003
  json_extract(e.attributes, '$.tool_name') AS tool_name,
21004
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20301
21005
  e.event_type AS kind, f.finding_key AS finding_key,
20302
21006
  latest.status AS latest_status,
20303
21007
  ROW_NUMBER() OVER (
@@ -20329,6 +21033,8 @@ var SqliteFindingsRepository = class {
20329
21033
  repo: r.repo ?? "",
20330
21034
  file: r.file ?? "",
20331
21035
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
21036
+ eventId: r.event_id,
21037
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20332
21038
  status: deriveInstanceStatus(r)
20333
21039
  }));
20334
21040
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20352,18 +21058,23 @@ var SqliteFindingsRepository = class {
20352
21058
  groups: sorted.length
20353
21059
  };
20354
21060
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21061
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21062
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21063
+ const page = sorted.slice(start, start + limit);
21064
+ const lastOnPage = page.at(-1);
21065
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21066
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20355
21067
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20356
- const items = sorted.slice(0, limit).map(
20357
- (g) => statusSet ? {
20358
- ...g,
20359
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20360
- } : g
20361
- );
21068
+ const narrow = (g) => statusSet ? {
21069
+ ...g,
21070
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21071
+ } : g;
21072
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20362
21073
  return Promise.resolve({
20363
21074
  totals,
20364
21075
  facets,
20365
21076
  items,
20366
- nextCursor: null,
21077
+ nextCursor,
20367
21078
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20368
21079
  });
20369
21080
  }
@@ -20395,6 +21106,266 @@ var SqliteFindingsRepository = class {
20395
21106
  * request actually carries a `q`. (Substring matching is unaffected by a
20396
21107
  * path repeating across tuples.)
20397
21108
  */
21109
+ /**
21110
+ * The instance-level (flat) findings list: one row per finding, newest first,
21111
+ * paged by keyset.
21112
+ *
21113
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21114
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21115
+ * them changes no reported number. Severity, subtype, provider, action,
21116
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21117
+ * facet excludes its own filter, so a row the filter rejects still has to be
21118
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21119
+ * Several could not be expressed there anyway: status comes from the one
21120
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21121
+ * none of the mappers names", which no IN-list can say.
21122
+ *
21123
+ * The scan runs from the top of the scope on every request, not from the
21124
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21125
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21126
+ * while the counting runs, and only the page itself is retained.
21127
+ */
21128
+ listFindingInstances(query) {
21129
+ const opts = {
21130
+ severity: query.severity,
21131
+ subtype: query.subtype,
21132
+ providers: query.provider,
21133
+ actions: query.action,
21134
+ statuses: query.status,
21135
+ tools: query.tool,
21136
+ repo: query.repo,
21137
+ file: query.file,
21138
+ q: query.q
21139
+ };
21140
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21141
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21142
+ const accumulator = createInstanceFacetAccumulator(opts);
21143
+ const items = [];
21144
+ let total = 0;
21145
+ let last;
21146
+ let hasMore = false;
21147
+ for (const row of this.scanFindingRows({
21148
+ sessionId: query.sessionId,
21149
+ from: query.from
21150
+ })) {
21151
+ accumulator.add(row);
21152
+ if (!matchesInstanceFilters(row, opts)) continue;
21153
+ total += 1;
21154
+ if (items.length < limit) {
21155
+ items.push(toInstanceDetail(row));
21156
+ last = row;
21157
+ } else {
21158
+ hasMore = true;
21159
+ }
21160
+ }
21161
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21162
+ if (cursor !== null) {
21163
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21164
+ return Promise.resolve({
21165
+ totals: { findings: total },
21166
+ facets: accumulator.facets(),
21167
+ items: resumed.items,
21168
+ nextCursor: resumed.nextCursor
21169
+ });
21170
+ }
21171
+ return Promise.resolve({
21172
+ totals: { findings: total },
21173
+ facets: accumulator.facets(),
21174
+ items,
21175
+ nextCursor
21176
+ });
21177
+ }
21178
+ /**
21179
+ * The page of matching rows strictly after `cursor`. Separate from the
21180
+ * counting pass because that one starts at the top of the scope by design;
21181
+ * this one narrows the scan with the same keyset predicate the activity list
21182
+ * uses, so a later page costs less than the first rather than more.
21183
+ */
21184
+ pageAfter(cursor, opts, limit, query) {
21185
+ const items = [];
21186
+ let last;
21187
+ let hasMore = false;
21188
+ for (const row of this.scanFindingRows({
21189
+ sessionId: query.sessionId,
21190
+ from: query.from,
21191
+ after: cursor
21192
+ })) {
21193
+ if (!matchesInstanceFilters(row, opts)) continue;
21194
+ if (items.length < limit) {
21195
+ items.push(toInstanceDetail(row));
21196
+ last = row;
21197
+ } else {
21198
+ hasMore = true;
21199
+ break;
21200
+ }
21201
+ }
21202
+ return {
21203
+ items,
21204
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21205
+ };
21206
+ }
21207
+ /**
21208
+ * The same findings folded by location: repository, then file within it.
21209
+ *
21210
+ * The grouping keys come from the capturing event's attributes, which is what
21211
+ * the local store relates a finding to — there is no finding↔asset row to
21212
+ * group by instead. A repo or file the event did not record folds into the
21213
+ * empty-string bucket, which the view renders but does not link, since no
21214
+ * filter can name it.
21215
+ */
21216
+ listFindingLocations(query) {
21217
+ const opts = {
21218
+ severity: query.severity,
21219
+ subtype: query.subtype,
21220
+ providers: query.provider,
21221
+ actions: query.action,
21222
+ statuses: query.status,
21223
+ tools: query.tool,
21224
+ q: query.q
21225
+ };
21226
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21227
+ const byRepo = /* @__PURE__ */ new Map();
21228
+ let total = 0;
21229
+ for (const row of this.scanFindingRows({
21230
+ sessionId: query.sessionId,
21231
+ from: query.from
21232
+ })) {
21233
+ if (!matchesInstanceFilters(row, opts)) continue;
21234
+ total += 1;
21235
+ let files = byRepo.get(row.repo);
21236
+ if (files === void 0) {
21237
+ files = /* @__PURE__ */ new Map();
21238
+ byRepo.set(row.repo, files);
21239
+ }
21240
+ let acc = files.get(row.file);
21241
+ if (acc === void 0) {
21242
+ acc = newLocationAccumulator();
21243
+ files.set(row.file, acc);
21244
+ }
21245
+ addToLocation(acc, row);
21246
+ }
21247
+ let fileCount = 0;
21248
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21249
+ fileCount += files.size;
21250
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21251
+ file: file2,
21252
+ instanceCount: acc.instanceCount,
21253
+ maxSeverity: acc.maxSeverity,
21254
+ latestDetectedAt: acc.latestDetectedAt,
21255
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21256
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21257
+ })).sort(compareLocationOrder);
21258
+ const rollup = fileRows.reduce(
21259
+ (a, f) => ({
21260
+ instanceCount: a.instanceCount + f.instanceCount,
21261
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21262
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21263
+ }),
21264
+ {
21265
+ instanceCount: 0,
21266
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21267
+ latestDetectedAt: ""
21268
+ }
21269
+ );
21270
+ const statuses = fileRows.map((f) => f.status);
21271
+ const folded = foldGroupStatus(statuses);
21272
+ return {
21273
+ repo,
21274
+ instanceCount: rollup.instanceCount,
21275
+ maxSeverity: rollup.maxSeverity,
21276
+ latestDetectedAt: rollup.latestDetectedAt,
21277
+ ...folded === void 0 ? {} : { status: folded },
21278
+ files: fileRows
21279
+ };
21280
+ });
21281
+ repos.sort(compareLocationOrder);
21282
+ return Promise.resolve({
21283
+ totals: { findings: total, repos: repos.length, files: fileCount },
21284
+ items: repos.slice(0, limit),
21285
+ hasMore: repos.length > limit
21286
+ });
21287
+ }
21288
+ /**
21289
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21290
+ *
21291
+ * A generator so a caller streams the scope without it ever being an array:
21292
+ * the flat list counts and facets the whole filtered scope, which on a large
21293
+ * store is far more rows than any page. Each batch advances the same keyset
21294
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21295
+ * rather than one unbounded result set.
21296
+ *
21297
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21298
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21299
+ * makes it a point lookup per row, and the derived table would re-materialize
21300
+ * a window over the whole resolution table once per batch.
21301
+ *
21302
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21303
+ * would be missing from its own facet, which is computed by excluding that
21304
+ * dimension — see listFindingInstances.
21305
+ */
21306
+ *scanFindingRows(scope) {
21307
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21308
+ const params = [];
21309
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21310
+ conditions.push("e.root_session_id = ?");
21311
+ params.push(scope.sessionId);
21312
+ }
21313
+ if (scope.from !== void 0) {
21314
+ conditions.push("e.started_at >= ?");
21315
+ params.push(isoToEpochMillis(scope.from));
21316
+ }
21317
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21318
+ d.severity AS severity, f.masked_match AS masked_match,
21319
+ f.action_taken AS action_taken, f.confidence AS confidence,
21320
+ e.started_at AS occurred_at,
21321
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21322
+ json_extract(e.attributes, '$.repo') AS repo,
21323
+ json_extract(e.attributes, '$.file_path') AS file,
21324
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21325
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21326
+ e.event_type AS kind, f.finding_key AS finding_key,
21327
+ ${latestResolutionStatusSql("f")} AS latest_status
21328
+ FROM inspection_findings f
21329
+ JOIN audit_events e ON e.id = f.audit_event_id
21330
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21331
+ WHERE ${conditions.join(" AND ")}
21332
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21333
+ ORDER BY e.started_at DESC, f.id DESC
21334
+ LIMIT ?`;
21335
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21336
+ for (; ; ) {
21337
+ const rows = allRows(this.db.prepare(sql), [
21338
+ ...params,
21339
+ after.startedAtMs,
21340
+ after.startedAtMs,
21341
+ after.id,
21342
+ SCAN_BATCH_ROWS
21343
+ ]);
21344
+ for (const r of rows) {
21345
+ yield {
21346
+ id: r.id,
21347
+ ruleId: r.rule_id,
21348
+ category: r.category,
21349
+ severity: r.severity,
21350
+ maskedMatch: r.masked_match,
21351
+ actionTaken: r.action_taken,
21352
+ confidence: r.confidence,
21353
+ occurredAt: epochMillisToIso(r.occurred_at),
21354
+ sourceTool: r.source_tool,
21355
+ repo: r.repo ?? "",
21356
+ file: r.file ?? "",
21357
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21358
+ eventId: r.event_id,
21359
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21360
+ status: deriveInstanceStatus(r)
21361
+ };
21362
+ }
21363
+ if (rows.length < SCAN_BATCH_ROWS) return;
21364
+ const lastRow = rows[rows.length - 1];
21365
+ if (lastRow === void 0) return;
21366
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21367
+ }
21368
+ }
20398
21369
  groupAggregates(withSearchText, scope) {
20399
21370
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20400
21371
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20655,7 +21626,7 @@ var SqliteInspectionFindingsRepository = class {
20655
21626
  };
20656
21627
 
20657
21628
  // ../../packages/persistence/src/repositories/installed-packs.ts
20658
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21629
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20659
21630
 
20660
21631
  // ../../packages/persistence/src/semver.ts
20661
21632
  function parse3(version2) {
@@ -20806,7 +21777,7 @@ var SqliteInstalledPacksRepository = class {
20806
21777
  let behind = false;
20807
21778
  for (const row of rows) {
20808
21779
  const params = {
20809
- id: randomUUID2(),
21780
+ id: randomUUID3(),
20810
21781
  namespace: row.namespace,
20811
21782
  packId: row.packId,
20812
21783
  version: row.version,
@@ -20818,7 +21789,7 @@ var SqliteInstalledPacksRepository = class {
20818
21789
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20819
21790
  this.upsertAvailableStmt.run({
20820
21791
  ...params,
20821
- id: randomUUID2(),
21792
+ id: randomUUID3(),
20822
21793
  recordedBy: meta3?.recordedBy ?? null
20823
21794
  });
20824
21795
  } else {
@@ -21141,14 +22112,15 @@ var SqliteInventoryRepository = class {
21141
22112
  };
21142
22113
 
21143
22114
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21144
- import { randomUUID as randomUUID3 } from "crypto";
22115
+ import { randomUUID as randomUUID4 } from "crypto";
21145
22116
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21146
22117
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21147
22118
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21148
22119
  var HARNESS_LABELS = {
21149
22120
  claudecode: "Claude Code",
21150
22121
  cursor: "Cursor",
21151
- codex: "Codex"
22122
+ codex: "Codex",
22123
+ antigravity: "Antigravity"
21152
22124
  };
21153
22125
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21154
22126
  var EMPTY_PROJECT_AGG = {
@@ -21163,6 +22135,7 @@ function resolveHarnessId(attrs, row) {
21163
22135
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21164
22136
  if (t.includes("cursor")) return "cursor";
21165
22137
  if (t.includes("codex")) return "codex";
22138
+ if (t.includes("antigravity")) return "antigravity";
21166
22139
  return null;
21167
22140
  }
21168
22141
  function isLiveRealClaudeCode(rows) {
@@ -21621,7 +22594,7 @@ var SqliteInventoryAssetsRepository = class {
21621
22594
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21622
22595
  VALUES (:id, :projectId, :path, :access, :now, :now)
21623
22596
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21624
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22597
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21625
22598
  }
21626
22599
  return true;
21627
22600
  }
@@ -21642,7 +22615,7 @@ var SqliteInventoryAssetsRepository = class {
21642
22615
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21643
22616
  VALUES (:id, :assetId, :trust, :now, :now)
21644
22617
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21645
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22618
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21646
22619
  }
21647
22620
  this.configRowsCache = void 0;
21648
22621
  return "ok";
@@ -21939,7 +22912,7 @@ var SqliteInventoryAssetsRepository = class {
21939
22912
  };
21940
22913
 
21941
22914
  // ../../packages/persistence/src/repositories/policies.ts
21942
- import { randomUUID as randomUUID4 } from "crypto";
22915
+ import { randomUUID as randomUUID5 } from "crypto";
21943
22916
  var SqlitePoliciesRepository = class {
21944
22917
  constructor(db) {
21945
22918
  this.db = db;
@@ -21974,7 +22947,7 @@ var SqlitePoliciesRepository = class {
21974
22947
  failOpenTransaction(this.db, () => {
21975
22948
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
21976
22949
  stmt.run({
21977
- id: randomUUID4(),
22950
+ id: randomUUID5(),
21978
22951
  target: JSON.stringify({ category }),
21979
22952
  action,
21980
22953
  now: Date.now()
@@ -21994,7 +22967,7 @@ var SqlitePoliciesRepository = class {
21994
22967
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21995
22968
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
21996
22969
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21997
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22970
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
21998
22971
  }
21999
22972
  // Caps every global per-category policy currently set to block/redact down
22000
22973
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22062,7 +23035,7 @@ var SqlitePolicyCatalogRepository = class {
22062
23035
  };
22063
23036
 
22064
23037
  // ../../packages/persistence/src/repositories/project-files.ts
22065
- import { randomUUID as randomUUID5 } from "crypto";
23038
+ import { randomUUID as randomUUID6 } from "crypto";
22066
23039
  var SqliteProjectFilesRepository = class {
22067
23040
  constructor(db) {
22068
23041
  this.db = db;
@@ -22094,7 +23067,7 @@ var SqliteProjectFilesRepository = class {
22094
23067
  const stamp = Math.max(now, maxStamp + 1);
22095
23068
  for (const file2 of scan2.files) {
22096
23069
  this.upsertStmt.run({
22097
- id: randomUUID5(),
23070
+ id: randomUUID6(),
22098
23071
  projectId,
22099
23072
  path: file2.path,
22100
23073
  name: file2.name,
@@ -22108,7 +23081,7 @@ var SqliteProjectFilesRepository = class {
22108
23081
  };
22109
23082
 
22110
23083
  // ../../packages/persistence/src/repositories/resolutions.ts
22111
- import { randomUUID as randomUUID6 } from "crypto";
23084
+ import { randomUUID as randomUUID7 } from "crypto";
22112
23085
  var SqliteResolutionsRepository = class {
22113
23086
  constructor(db, now = () => Date.now()) {
22114
23087
  this.db = db;
@@ -22162,7 +23135,7 @@ var SqliteResolutionsRepository = class {
22162
23135
  */
22163
23136
  insertResolution(r) {
22164
23137
  this.insertStmt.run({
22165
- id: randomUUID6(),
23138
+ id: randomUUID7(),
22166
23139
  findingKey: r.findingKey,
22167
23140
  status: FindingStatus.parse(r.status),
22168
23141
  method: ResolutionMethod.parse(r.method),
@@ -22221,13 +23194,51 @@ var SqliteRuleProbeCacheRepository = class {
22221
23194
  this.readStmt = db.prepare(
22222
23195
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22223
23196
  );
23197
+ this.countQuarantinedStmt = db.prepare(
23198
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23199
+ );
23200
+ this.clearQuarantinedStmt = db.prepare(
23201
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23202
+ );
22224
23203
  }
22225
23204
  db;
22226
23205
  upsertStmt;
22227
23206
  readStmt;
23207
+ countQuarantinedStmt;
23208
+ clearQuarantinedStmt;
22228
23209
  getVerdict(ruleKey) {
22229
23210
  return getRow(this.readStmt, { ruleKey });
22230
23211
  }
23212
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23213
+ countQuarantined() {
23214
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23215
+ }
23216
+ /**
23217
+ * Forgets every quarantine verdict, so the rules behind them are measured
23218
+ * again on the next load. This is the undo for a verdict the machine reached
23219
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23220
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23221
+ * loaded or slow machine can reach about a rule that is in fact fine.
23222
+ *
23223
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23224
+ * keeping, and dropping it would make every rule pay the battery again.
23225
+ *
23226
+ * Reports `refused` from the write's own result rather than inferring it from
23227
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23228
+ * swallows a contended DELETE (another writer holding the lock past
23229
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23230
+ * leaves the count unchanged, which is indistinguishable from "there was
23231
+ * nothing to clear". An undo that reports success while the quarantines are
23232
+ * still in place is worse than one that fails, because the rules it claimed
23233
+ * to restore are silently still disabled.
23234
+ */
23235
+ clearQuarantined() {
23236
+ const before = this.countQuarantined();
23237
+ const committed = failOpenTransaction(this.db, () => {
23238
+ this.clearQuarantinedStmt.run();
23239
+ });
23240
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23241
+ }
22231
23242
  setVerdict(ruleKey, verdict, worstProbeMs) {
22232
23243
  failOpenTransaction(this.db, () => {
22233
23244
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -22281,6 +23292,419 @@ var SqliteScanLedgerRepository = class {
22281
23292
  }
22282
23293
  };
22283
23294
 
23295
+ // ../../packages/persistence/src/repositories/secret-vault.ts
23296
+ import { randomUUID as randomUUID8 } from "crypto";
23297
+ function pageLimit(requested, fallback) {
23298
+ if (requested === void 0) return fallback;
23299
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
23300
+ }
23301
+ function encodeReuseCursor(payload) {
23302
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
23303
+ }
23304
+ function decodeReuseCursor(cursor) {
23305
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23306
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23307
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23308
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23309
+ // malformed cursor must never produce, since restarting from the top is the
23310
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23311
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23312
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23313
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23314
+ }
23315
+ return null;
23316
+ }
23317
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23318
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23319
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23320
+ v.occurrence_count, v.first_seen, v.last_seen`;
23321
+ function toSighting(row) {
23322
+ return {
23323
+ location: row.location,
23324
+ kind: row.kind,
23325
+ firstSeen: new Date(row.first_seen).toISOString(),
23326
+ lastSeen: new Date(row.last_seen).toISOString()
23327
+ };
23328
+ }
23329
+ var SELECT_COLUMNS = `
23330
+ pointer_id AS pointerId,
23331
+ value_fingerprint AS valueFingerprint,
23332
+ fingerprint_key_version AS fingerprintKeyVersion,
23333
+ key_version AS keyVersion,
23334
+ format_version AS formatVersion,
23335
+ category,
23336
+ rule_id AS ruleId,
23337
+ masked_match AS maskedMatch,
23338
+ provider,
23339
+ ciphertext,
23340
+ nonce,
23341
+ auth_tag AS authTag,
23342
+ occurrence_count AS occurrenceCount,
23343
+ first_seen AS firstSeen,
23344
+ last_seen AS lastSeen`;
23345
+ function toRow(raw) {
23346
+ const { provider, ...rest } = raw;
23347
+ return provider === null ? rest : { ...rest, provider };
23348
+ }
23349
+ var SqliteSecretVaultRepository = class {
23350
+ constructor(db) {
23351
+ this.db = db;
23352
+ this.insertStmt = db.prepare(
23353
+ `INSERT INTO secret_vault (
23354
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
23355
+ format_version, category, rule_id, masked_match, provider,
23356
+ ciphertext, nonce, auth_tag,
23357
+ occurrence_count, first_seen, last_seen
23358
+ ) VALUES (
23359
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
23360
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
23361
+ :ciphertext, :nonce, :authTag,
23362
+ 1, :now, :now
23363
+ )`
23364
+ );
23365
+ this.bumpStmt = db.prepare(
23366
+ `UPDATE secret_vault
23367
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
23368
+ WHERE value_fingerprint = :valueFingerprint`
23369
+ );
23370
+ this.byPointerStmt = db.prepare(
23371
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
23372
+ );
23373
+ this.byFingerprintStmt = db.prepare(
23374
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
23375
+ );
23376
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
23377
+ this.replaceCiphertextStmt = db.prepare(
23378
+ `UPDATE secret_vault
23379
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
23380
+ WHERE pointer_id = :pointerId`
23381
+ );
23382
+ this.refreshFingerprintStmt = db.prepare(
23383
+ `UPDATE secret_vault
23384
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
23385
+ WHERE pointer_id = :pointerId`
23386
+ );
23387
+ this.derefStmt = db.prepare(
23388
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
23389
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
23390
+ );
23391
+ }
23392
+ db;
23393
+ insertStmt;
23394
+ bumpStmt;
23395
+ byPointerStmt;
23396
+ byFingerprintStmt;
23397
+ listStmt;
23398
+ replaceCiphertextStmt;
23399
+ refreshFingerprintStmt;
23400
+ derefStmt;
23401
+ /**
23402
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
23403
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
23404
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
23405
+ * pointer, category and ciphertext, so the same secret always resolves to one
23406
+ * wire token. `minted` is true only when this call created the row.
23407
+ *
23408
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
23409
+ * writers cannot both decide they are minting.
23410
+ */
23411
+ upsert(input, now) {
23412
+ let minted = false;
23413
+ withTransaction(
23414
+ this.db,
23415
+ () => {
23416
+ const existing = getRow(this.byFingerprintStmt, {
23417
+ valueFingerprint: input.valueFingerprint
23418
+ });
23419
+ if (existing === void 0) {
23420
+ this.insertStmt.run(
23421
+ bindParams({
23422
+ pointerId: input.pointerId,
23423
+ valueFingerprint: input.valueFingerprint,
23424
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
23425
+ keyVersion: input.keyVersion,
23426
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
23427
+ category: input.category,
23428
+ ruleId: input.ruleId,
23429
+ maskedMatch: input.maskedMatch,
23430
+ provider: input.provider,
23431
+ ciphertext: input.ciphertext,
23432
+ nonce: input.nonce,
23433
+ authTag: input.authTag,
23434
+ now
23435
+ })
23436
+ );
23437
+ minted = true;
23438
+ return;
23439
+ }
23440
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
23441
+ },
23442
+ "IMMEDIATE"
23443
+ );
23444
+ const row = getRow(this.byFingerprintStmt, {
23445
+ valueFingerprint: input.valueFingerprint
23446
+ });
23447
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
23448
+ return { row: toRow(row), minted };
23449
+ }
23450
+ byPointerId(pointerId) {
23451
+ const raw = getRow(this.byPointerStmt, { pointerId });
23452
+ return raw === void 0 ? null : toRow(raw);
23453
+ }
23454
+ byValueFingerprint(fingerprint) {
23455
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
23456
+ return raw === void 0 ? null : toRow(raw);
23457
+ }
23458
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
23459
+ recordDeref(entry) {
23460
+ this.derefStmt.run(
23461
+ bindParams({
23462
+ id: entry.id,
23463
+ pointerId: entry.pointerId,
23464
+ at: entry.at,
23465
+ target: entry.target,
23466
+ reason: entry.reason,
23467
+ outcome: entry.outcome,
23468
+ grantId: entry.grantId,
23469
+ pointerCount: entry.pointerCount ?? 1
23470
+ })
23471
+ );
23472
+ }
23473
+ listAll() {
23474
+ return allRows(this.listStmt).map(toRow);
23475
+ }
23476
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
23477
+ replaceCiphertext(pointerId, next) {
23478
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
23479
+ }
23480
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
23481
+ refreshFingerprint(pointerId, next) {
23482
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
23483
+ }
23484
+ /**
23485
+ * Destroy every vaulted value and report how many were destroyed. The deref
23486
+ * audit is left alone on purpose — see the table note above.
23487
+ */
23488
+ purgeAll() {
23489
+ let destroyed = 0;
23490
+ withTransaction(
23491
+ this.db,
23492
+ () => {
23493
+ destroyed = this.countEntries();
23494
+ this.db.exec("DELETE FROM secret_vault");
23495
+ },
23496
+ "IMMEDIATE"
23497
+ );
23498
+ return destroyed;
23499
+ }
23500
+ /**
23501
+ * Record (or re-stamp) one place a pointer has been written. One row per
23502
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
23503
+ * on hook paths — a failure must never affect the rewrite that triggered it,
23504
+ * so callers wrap this, not the other way around.
23505
+ */
23506
+ recordSighting(entry, now) {
23507
+ this.db.prepare(
23508
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
23509
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
23510
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
23511
+ ).run({
23512
+ id: randomUUID8(),
23513
+ pointerId: entry.pointerId,
23514
+ location: entry.location,
23515
+ kind: entry.kind,
23516
+ now
23517
+ });
23518
+ }
23519
+ /**
23520
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23521
+ * than one query per row. A pointer with no sightings still gets an entry, so
23522
+ * the caller never has to distinguish "none" from "missing".
23523
+ *
23524
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23525
+ * the instance the way the fixed-shape ones in the constructor are.
23526
+ */
23527
+ sightingsFor(pointerIds) {
23528
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23529
+ if (pointerIds.length === 0) return byPointer;
23530
+ const rows = allRows(
23531
+ this.db.prepare(
23532
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23533
+ FROM secret_vault_sighting
23534
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23535
+ ORDER BY last_seen DESC`
23536
+ ),
23537
+ pointerIds
23538
+ );
23539
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23540
+ return byPointer;
23541
+ }
23542
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23543
+ toInventoryEntries(rows) {
23544
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
23545
+ return rows.map((r) => ({
23546
+ pointerId: r.pointer_id,
23547
+ category: r.category,
23548
+ ...r.provider === null ? {} : { provider: r.provider },
23549
+ maskedMatch: r.masked_match,
23550
+ occurrences: r.occurrence_count,
23551
+ firstSeen: new Date(r.first_seen).toISOString(),
23552
+ lastSeen: new Date(r.last_seen).toISOString(),
23553
+ revealGrantId: r.grant_id,
23554
+ sightings: sightings.get(r.pointer_id) ?? []
23555
+ }));
23556
+ }
23557
+ /**
23558
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23559
+ * value's descriptor data joined with its sightings and the active
23560
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23561
+ * the fingerprint nor the ciphertext columns are selected.
23562
+ *
23563
+ * `totals.values` counts the whole store, not the page, so the count a reader
23564
+ * sees never depends on how far they have paged.
23565
+ */
23566
+ listInventory(query = {}, now = Date.now()) {
23567
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23568
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23569
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
23570
+ const rows = allRows(
23571
+ this.db.prepare(
23572
+ `SELECT ${INVENTORY_COLUMNS},
23573
+ (SELECT e.id FROM exceptions e
23574
+ WHERE e.rule_id = v.rule_id
23575
+ AND e.value_fingerprint = v.value_fingerprint
23576
+ AND e.key_version = v.fingerprint_key_version
23577
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23578
+ LIMIT 1) AS grant_id
23579
+ FROM secret_vault v
23580
+ ${where}
23581
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23582
+ LIMIT :limit`
23583
+ ),
23584
+ bindParams({
23585
+ now,
23586
+ limit: limit + 1,
23587
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23588
+ })
23589
+ );
23590
+ const hasMore = rows.length > limit;
23591
+ const page = hasMore ? rows.slice(0, limit) : rows;
23592
+ const last = page[page.length - 1];
23593
+ return {
23594
+ totals: { values: this.countEntries() },
23595
+ items: this.toInventoryEntries(page),
23596
+ // Minted from the last row of the PAGE, never the extra probe row.
23597
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23598
+ };
23599
+ }
23600
+ /**
23601
+ * Values reused on this machine — detected more than once, or written to more
23602
+ * than one location — most-reused first, one page at a time.
23603
+ *
23604
+ * Its own read rather than a filter over an inventory page: reuse is a
23605
+ * property of the whole store, and deriving it from 50 newest rows would
23606
+ * under-report exactly the values a reader most needs to see.
23607
+ */
23608
+ listReuse(query = {}, now = Date.now()) {
23609
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23610
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23611
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23612
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23613
+ const rows = allRows(
23614
+ this.db.prepare(
23615
+ `SELECT ${INVENTORY_COLUMNS},
23616
+ (SELECT e.id FROM exceptions e
23617
+ WHERE e.rule_id = v.rule_id
23618
+ AND e.value_fingerprint = v.value_fingerprint
23619
+ AND e.key_version = v.fingerprint_key_version
23620
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23621
+ LIMIT 1) AS grant_id
23622
+ FROM secret_vault v
23623
+ WHERE ${REUSED_PREDICATE} ${after}
23624
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23625
+ LIMIT :limit`
23626
+ ),
23627
+ bindParams({
23628
+ now,
23629
+ limit: limit + 1,
23630
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23631
+ })
23632
+ );
23633
+ const hasMore = rows.length > limit;
23634
+ const page = hasMore ? rows.slice(0, limit) : rows;
23635
+ const last = page[page.length - 1];
23636
+ return {
23637
+ totals: { reused: this.countReused() },
23638
+ items: this.toInventoryEntries(page),
23639
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23640
+ };
23641
+ }
23642
+ /**
23643
+ * The de-reference trail, newest first, one page at a time. By default the
23644
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23645
+ * instead — the rows that matter as a signal are the model crossings, and
23646
+ * burying them under render noise would defeat the audit's purpose.
23647
+ *
23648
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23649
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23650
+ * the reader pages.
23651
+ */
23652
+ listDerefs(query = {}) {
23653
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23654
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23655
+ const conditions = [];
23656
+ if (query.includeBatched !== true) {
23657
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23658
+ }
23659
+ if (cursor !== null) {
23660
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23661
+ }
23662
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
23663
+ const rows = allRows(
23664
+ this.db.prepare(
23665
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
23666
+ FROM secret_vault_deref ${where}
23667
+ ORDER BY at DESC, id DESC LIMIT :limit`
23668
+ ),
23669
+ bindParams({
23670
+ limit: limit + 1,
23671
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23672
+ })
23673
+ );
23674
+ const hasMore = rows.length > limit;
23675
+ const page = hasMore ? rows.slice(0, limit) : rows;
23676
+ const last = page[page.length - 1];
23677
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
23678
+ this.db,
23679
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
23680
+ );
23681
+ return {
23682
+ items: page.map((r) => ({
23683
+ id: r.id,
23684
+ pointerId: r.pointer_id,
23685
+ at: new Date(r.at).toISOString(),
23686
+ target: r.target,
23687
+ reason: r.reason,
23688
+ outcome: r.outcome,
23689
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
23690
+ pointerCount: r.pointer_count
23691
+ })),
23692
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
23693
+ hiddenBatched
23694
+ };
23695
+ }
23696
+ countEntries() {
23697
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
23698
+ }
23699
+ /** Values reused on this machine — the reuse list's page-independent total. */
23700
+ countReused() {
23701
+ return countScalar(
23702
+ this.db,
23703
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23704
+ );
23705
+ }
23706
+ };
23707
+
22284
23708
  // ../../packages/persistence/src/repositories/security.ts
22285
23709
  var DAY_MS4 = 864e5;
22286
23710
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22293,7 +23717,9 @@ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22293
23717
  var SCAN_COVERAGE = [
22294
23718
  { provider: "claudecode", coverage: 100, supported: true },
22295
23719
  { provider: "cursor", coverage: 0, supported: false },
22296
- { provider: "codex", coverage: 0, supported: false },
23720
+ { provider: "codex", coverage: 80, supported: true },
23721
+ { provider: "antigravity", coverage: 60, supported: true },
23722
+ { provider: "claudeai", coverage: 0, supported: false },
22297
23723
  { provider: "chatgpt", coverage: 0, supported: false },
22298
23724
  { provider: "copilot", coverage: 0, supported: false },
22299
23725
  { provider: "api", coverage: 0, supported: false }
@@ -22626,7 +24052,7 @@ var SqliteSecurityRepository = class {
22626
24052
  };
22627
24053
 
22628
24054
  // ../../packages/persistence/src/repositories/shares.ts
22629
- import { randomUUID as randomUUID7 } from "crypto";
24055
+ import { randomUUID as randomUUID9 } from "crypto";
22630
24056
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22631
24057
  var IN_CHUNK = 500;
22632
24058
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22882,7 +24308,7 @@ var SqliteSharesRepository = class {
22882
24308
  (id, destination_id, host, decision, created_at, updated_at)
22883
24309
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22884
24310
  ).run({
22885
- id: randomUUID7(),
24311
+ id: randomUUID9(),
22886
24312
  destinationId,
22887
24313
  host: dest.host,
22888
24314
  decision,
@@ -23031,7 +24457,7 @@ var SqliteSharesRepository = class {
23031
24457
  let destinationId = destIds.get(hit.host);
23032
24458
  if (destinationId === void 0) {
23033
24459
  destStmt.run({
23034
- id: randomUUID7(),
24460
+ id: randomUUID9(),
23035
24461
  kind: hit.kind,
23036
24462
  name: hit.name,
23037
24463
  host: hit.host,
@@ -23047,7 +24473,7 @@ var SqliteSharesRepository = class {
23047
24473
  let endpointId = endpointIds.get(endpointKey);
23048
24474
  if (endpointId === void 0) {
23049
24475
  endpointStmt.run({
23050
- id: randomUUID7(),
24476
+ id: randomUUID9(),
23051
24477
  destinationId,
23052
24478
  method: hit.method,
23053
24479
  transport: hit.transport,
@@ -23060,7 +24486,7 @@ var SqliteSharesRepository = class {
23060
24486
  endpointIds.set(endpointKey, endpointId);
23061
24487
  }
23062
24488
  siteStmt.run({
23063
- id: randomUUID7(),
24489
+ id: randomUUID9(),
23064
24490
  endpointId,
23065
24491
  project: input.project,
23066
24492
  projectKey: input.projectKey,
@@ -23425,6 +24851,9 @@ function purgeSampleData(db) {
23425
24851
  }
23426
24852
 
23427
24853
  // ../../packages/persistence/src/database.ts
24854
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24855
+ "aka.persistence.unsafeTestOnlyRawHandle"
24856
+ );
23428
24857
  function linkHost(input, hostId) {
23429
24858
  return hostId ? { ...input, hostId } : input;
23430
24859
  }
@@ -23446,21 +24875,34 @@ function openWithPragmas(file2) {
23446
24875
  }
23447
24876
  return db;
23448
24877
  }
23449
- function backupLegacyStore(file2) {
23450
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23451
- renameSync2(file2, backup);
23452
- tightenFile(backup);
23453
- for (const sidecar of dbSidecars(file2)) {
23454
- if (existsSync(sidecar)) rmSync2(sidecar);
24878
+ function backupLegacyStore(db, file2) {
24879
+ reapStalePartials(file2);
24880
+ const backup = backupPath(file2, "legacy");
24881
+ let snapshotted = false;
24882
+ let snapshotError;
24883
+ try {
24884
+ snapshotStore(db, backup);
24885
+ snapshotted = true;
24886
+ } catch (error51) {
24887
+ snapshotError = error51;
24888
+ } finally {
24889
+ db.close();
23455
24890
  }
24891
+ if (!snapshotted) {
24892
+ akaWarn(
24893
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24894
+ );
24895
+ moveStoreAside(file2, backup);
24896
+ return backup;
24897
+ }
24898
+ discardStore(file2, backup);
23456
24899
  return backup;
23457
24900
  }
23458
24901
  function openAndInitialize(file2) {
23459
24902
  let db = openWithPragmas(file2);
23460
24903
  try {
23461
24904
  if (isForeignSqliteLineage(db)) {
23462
- db.close();
23463
- const backup = backupLegacyStore(file2);
24905
+ const backup = backupLegacyStore(db, file2);
23464
24906
  db = openWithPragmas(file2);
23465
24907
  akaWarn(
23466
24908
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23476,6 +24918,7 @@ function openAndInitialize(file2) {
23476
24918
  policies,
23477
24919
  installedPacks,
23478
24920
  scanLedger: new SqliteScanLedgerRepository(db),
24921
+ secretVault: new SqliteSecretVaultRepository(db),
23479
24922
  exceptions: new SqliteExceptionsRepository(db),
23480
24923
  resolutions: new SqliteResolutionsRepository(db),
23481
24924
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23503,7 +24946,7 @@ function openAndInitialize(file2) {
23503
24946
  }
23504
24947
  function openLocalDatabase(dir) {
23505
24948
  ensureDataDirSync(dir);
23506
- const file2 = join(dir, DB_FILENAME);
24949
+ const file2 = join2(dir, DB_FILENAME);
23507
24950
  const {
23508
24951
  db,
23509
24952
  events,
@@ -23511,6 +24954,7 @@ function openLocalDatabase(dir) {
23511
24954
  policies,
23512
24955
  installedPacks,
23513
24956
  scanLedger,
24957
+ secretVault,
23514
24958
  exceptions,
23515
24959
  resolutions,
23516
24960
  ruleProbeCache,
@@ -23619,7 +25063,7 @@ function openLocalDatabase(dir) {
23619
25063
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23620
25064
  if (!definitionId) continue;
23621
25065
  inspectionFindings.insertFinding({
23622
- id: randomUUID8(),
25066
+ id: randomUUID10(),
23623
25067
  auditEventId: record2.scanEvent.id,
23624
25068
  inspectionDefinitionId: definitionId,
23625
25069
  span: finding.span,
@@ -23696,6 +25140,7 @@ function openLocalDatabase(dir) {
23696
25140
  policies,
23697
25141
  installedPacks,
23698
25142
  scanLedger,
25143
+ secretVault,
23699
25144
  exceptions,
23700
25145
  resolutions,
23701
25146
  ruleProbeCache,
@@ -23724,22 +25169,38 @@ function openLocalDatabase(dir) {
23724
25169
  transaction,
23725
25170
  close: () => {
23726
25171
  db.close();
23727
- }
25172
+ },
25173
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25174
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
23728
25175
  };
23729
25176
  }
23730
25177
 
25178
+ // ../../packages/persistence/src/file-lock.ts
25179
+ import { randomUUID as randomUUID11 } from "crypto";
25180
+ import {
25181
+ closeSync,
25182
+ existsSync as existsSync2,
25183
+ openSync,
25184
+ readFileSync,
25185
+ rmSync as rmSync3,
25186
+ statSync as statSync2,
25187
+ writeFileSync as writeFileSync2
25188
+ } from "fs";
25189
+ import { hostname as hostname3 } from "os";
25190
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25191
+
23731
25192
  // ../../packages/persistence/src/finding-key.ts
23732
25193
  import { createHash as createHash3 } from "crypto";
23733
25194
 
23734
25195
  // ../../packages/persistence/src/fingerprint.ts
23735
25196
  import { createHmac, randomBytes } from "crypto";
23736
- import { existsSync as existsSync2, readFileSync } from "fs";
23737
- import { join as join2 } from "path";
25197
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25198
+ import { join as join3 } from "path";
23738
25199
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23739
- var KEY_FILENAME = "exception.key";
25200
+ var EXCEPTION_KEY_FILENAME = "exception.key";
23740
25201
  var KEY_MATERIAL_BYTES = 32;
23741
25202
  function keyFilePath(dataDir2) {
23742
- return join2(dataDir2, KEY_FILENAME);
25203
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
23743
25204
  }
23744
25205
  function parseKeyFile(raw) {
23745
25206
  const parsed = JSON.parse(raw);
@@ -23762,7 +25223,7 @@ function parseKeyFile(raw) {
23762
25223
  function readFingerprintKey(dataDir2) {
23763
25224
  let raw;
23764
25225
  try {
23765
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25226
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
23766
25227
  } catch (err) {
23767
25228
  if (err.code === "ENOENT") return null;
23768
25229
  throw err instanceof Error ? err : new Error(String(err));
@@ -23774,18 +25235,18 @@ function readFingerprintKey(dataDir2) {
23774
25235
  import { renameSync as renameSync3 } from "fs";
23775
25236
  import { mkdir } from "fs/promises";
23776
25237
  import { homedir } from "os";
23777
- import { join as join3 } from "path";
25238
+ import { join as join4 } from "path";
23778
25239
  function defaultDataDir() {
23779
- return join3(homedir(), ".aka");
25240
+ return join4(homedir(), ".aka");
23780
25241
  }
23781
25242
  function settingsDir(base = defaultDataDir()) {
23782
- return join3(base, "settings");
25243
+ return join4(base, "settings");
23783
25244
  }
23784
25245
  function dataDir(base = defaultDataDir()) {
23785
- return join3(base, "data");
25246
+ return join4(base, "data");
23786
25247
  }
23787
25248
  function dbPath(base = defaultDataDir()) {
23788
- return join3(dataDir(base), "aka.db");
25249
+ return join4(dataDir(base), "aka.db");
23789
25250
  }
23790
25251
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23791
25252
  ensureDataDirSync(dir);
@@ -23798,8 +25259,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23798
25259
  for (const { name, dest } of moves) {
23799
25260
  try {
23800
25261
  ensureDataDirSync(dest);
23801
- const moved = join3(dest, name);
23802
- renameSync3(join3(base, name), moved);
25262
+ const moved = join4(dest, name);
25263
+ renameSync3(join4(base, name), moved);
23803
25264
  tightenFile(moved);
23804
25265
  } catch {
23805
25266
  }
@@ -23807,10 +25268,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23807
25268
  }
23808
25269
 
23809
25270
  // ../../packages/persistence/src/settings.ts
23810
- import { readFileSync as readFileSync2 } from "fs";
23811
- import { join as join4 } from "path";
25271
+ import { readFileSync as readFileSync3 } from "fs";
25272
+ import { join as join5 } from "path";
25273
+ var SETTINGS_FILENAME = "settings.json";
23812
25274
  function readWorkspaceSettings(base = defaultDataDir()) {
23813
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25275
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
23814
25276
  if (!record2) return defaultWorkspaceSettings();
23815
25277
  try {
23816
25278
  return WorkspaceSettings.parse(record2);
@@ -23821,30 +25283,56 @@ function readWorkspaceSettings(base = defaultDataDir()) {
23821
25283
  function readJson(file2) {
23822
25284
  let text;
23823
25285
  try {
23824
- text = readFileSync2(file2, "utf8");
25286
+ text = readFileSync3(file2, "utf8");
23825
25287
  } catch {
23826
25288
  return null;
23827
25289
  }
23828
25290
  return parseJsonObject(text) ?? null;
23829
25291
  }
23830
25292
 
25293
+ // ../../packages/persistence/src/vault/crypto.ts
25294
+ import {
25295
+ createCipheriv,
25296
+ createDecipheriv,
25297
+ createHmac as createHmac2,
25298
+ hkdfSync,
25299
+ timingSafeEqual
25300
+ } from "crypto";
25301
+
25302
+ // ../../packages/persistence/src/vault/key-provider.ts
25303
+ import { execFileSync } from "child_process";
25304
+ import { randomBytes as randomBytes2 } from "crypto";
25305
+ import {
25306
+ chmodSync as chmodSync2,
25307
+ mkdirSync as mkdirSync2,
25308
+ readFileSync as readFileSync4,
25309
+ renameSync as renameSync4,
25310
+ rmSync as rmSync4,
25311
+ statSync as statSync3,
25312
+ writeFileSync as writeFileSync3
25313
+ } from "fs";
25314
+ import { join as join6 } from "path";
25315
+
25316
+ // ../../packages/persistence/src/vault/vault.ts
25317
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
25318
+
23831
25319
  // ../../packages/persistence/src/warn-era-cap.ts
23832
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
23833
- import { join as join5 } from "path";
25320
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
25321
+ import { join as join7 } from "path";
23834
25322
  var MARKER = "warn-era-capped";
23835
25323
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23836
25324
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23837
- const marker = join5(dataDir2, MARKER);
23838
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25325
+ const marker = join7(dataDir2, MARKER);
25326
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
23839
25327
  const capped = db.policies.capCategoryActions();
23840
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
25328
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
23841
25329
  `, { mode: DATA_FILE_MODE });
23842
25330
  return { capped };
23843
25331
  }
23844
25332
 
23845
25333
  // ../../packages/plugin-sdk/src/config.ts
23846
- import { existsSync as existsSync4 } from "fs";
23847
- import { join as join6 } from "path";
25334
+ import { existsSync as existsSync5 } from "fs";
25335
+ import { join as join8 } from "path";
23848
25336
 
23849
25337
  // ../../packages/plugin-sdk/src/provider-env.ts
23850
25338
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -23895,11 +25383,11 @@ function resolveProvider() {
23895
25383
  }
23896
25384
 
23897
25385
  // ../../packages/plugin-sdk/src/config.ts
23898
- function loadConfig(base = defaultDataDir()) {
25386
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
23899
25387
  try {
23900
25388
  ensureLayoutDirSync(base);
23901
- const settingsFile = join6(settingsDir(base), "settings.json");
23902
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
25389
+ const settingsFile = join8(settingsDir(base), "settings.json");
25390
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
23903
25391
  } catch {
23904
25392
  }
23905
25393
  migrateLegacyLayout(base);
@@ -23910,21 +25398,21 @@ function loadConfig(base = defaultDataDir()) {
23910
25398
  dbPath: dbPath(base),
23911
25399
  settingsDir: settingsDir(base),
23912
25400
  onboarded: settings.onboardedAt != null,
23913
- provider: resolveProviderSafe()
25401
+ provider: resolveProviderSafe(resolveProviderFn)
23914
25402
  };
23915
25403
  }
23916
- function resolveProviderSafe() {
25404
+ function resolveProviderSafe(resolveProviderFn) {
23917
25405
  try {
23918
- return resolveProvider();
25406
+ return resolveProviderFn();
23919
25407
  } catch {
23920
25408
  return { provider: "anthropic" };
23921
25409
  }
23922
25410
  }
23923
25411
 
23924
25412
  // ../../packages/plugin-sdk/src/config-inventory.ts
23925
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
25413
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
23926
25414
  import { homedir as homedir2 } from "os";
23927
- import { basename as basename2, join as join8 } from "path";
25415
+ import { basename as basename3, join as join10 } from "path";
23928
25416
 
23929
25417
  // ../../packages/detections/src/egress/registry.ts
23930
25418
  var EXTRACTOR_VERSION = "1";
@@ -26288,7 +27776,7 @@ var gcp_service_account_default = {
26288
27776
  severity: "critical",
26289
27777
  matcher: {
26290
27778
  type: "regex",
26291
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
27779
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
26292
27780
  flags: "g"
26293
27781
  },
26294
27782
  examples: [
@@ -26660,40 +28148,71 @@ function bundledDetections() {
26660
28148
  }
26661
28149
 
26662
28150
  // ../../packages/plugin-sdk/src/repo.ts
26663
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
26664
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
28151
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
28152
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
26665
28153
 
26666
28154
  // ../../packages/plugin-sdk/src/events.ts
26667
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
28155
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
28156
+
28157
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
28158
+ import { existsSync as existsSync7 } from "fs";
28159
+ import { fileURLToPath } from "url";
28160
+ import { Worker } from "worker_threads";
26668
28161
 
26669
28162
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
26670
- import { arch, hostname as hostname3, platform, release } from "os";
28163
+ import { arch, hostname as hostname4, platform, release } from "os";
26671
28164
 
26672
28165
  // ../../packages/plugin-sdk/src/nudge.ts
26673
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
26674
- import { join as join9 } from "path";
28166
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
28167
+ import { join as join11 } from "path";
26675
28168
 
26676
28169
  // ../../packages/plugin-sdk/src/paths.ts
26677
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26678
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
28170
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
28171
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
26679
28172
 
26680
28173
  // ../../packages/plugin-sdk/src/project-files.ts
26681
28174
  var import_ignore = __toESM(require_ignore(), 1);
26682
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26683
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
28175
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
28176
+ import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
28177
+
28178
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
28179
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
28180
+ if (typeof v === "string" && v.trim() === "") return void 0;
28181
+ return v;
28182
+ }, external_exports.string().optional()).catch(void 0);
28183
+ var optionalFlag = external_exports.preprocess((v) => {
28184
+ if (typeof v !== "string") return false;
28185
+ const normalized = v.trim().toLowerCase();
28186
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
28187
+ }, external_exports.boolean()).catch(false);
28188
+ var antigravityProviderEnvShape = {
28189
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
28190
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
28191
+ };
28192
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
28193
+
28194
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
28195
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
28196
+ if (typeof v === "string" && v.trim() === "") return void 0;
28197
+ return v;
28198
+ }, external_exports.string().optional()).catch(void 0);
28199
+ var codexProviderEnvShape = {
28200
+ OPENAI_BASE_URL: optionalBaseUrl3
28201
+ };
28202
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
26684
28203
 
26685
28204
  // ../../packages/plugin-sdk/src/runtime.ts
26686
- import { randomUUID as randomUUID10 } from "crypto";
28205
+ import { randomUUID as randomUUID14 } from "crypto";
26687
28206
 
26688
28207
  // ../../packages/plugin-sdk/src/suppressions.ts
26689
28208
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26690
28209
 
26691
28210
  // ../../packages/plugin-sdk/src/throttle.ts
26692
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
26693
- import { join as join11 } from "path";
28211
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
28212
+ import { join as join13 } from "path";
26694
28213
 
26695
28214
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
26696
- import { randomUUID as randomUUID11 } from "crypto";
28215
+ import { randomUUID as randomUUID15 } from "crypto";
26697
28216
 
26698
28217
  // ../../packages/plugin-runtime/src/recorder.ts
26699
28218
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -26855,7 +28374,7 @@ var StandaloneDataGateway = class {
26855
28374
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
26856
28375
  const installed = this.installedScanRules();
26857
28376
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
26858
- id: randomUUID11(),
28377
+ id: randomUUID15(),
26859
28378
  scope: "global",
26860
28379
  target: { ruleId },
26861
28380
  action,
@@ -27008,7 +28527,7 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
27008
28527
  }
27009
28528
 
27010
28529
  // ../../packages/plugin-runtime/src/handle-session-start.ts
27011
- import { randomUUID as randomUUID12 } from "crypto";
28530
+ import { randomUUID as randomUUID16 } from "crypto";
27012
28531
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
27013
28532
 
27014
28533
  // src/present.ts
@@ -27127,10 +28646,58 @@ function fenced(body) {
27127
28646
  return [fence, body, fence].join("\n");
27128
28647
  }
27129
28648
 
28649
+ // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
28650
+ import { writeFileSync as writeFileSync7 } from "fs";
28651
+ import { join as join14 } from "path";
28652
+
28653
+ // ../../packages/setup-wizard/src/triage/plan-file.ts
28654
+ import { mkdtempSync, readFileSync as readFileSync9, rmdirSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
28655
+ import { tmpdir } from "os";
28656
+ import { basename as basename6, dirname as dirname4, join as join15 } from "path";
28657
+ var SuppressionEntrySchema = external_exports.object({
28658
+ ruleId: external_exports.string(),
28659
+ category: DetectionCategory,
28660
+ valueFingerprint: external_exports.string(),
28661
+ keyVersion: external_exports.number(),
28662
+ maskedValue: external_exports.string(),
28663
+ justification: external_exports.string()
28664
+ });
28665
+ var ShowcaseCategorySchema = external_exports.object({
28666
+ category: DetectionCategory,
28667
+ action: BuiltinPolicyId,
28668
+ genuineCount: external_exports.number(),
28669
+ fpCount: external_exports.number(),
28670
+ reasoning: external_exports.string()
28671
+ });
28672
+ var JoinEntrySchema = external_exports.object({
28673
+ id: external_exports.string(),
28674
+ ruleId: external_exports.string(),
28675
+ category: DetectionCategory,
28676
+ valueFingerprint: external_exports.string().optional(),
28677
+ keyVersion: external_exports.number().optional(),
28678
+ maskedMatch: external_exports.string(),
28679
+ maskedContext: external_exports.string()
28680
+ });
28681
+ var PLAN_FILE_VERSION = 3;
28682
+ var PersistedPlanSchema = external_exports.object({
28683
+ version: external_exports.literal(PLAN_FILE_VERSION),
28684
+ // partialRecord (not record): a posture only covers the categories present in
28685
+ // the evidence, so an exhaustive-key record would reject every real plan.
28686
+ posture: external_exports.partialRecord(DetectionCategory, BuiltinPolicyId),
28687
+ entries: external_exports.array(SuppressionEntrySchema),
28688
+ showcase: external_exports.array(ShowcaseCategorySchema),
28689
+ join: external_exports.array(JoinEntrySchema),
28690
+ notes: external_exports.string(),
28691
+ // The store's per-category action at preview time. The downgrade view is
28692
+ // rendered from it at PREVIEW (renderPosturePlan); confirm reads it back ONLY to
28693
+ // compare against the live store and reject a stale plan (runConfirm's drift gate).
28694
+ current: external_exports.partialRecord(DetectionCategory, ActionTaken)
28695
+ });
28696
+
27130
28697
  // src/command-registry.ts
27131
- import { readdirSync as readdirSync4 } from "fs";
27132
- import { fileURLToPath } from "url";
27133
- var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
28698
+ import { readdirSync as readdirSync5 } from "fs";
28699
+ import { fileURLToPath as fileURLToPath2 } from "url";
28700
+ var COMMANDS_DIR = fileURLToPath2(new URL("../commands", import.meta.url));
27134
28701
 
27135
28702
  // src/render.ts
27136
28703
  var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
@@ -27217,8 +28784,8 @@ function renderStatusBar(s, opts = {}) {
27217
28784
  const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
27218
28785
  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)}`;
27219
28786
  const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
27220
- const open = `${flag} ${String(s.openFindings)} open findings`;
27221
- return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
28787
+ const open2 = `${flag} ${String(s.openFindings)} open findings`;
28788
+ return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open2}`;
27222
28789
  }
27223
28790
  function findingStatus(summary) {
27224
28791
  return {
@@ -27444,7 +29011,11 @@ function renderExceptions(exceptions, nowMs = Date.now()) {
27444
29011
  }
27445
29012
  const rows = exceptions.map((e) => [
27446
29013
  e.id.slice(0, 8),
27447
- e.maskedValue,
29014
+ // A reveal grant is strictly stronger than a plain suppression: while it is
29015
+ // active the model can receive this value's RAW form at tool boundaries.
29016
+ // Tag the row so it can never be mistaken for a suppress-only grant. The
29017
+ // value itself stays masked — this list shows metadata, never raw values.
29018
+ e.capability === "reveal_to_model" ? `${e.maskedValue} \xB7 REVEALS-TO-MODEL` : e.maskedValue,
27448
29019
  e.ruleId,
27449
29020
  e.scope,
27450
29021
  relativeExpiry(e.expiresAt, nowMs),