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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -492,13 +492,12 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync4 } from "fs";
496
- import { join as join6 } from "path";
495
+ import { existsSync as existsSync5 } from "fs";
496
+ import { join as join8 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/database.ts
499
- import { randomUUID as randomUUID8 } from "crypto";
500
- import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
501
- import { join, sep } from "path";
499
+ import { randomUUID as randomUUID10 } from "crypto";
500
+ import { join as join2, sep } from "path";
502
501
  import { DatabaseSync } from "node:sqlite";
503
502
 
504
503
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
@@ -562,6 +561,30 @@ var SQLITE_MIGRATIONS = [
562
561
  {
563
562
  tag: "0014_drop_legacy_events_findings",
564
563
  sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
564
+ },
565
+ {
566
+ tag: "0015_busy_vengeance",
567
+ sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
568
+ },
569
+ {
570
+ tag: "0016_breezy_zodiak",
571
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
572
+ },
573
+ {
574
+ tag: "0017_rainy_kat_farrell",
575
+ sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
576
+ },
577
+ {
578
+ tag: "0018_serious_tana_nile",
579
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
580
+ },
581
+ {
582
+ tag: "0019_audit_started_at_index",
583
+ sql: "CREATE INDEX `idx_audit_started_at` ON `audit_events` (`started_at`);"
584
+ },
585
+ {
586
+ tag: "0020_secret_vault_pagination_indexes",
587
+ sql: "CREATE INDEX `idx_secret_vault_last_seen` ON `secret_vault` (`last_seen`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_reuse` ON `secret_vault` (`occurrence_count`,`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_at` ON `secret_vault_deref` (`at`,`id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_signal` ON `secret_vault_deref` (`at`,`id`) WHERE `reason` NOT IN ('display', 'view-render');"
565
588
  }
566
589
  ];
567
590
 
@@ -15299,7 +15322,17 @@ var Finding = external_exports.object({
15299
15322
  }).meta({ id: "Finding" });
15300
15323
  var DetectedFinding = Finding.meta({ id: "DetectedFinding" });
15301
15324
  var FindingAction = external_exports.enum(["blocked", "redacted", "warned", "allowed", "quarantined", "monitored"]).meta({ id: "FindingAction" });
15302
- var FindingProvider = external_exports.enum(["claudecode", "claudedesktop", "cursor", "copilot", "chatgpt", "api"]).meta({ id: "FindingProvider" });
15325
+ var FindingProvider = external_exports.enum([
15326
+ "claudecode",
15327
+ "claudedesktop",
15328
+ "cursor",
15329
+ "copilot",
15330
+ "chatgpt",
15331
+ "claudeai",
15332
+ "codex",
15333
+ "antigravity",
15334
+ "api"
15335
+ ]).meta({ id: "FindingProvider" });
15303
15336
  var FindingCategory = external_exports.enum([
15304
15337
  "secret",
15305
15338
  "pii",
@@ -15353,7 +15386,16 @@ var FindingInstance = external_exports.object({
15353
15386
  confidence: external_exports.number().min(0).max(1),
15354
15387
  // Lifecycle status (see FindingStatus). Optional so legacy callers/rows
15355
15388
  // that predate the resolution feature stay valid.
15356
- status: FindingStatus.optional()
15389
+ status: FindingStatus.optional(),
15390
+ // The audit event this finding was captured from. Optional so callers that
15391
+ // do not project it stay valid. An at-rest finding is content-addressed by
15392
+ // finding_key and its row is upserted on re-detection, so this names the
15393
+ // MOST RECENT detection event, not the first.
15394
+ eventId: external_exports.string().optional(),
15395
+ // The session that event belongs to, when it has one — the seam a
15396
+ // per-instance "view session" link needs. Absent for events captured
15397
+ // outside a session.
15398
+ sessionId: external_exports.string().optional()
15357
15399
  }).meta({ id: "FindingInstance" });
15358
15400
  var FindingGroup = external_exports.object({
15359
15401
  id: external_exports.string(),
@@ -15397,7 +15439,11 @@ var FindingFacets = external_exports.object({
15397
15439
  // for every instance, so every group lands in a bucket; a status-less
15398
15440
  // group (possible only for callers whose rows carry no statuses) is
15399
15441
  // counted under no value.
15400
- status: external_exports.array(FindingFacetItem)
15442
+ status: external_exports.array(FindingFacetItem),
15443
+ // Host tool (attributes.tool_name). Present only on the instance-level
15444
+ // reads, which can filter by it; the grouped read omits the dimension
15445
+ // because a group spans tools.
15446
+ tool: external_exports.array(FindingFacetItem).optional()
15401
15447
  }).meta({ id: "FindingFacets" });
15402
15448
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15403
15449
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15415,6 +15461,16 @@ var ListGroupedFindingsQuery = external_exports.object({
15415
15461
  // Scope to findings whose event carries this session id (the Activity page's
15416
15462
  // session → findings drilldown). Findings without a session never match.
15417
15463
  sessionId: external_exports.string().optional(),
15464
+ // Inclusive lower bound on the parent event's timestamp, so a caller arriving
15465
+ // from a time-scoped page (Activity's range) can carry that scope. Absent
15466
+ // means all time — this list has no default window.
15467
+ from: external_exports.iso.datetime().optional(),
15468
+ // A group or instance id that must appear in the page even when the cursor
15469
+ // has already advanced past its sort position. This is what keeps the
15470
+ // Findings page's one-shot ?finding= deep link resolving once the list
15471
+ // paginates: the target group is appended out of sort order rather than
15472
+ // scanning forward for it. Never affects totals, facets or the cursor.
15473
+ includeId: external_exports.string().optional(),
15418
15474
  groupBy: external_exports.literal("type").optional(),
15419
15475
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15420
15476
  cursor: external_exports.string().optional()
@@ -15459,15 +15515,110 @@ var FindingInstanceDetail = FindingInstance.extend({
15459
15515
  detection: FindingDetectionRef,
15460
15516
  policy: FindingPolicyRef
15461
15517
  }).meta({ id: "FindingInstanceDetail" });
15518
+ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
15519
+ var ListFindingInstancesQuery = external_exports.object({
15520
+ severity: external_exports.array(Severity).optional(),
15521
+ // Rule ids, the same vocabulary the grouped list's `subtype` carries.
15522
+ subtype: external_exports.array(external_exports.string()).optional(),
15523
+ provider: external_exports.array(FindingProvider).optional(),
15524
+ action: external_exports.array(FindingAction).optional(),
15525
+ // Matches each instance's OWN derived status (deriveFindingStatus), unlike
15526
+ // the grouped query's group-level fold.
15527
+ status: external_exports.array(FindingStatus).optional(),
15528
+ // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
15529
+ // where the free-text `q` can only match the rendered "via Bash" label.
15530
+ tool: external_exports.array(external_exports.string()).optional(),
15531
+ // Exact repository / file-path matches, for the drill-down out of the
15532
+ // locations view. A row whose event carries no repo/file matches neither.
15533
+ repo: external_exports.string().optional(),
15534
+ file: external_exports.string().optional(),
15535
+ q: external_exports.string().optional(),
15536
+ sessionId: external_exports.string().optional(),
15537
+ from: external_exports.iso.datetime().optional(),
15538
+ limit: external_exports.coerce.number().int().min(1).max(200).optional(),
15539
+ cursor: external_exports.string().optional()
15540
+ });
15541
+ var ListFindingInstancesResponse = external_exports.object({
15542
+ // Instances matching the filters across the whole scope, not just this
15543
+ // page — cursor-independent, like the grouped list's totals.
15544
+ totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
15545
+ // Counts in INSTANCES here, where the grouped response counts groups. Each
15546
+ // dimension still excludes its own filter.
15547
+ facets: FindingFacets,
15548
+ items: external_exports.array(FindingInstanceDetail),
15549
+ nextCursor: external_exports.string().nullable()
15550
+ }).meta({ id: "ListFindingInstancesResponse" });
15551
+ var FindingLocationFile = external_exports.object({
15552
+ // Empty when the instances carried no file path (a prompt or a tool call
15553
+ // with no file attribution).
15554
+ file: external_exports.string(),
15555
+ instanceCount: external_exports.number().int().nonnegative(),
15556
+ maxSeverity: Severity,
15557
+ latestDetectedAt: external_exports.iso.datetime(),
15558
+ // Folded from the instances' derived statuses with the same
15559
+ // open-dominates precedence a group uses.
15560
+ status: FindingStatus.optional(),
15561
+ // Distinct rules seen at this location, capped — the row shows them as
15562
+ // chips, and the count is what conveys scale.
15563
+ ruleIds: external_exports.array(external_exports.string())
15564
+ }).meta({ id: "FindingLocationFile" });
15565
+ var FindingLocationRepo = external_exports.object({
15566
+ /** Empty when the instances carried no repo attribute. */
15567
+ repo: external_exports.string(),
15568
+ instanceCount: external_exports.number().int().nonnegative(),
15569
+ maxSeverity: Severity,
15570
+ latestDetectedAt: external_exports.iso.datetime(),
15571
+ status: FindingStatus.optional(),
15572
+ files: external_exports.array(FindingLocationFile)
15573
+ }).meta({ id: "FindingLocationRepo" });
15574
+ var ListFindingLocationsQuery = external_exports.object({
15575
+ severity: external_exports.array(Severity).optional(),
15576
+ subtype: external_exports.array(external_exports.string()).optional(),
15577
+ provider: external_exports.array(FindingProvider).optional(),
15578
+ action: external_exports.array(FindingAction).optional(),
15579
+ // Per-instance, as in ListFindingInstancesQuery: a location keeps the
15580
+ // instances that match, and folds its status from those.
15581
+ status: external_exports.array(FindingStatus).optional(),
15582
+ tool: external_exports.array(external_exports.string()).optional(),
15583
+ q: external_exports.string().optional(),
15584
+ sessionId: external_exports.string().optional(),
15585
+ from: external_exports.iso.datetime().optional(),
15586
+ limit: external_exports.coerce.number().int().min(1).max(500).optional()
15587
+ });
15588
+ var ListFindingLocationsResponse = external_exports.object({
15589
+ totals: external_exports.object({
15590
+ findings: external_exports.number().int().nonnegative(),
15591
+ repos: external_exports.number().int().nonnegative(),
15592
+ files: external_exports.number().int().nonnegative()
15593
+ }),
15594
+ /** Sorted by max severity, then most recent. */
15595
+ items: external_exports.array(FindingLocationRepo),
15596
+ /** Whether `limit` truncated the repo list. */
15597
+ hasMore: external_exports.boolean()
15598
+ }).meta({ id: "ListFindingLocationsResponse" });
15462
15599
 
15463
15600
  // ../../packages/schema/src/zod/harness-map.ts
15464
- var Harness = external_exports.enum(["claudecode", "cursor", "copilot", "codex", "windsurf", "claudedesktop", "chatgpt", "api"]).meta({ id: "Harness" });
15601
+ var Harness = external_exports.enum([
15602
+ "claudecode",
15603
+ "cursor",
15604
+ "copilot",
15605
+ "codex",
15606
+ "antigravity",
15607
+ "windsurf",
15608
+ "claudedesktop",
15609
+ "chatgpt",
15610
+ "claudeai",
15611
+ "api"
15612
+ ]).meta({ id: "Harness" });
15465
15613
  var TOOL_TO_HARNESS = {
15466
15614
  "claude-code": "claudecode",
15467
15615
  "claude-desktop": "claudedesktop",
15468
15616
  "github-copilot": "copilot",
15469
15617
  cursor: "cursor",
15470
- chatgpt: "chatgpt"
15618
+ chatgpt: "chatgpt",
15619
+ codex: "codex",
15620
+ antigravity: "antigravity",
15621
+ "claude-ai": "claudeai"
15471
15622
  };
15472
15623
  function harnessFromTool(tool) {
15473
15624
  return TOOL_TO_HARNESS[tool] ?? tool;
@@ -15928,7 +16079,18 @@ var ActivityOverviewResponse = external_exports.object({
15928
16079
  // ../../packages/schema/src/zod/event.ts
15929
16080
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15930
16081
  var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15931
- var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16082
+ var SourceTool = external_exports.enum([
16083
+ "claude-code",
16084
+ "claude-desktop",
16085
+ "cursor",
16086
+ "chatgpt",
16087
+ "claude-ai",
16088
+ "github-copilot",
16089
+ "codex",
16090
+ "antigravity",
16091
+ "cli",
16092
+ "unknown"
16093
+ ]).meta({ id: "SourceTool" });
15932
16094
  var EventMetadata = external_exports.object({
15933
16095
  sessionId: external_exports.string().optional(),
15934
16096
  repo: external_exports.string().optional(),
@@ -15999,7 +16161,7 @@ var TrustLevel = external_exports.enum(["known-good", "risky", "unapproved"]).me
15999
16161
  var Flag = external_exports.enum(["update", "stale", "conflict", "unknown", "change", "untracked", "risk", "findings"]).meta({ id: "Flag" });
16000
16162
  var Visibility = external_exports.enum(["public", "private"]).meta({ id: "Visibility" });
16001
16163
  var HarnessEventKind = external_exports.enum(["block", "redact", "warn"]).meta({ id: "HarnessEventKind" });
16002
- var HarnessId = external_exports.enum(["claudecode", "cursor", "codex"]).meta({ id: "HarnessId" });
16164
+ var HarnessId = external_exports.enum(["claudecode", "cursor", "codex", "antigravity"]).meta({ id: "HarnessId" });
16003
16165
  var AccessCounts = external_exports.object({
16004
16166
  open: external_exports.number().int().nonnegative(),
16005
16167
  approved: external_exports.number().int().nonnegative(),
@@ -16221,6 +16383,7 @@ var ExceptionConditions = external_exports.object({
16221
16383
  sourceTool: external_exports.string().optional(),
16222
16384
  provider: external_exports.string().optional()
16223
16385
  }).strict();
16386
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16224
16387
  var DetectionException = external_exports.object({
16225
16388
  id: external_exports.guid(),
16226
16389
  ruleId: external_exports.string(),
@@ -16237,6 +16400,7 @@ var DetectionException = external_exports.object({
16237
16400
  keyVersion: external_exports.number().int().positive(),
16238
16401
  // maskMatch() preview of the approved value — never the raw value.
16239
16402
  maskedValue: external_exports.string(),
16403
+ capability: ExceptionCapability.default("suppress"),
16240
16404
  scope: ExceptionScope,
16241
16405
  expiresAt: external_exports.iso.datetime().nullable(),
16242
16406
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16260,11 +16424,13 @@ var ExceptionBundleEntry = DetectionException.pick({
16260
16424
  ruleId: true,
16261
16425
  valueFingerprint: true,
16262
16426
  keyVersion: true,
16427
+ capability: true,
16263
16428
  expiresAt: true,
16264
16429
  maxUses: true,
16265
16430
  useCount: true,
16266
16431
  conditions: true
16267
16432
  });
16433
+ var ExceptionDescriptor = DetectionException.omit({ valueFingerprint: true });
16268
16434
 
16269
16435
  // ../../packages/schema/src/zod/rule.ts
16270
16436
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -17075,6 +17241,35 @@ var EgressWriteSummary = external_exports.object({
17075
17241
  droppedFiles: external_exports.array(external_exports.string()).default([])
17076
17242
  }).meta({ id: "EgressWriteSummary" });
17077
17243
 
17244
+ // ../../packages/schema/src/zod/exception-action.ts
17245
+ var confirmation = external_exports.string().optional();
17246
+ var ApproveBlockedInput = external_exports.object({
17247
+ reference: external_exports.string(),
17248
+ scope: external_exports.string(),
17249
+ reason: external_exports.string(),
17250
+ confirmation
17251
+ });
17252
+ var AddExceptionInput = external_exports.object({
17253
+ ruleId: external_exports.string(),
17254
+ value: external_exports.string(),
17255
+ scope: external_exports.string(),
17256
+ reason: external_exports.string(),
17257
+ confirmation
17258
+ });
17259
+ var GrantRevealInput = external_exports.object({
17260
+ pointer: external_exports.string(),
17261
+ scope: external_exports.string(),
17262
+ justification: external_exports.string(),
17263
+ confirmation
17264
+ });
17265
+ var RevokeExceptionInput = external_exports.object({
17266
+ id: external_exports.string(),
17267
+ reason: external_exports.string()
17268
+ });
17269
+ var RotateKeyInput = external_exports.object({
17270
+ confirmation: external_exports.string()
17271
+ });
17272
+
17078
17273
  // ../../packages/schema/src/zod/findings-group-build.ts
17079
17274
  function toApiAction(dbVal) {
17080
17275
  const map2 = {
@@ -17130,6 +17325,8 @@ function buildFindingGroups(rows, opts = {}) {
17130
17325
  repo: r.repo,
17131
17326
  file: r.file,
17132
17327
  ...r.toolName === void 0 ? {} : { toolName: r.toolName },
17328
+ ...r.eventId === void 0 ? {} : { eventId: r.eventId },
17329
+ ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
17133
17330
  action: toApiAction(effectiveDbAction),
17134
17331
  detectedAt: r.occurredAt,
17135
17332
  confidence: r.confidence,
@@ -17261,14 +17458,17 @@ function applyFindingFilters(groups, opts) {
17261
17458
  }
17262
17459
  var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
17263
17460
  var SEVERITY_RANK = SEVERITY_ORDER;
17461
+ function compareFindingGroupOrder(a, b) {
17462
+ const rankA = SEVERITY_RANK[a.severity] ?? -1;
17463
+ const rankB = SEVERITY_RANK[b.severity] ?? -1;
17464
+ const severityDiff = rankA - rankB;
17465
+ if (severityDiff !== 0) return severityDiff;
17466
+ const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17467
+ if (recencyDiff !== 0) return recencyDiff;
17468
+ return a.id.localeCompare(b.id);
17469
+ }
17264
17470
  function sortFindingGroups(groups) {
17265
- return [...groups].sort((a, b) => {
17266
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
17267
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
17268
- const severityDiff = rankA - rankB;
17269
- if (severityDiff !== 0) return severityDiff;
17270
- return b.latestDetectedAt.localeCompare(a.latestDetectedAt);
17271
- });
17471
+ return [...groups].sort(compareFindingGroupOrder);
17272
17472
  }
17273
17473
  function computeFindingFacets(allGroups, opts) {
17274
17474
  const forSeverity = applyFindingFilters(allGroups, {
@@ -17324,15 +17524,158 @@ function computeFindingFacets(allGroups, opts) {
17324
17524
  for (const g of forStatus) {
17325
17525
  if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17326
17526
  }
17327
- const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17527
+ const toItems2 = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17528
+ return {
17529
+ severity: toItems2(severityMap),
17530
+ provider: toItems2(providerMap),
17531
+ action: toItems2(actionMap),
17532
+ subtype: toItems2(subtypeMap),
17533
+ status: toItems2(statusMap)
17534
+ };
17535
+ }
17536
+
17537
+ // ../../packages/schema/src/zod/findings-flat-build.ts
17538
+ function rowHaystack(row) {
17539
+ return [
17540
+ row.ruleId,
17541
+ row.category,
17542
+ row.maskedMatch,
17543
+ row.repo,
17544
+ row.file,
17545
+ row.toolName ? `via ${row.toolName}` : "",
17546
+ row.id
17547
+ ].join(" ").toLowerCase();
17548
+ }
17549
+ function matchesDimension(row, opts, dimension) {
17550
+ switch (dimension) {
17551
+ case "severity":
17552
+ return !opts.severity?.length || opts.severity.includes(row.severity);
17553
+ case "subtype":
17554
+ return !opts.subtype?.length || opts.subtype.includes(row.ruleId);
17555
+ case "providers":
17556
+ return !opts.providers?.length || opts.providers.includes(toApiProvider(row.sourceTool));
17557
+ case "actions":
17558
+ return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
17559
+ case "statuses":
17560
+ return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
17561
+ case "tools":
17562
+ return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
17563
+ case "repo":
17564
+ return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
17565
+ case "file":
17566
+ return opts.file === void 0 || opts.file === "" || row.file === opts.file;
17567
+ case "q":
17568
+ return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
17569
+ }
17570
+ }
17571
+ var DIMENSIONS = [
17572
+ "severity",
17573
+ "subtype",
17574
+ "providers",
17575
+ "actions",
17576
+ "statuses",
17577
+ "tools",
17578
+ "repo",
17579
+ "file",
17580
+ "q"
17581
+ ];
17582
+ function matchesInstanceFilters(row, opts, except) {
17583
+ for (const dimension of DIMENSIONS) {
17584
+ if (dimension === except) continue;
17585
+ if (!matchesDimension(row, opts, dimension)) return false;
17586
+ }
17587
+ return true;
17588
+ }
17589
+ function toItems(counts) {
17590
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
17591
+ }
17592
+ function bump(counts, value) {
17593
+ counts.set(value, (counts.get(value) ?? 0) + 1);
17594
+ }
17595
+ function createInstanceFacetAccumulator(opts) {
17596
+ const severity = /* @__PURE__ */ new Map();
17597
+ const subtype = /* @__PURE__ */ new Map();
17598
+ const provider = /* @__PURE__ */ new Map();
17599
+ const action = /* @__PURE__ */ new Map();
17600
+ const status = /* @__PURE__ */ new Map();
17601
+ const tool = /* @__PURE__ */ new Map();
17602
+ return {
17603
+ add(row) {
17604
+ if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
17605
+ if (matchesInstanceFilters(row, opts, "subtype")) bump(subtype, row.ruleId);
17606
+ if (matchesInstanceFilters(row, opts, "providers")) {
17607
+ bump(provider, toApiProvider(row.sourceTool));
17608
+ }
17609
+ if (matchesInstanceFilters(row, opts, "actions")) bump(action, toApiAction(row.actionTaken));
17610
+ if (row.status !== void 0 && matchesInstanceFilters(row, opts, "statuses")) {
17611
+ bump(status, row.status);
17612
+ }
17613
+ if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
17614
+ bump(tool, row.toolName);
17615
+ }
17616
+ },
17617
+ facets: () => ({
17618
+ severity: toItems(severity),
17619
+ subtype: toItems(subtype),
17620
+ provider: toItems(provider),
17621
+ action: toItems(action),
17622
+ status: toItems(status),
17623
+ tool: toItems(tool)
17624
+ })
17625
+ };
17626
+ }
17627
+ function toInstanceDetail(row) {
17628
+ const category = toApiCategory(row.category);
17328
17629
  return {
17329
- severity: toItems(severityMap),
17330
- provider: toItems(providerMap),
17331
- action: toItems(actionMap),
17332
- subtype: toItems(subtypeMap),
17333
- status: toItems(statusMap)
17630
+ id: row.id,
17631
+ provider: toApiProvider(row.sourceTool),
17632
+ repo: row.repo,
17633
+ file: row.file,
17634
+ ...row.toolName === void 0 ? {} : { toolName: row.toolName },
17635
+ eventId: row.eventId,
17636
+ ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
17637
+ action: toApiAction(row.actionTaken),
17638
+ detectedAt: row.occurredAt,
17639
+ confidence: row.confidence,
17640
+ ...row.status === void 0 ? {} : { status: row.status },
17641
+ groupId: row.ruleId,
17642
+ category,
17643
+ subtype: row.ruleId,
17644
+ severity: row.severity,
17645
+ match: { maskedValue: row.maskedMatch, contextPrefix: "" },
17646
+ detection: { id: row.ruleId, name: null },
17647
+ policy: { id: `category:${category}`, name: category }
17648
+ };
17649
+ }
17650
+ var SEVERITY_ORDER2 = {
17651
+ critical: 0,
17652
+ high: 1,
17653
+ medium: 2,
17654
+ low: 3
17655
+ };
17656
+ function newLocationAccumulator() {
17657
+ return {
17658
+ instanceCount: 0,
17659
+ // Sorts after every known severity, so the first row always wins the
17660
+ // comparison below rather than an unknown value pinning the location.
17661
+ maxSeverityRank: Number.MAX_SAFE_INTEGER,
17662
+ maxSeverity: "low",
17663
+ latestDetectedAt: "",
17664
+ statuses: [],
17665
+ ruleIds: /* @__PURE__ */ new Set()
17334
17666
  };
17335
17667
  }
17668
+ function addToLocation(acc, row) {
17669
+ acc.instanceCount += 1;
17670
+ const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
17671
+ if (rank < acc.maxSeverityRank) {
17672
+ acc.maxSeverityRank = rank;
17673
+ acc.maxSeverity = row.severity;
17674
+ }
17675
+ if (row.occurredAt > acc.latestDetectedAt) acc.latestDetectedAt = row.occurredAt;
17676
+ acc.statuses.push(row.status);
17677
+ acc.ruleIds.add(row.ruleId);
17678
+ }
17336
17679
 
17337
17680
  // ../../packages/schema/src/zod/installed-pack.ts
17338
17681
  var InstalledPack = external_exports.object({
@@ -17364,8 +17707,172 @@ var PatchInstalledPackRequest = external_exports.object({
17364
17707
  message: "At least one field must be provided"
17365
17708
  }).meta({ id: "PatchInstalledPackRequest" });
17366
17709
 
17710
+ // ../../packages/schema/src/zod/vault.ts
17711
+ var POINTER_FORMAT_VERSION = 2;
17712
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17713
+ var POINTER_TOKEN_PATTERN = new RegExp(
17714
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17715
+ );
17716
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17717
+ function pointerTokenScanner() {
17718
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
17719
+ }
17720
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17721
+ var ParsedPointer = external_exports.object({
17722
+ category: DetectionCategory,
17723
+ keyVersion: external_exports.number().int().positive(),
17724
+ pointerId: external_exports.string(),
17725
+ tag: external_exports.string()
17726
+ });
17727
+ var VaultEntry = external_exports.object({
17728
+ pointerId: external_exports.string(),
17729
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17730
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17731
+ // independently of the vault encryption key below.
17732
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17733
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17734
+ // The vault-key epoch this row's ciphertext was sealed under.
17735
+ keyVersion: external_exports.number().int().positive(),
17736
+ // Fixed at first mint and never updated: the same value detected later under a
17737
+ // different rule's category keeps the category it was minted with, so one
17738
+ // value always produces exactly one wire token.
17739
+ category: DetectionCategory,
17740
+ ruleId: external_exports.string(),
17741
+ // Partial-reveal preview for badges and listings. Never the raw value.
17742
+ maskedMatch: external_exports.string(),
17743
+ provider: external_exports.string().optional(),
17744
+ ciphertext: external_exports.string(),
17745
+ nonce: external_exports.string(),
17746
+ authTag: external_exports.string(),
17747
+ // How many times this value has been detected on this machine — the reuse
17748
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17749
+ occurrenceCount: external_exports.number().int().nonnegative(),
17750
+ firstSeen: external_exports.string(),
17751
+ lastSeen: external_exports.string()
17752
+ });
17753
+ var PointerDescriptor = external_exports.object({
17754
+ category: DetectionCategory,
17755
+ provider: external_exports.string().optional(),
17756
+ maskedMatch: external_exports.string(),
17757
+ occurrences: external_exports.number().int().nonnegative(),
17758
+ firstSeen: external_exports.string(),
17759
+ lastSeen: external_exports.string()
17760
+ });
17761
+ var PointerIdentity = external_exports.object({
17762
+ ruleId: external_exports.string(),
17763
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17764
+ fingerprintKeyVersion: external_exports.number().int().positive()
17765
+ });
17766
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17767
+ var VaultDerefReason = external_exports.enum([
17768
+ "display",
17769
+ "explicit-reveal",
17770
+ "view-render",
17771
+ "model-input",
17772
+ "remediation",
17773
+ "purge"
17774
+ ]);
17775
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17776
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
17777
+ function isBatchedDerefReason(reason) {
17778
+ return BATCHED_DEREF_REASONS.includes(reason);
17779
+ }
17780
+ var VaultDeref = external_exports.object({
17781
+ id: external_exports.guid(),
17782
+ pointerId: external_exports.string(),
17783
+ at: external_exports.string(),
17784
+ target: DetokenizeTarget,
17785
+ reason: VaultDerefReason,
17786
+ outcome: VaultDerefOutcome,
17787
+ // Present only on a model-target crossing that a reveal grant authorized.
17788
+ grantId: external_exports.string().optional(),
17789
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17790
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17791
+ pointerCount: external_exports.number().int().positive().default(1)
17792
+ });
17793
+ var VaultSightingKind = external_exports.enum([
17794
+ "prompt",
17795
+ "tool-input",
17796
+ "tool-output",
17797
+ "file",
17798
+ "transcript"
17799
+ ]);
17800
+ var VaultSighting = external_exports.object({
17801
+ location: external_exports.string(),
17802
+ kind: VaultSightingKind,
17803
+ firstSeen: external_exports.string(),
17804
+ lastSeen: external_exports.string()
17805
+ });
17806
+ var VaultInventoryEntry = external_exports.object({
17807
+ pointerId: external_exports.string(),
17808
+ category: DetectionCategory,
17809
+ provider: external_exports.string().optional(),
17810
+ maskedMatch: external_exports.string(),
17811
+ occurrences: external_exports.number().int().nonnegative(),
17812
+ firstSeen: external_exports.string(),
17813
+ lastSeen: external_exports.string(),
17814
+ // The active reveal-to-model grant covering this value, when one exists —
17815
+ // the inventory badges it, the row links to revocation.
17816
+ revealGrantId: external_exports.string().nullable(),
17817
+ sightings: external_exports.array(VaultSighting)
17818
+ });
17819
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
17820
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
17821
+ var MAX_VAULT_PAGE_LIMIT = 200;
17822
+ var ListVaultInventoryQuery = external_exports.object({
17823
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17824
+ // Opaque; names the last row of the page just served.
17825
+ cursor: external_exports.string().optional()
17826
+ });
17827
+ var ListVaultInventoryResponse = external_exports.object({
17828
+ // Vaulted values across the whole store, not just this page — cursor-
17829
+ // independent, so paging never changes what the count claims.
17830
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
17831
+ items: external_exports.array(VaultInventoryEntry),
17832
+ // `null` once the last page is reached.
17833
+ nextCursor: external_exports.string().nullable()
17834
+ });
17835
+ var ListVaultReuseQuery = external_exports.object({
17836
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17837
+ cursor: external_exports.string().optional()
17838
+ });
17839
+ var ListVaultReuseResponse = external_exports.object({
17840
+ // Reused values across the whole store — the number the section's claim
17841
+ // ("values detected in more than one place") is about.
17842
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
17843
+ items: external_exports.array(VaultInventoryEntry),
17844
+ nextCursor: external_exports.string().nullable()
17845
+ });
17846
+ var ListVaultDerefsQuery = external_exports.object({
17847
+ // Include the batched, high-volume reasons (display, view-render). Omitted
17848
+ // hides them and counts them into `hiddenBatched` instead, so the model
17849
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
17850
+ // over a Server Action, which preserves the type, never as a URL param.
17851
+ includeBatched: external_exports.boolean().optional(),
17852
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
17853
+ cursor: external_exports.string().optional()
17854
+ });
17855
+ var ListVaultDerefsResponse = external_exports.object({
17856
+ items: external_exports.array(VaultDeref),
17857
+ nextCursor: external_exports.string().nullable(),
17858
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
17859
+ // this page — it is the count the "N hidden" line and its toggle speak for.
17860
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
17861
+ hiddenBatched: external_exports.number().int().nonnegative()
17862
+ });
17863
+ var VaultKeyCustody = external_exports.string();
17864
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17865
+ var VAULT_CONSENT_VERSION = 1;
17866
+ var VaultConsent = external_exports.object({
17867
+ acknowledgedAt: external_exports.iso.datetime(),
17868
+ version: external_exports.number().int().positive()
17869
+ });
17870
+ function isVaultConsentValid(consent) {
17871
+ return consent?.version === VAULT_CONSENT_VERSION;
17872
+ }
17873
+
17367
17874
  // ../../packages/schema/src/zod/local.ts
17368
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17875
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17369
17876
  var RunMode = external_exports.enum(["standalone"]);
17370
17877
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17371
17878
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17387,6 +17894,16 @@ var WorkspaceSettings = external_exports.object({
17387
17894
  // In-place egress extraction on the scan paths; disable to stop all Data
17388
17895
  // Shares writes.
17389
17896
  dataSharesInPlace: external_exports.boolean().default(true),
17897
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17898
+ // vault, instead of destroying them. Absent by default: this is a custody
17899
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17900
+ // Revoking stops future vaulting; it does not erase what is already stored —
17901
+ // purging the vault is the eraser.
17902
+ vaultConsent: VaultConsent.optional(),
17903
+ // Where the vault master key lives.
17904
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17905
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17906
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17390
17907
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17391
17908
  onboardedAt: external_exports.iso.datetime().optional(),
17392
17909
  // Records that the user consented to sending findings to the model API for
@@ -17719,7 +18236,7 @@ var TopSourcesQuery = external_exports.object({
17719
18236
  // Omit for both kinds.
17720
18237
  kind: external_exports.enum(SOURCE_KINDS).optional()
17721
18238
  });
17722
- var Provider = external_exports.enum(["claudecode", "cursor", "codex", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
18239
+ var Provider = external_exports.enum(["claudecode", "cursor", "codex", "antigravity", "claudeai", "chatgpt", "copilot", "api"]).meta({ id: "Provider" });
17723
18240
  var ScanCoverageProvider = external_exports.object({
17724
18241
  provider: Provider,
17725
18242
  // Percent of that provider's traffic scanned in the window. 0 when unsupported.
@@ -17972,6 +18489,195 @@ function captureId(sessionId, contentHash, filePath = null) {
17972
18489
  );
17973
18490
  }
17974
18491
 
18492
+ // ../../packages/persistence/src/internal/snapshot.ts
18493
+ import { randomUUID } from "crypto";
18494
+ import { existsSync, readdirSync, renameSync as renameSync2, rmSync as rmSync2, statSync } from "fs";
18495
+ import { basename, dirname, join } from "path";
18496
+
18497
+ // ../../packages/persistence/src/paths.ts
18498
+ import {
18499
+ chmodSync,
18500
+ linkSync,
18501
+ lstatSync,
18502
+ mkdirSync,
18503
+ renameSync,
18504
+ rmSync,
18505
+ writeFileSync
18506
+ } from "fs";
18507
+ import { threadId } from "worker_threads";
18508
+ var DATA_DIR_MODE = 448;
18509
+ var DATA_FILE_MODE = 384;
18510
+ var DB_FILENAME = "aka.db";
18511
+ function isSymlink(path) {
18512
+ try {
18513
+ return lstatSync(path).isSymbolicLink();
18514
+ } catch {
18515
+ return false;
18516
+ }
18517
+ }
18518
+ function chmodBestEffort(path, mode) {
18519
+ if (isSymlink(path)) return;
18520
+ try {
18521
+ chmodSync(path, mode);
18522
+ } catch {
18523
+ }
18524
+ }
18525
+ function tightenDir(dir) {
18526
+ chmodBestEffort(dir, DATA_DIR_MODE);
18527
+ }
18528
+ function ensureDataDirSync(dir) {
18529
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18530
+ tightenDir(dir);
18531
+ }
18532
+ function dbSidecars(file2) {
18533
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18534
+ }
18535
+ function tightenFile(file2) {
18536
+ chmodBestEffort(file2, DATA_FILE_MODE);
18537
+ }
18538
+ function tightenPerms(file2) {
18539
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18540
+ }
18541
+ function classifyOccupant(file2) {
18542
+ try {
18543
+ if (lstatSync(file2).isSymbolicLink()) return { kind: "symlink" };
18544
+ return { kind: "gone" };
18545
+ } catch (err) {
18546
+ if (err.code === "ENOENT") return { kind: "gone" };
18547
+ return { kind: "unknown", cause: err };
18548
+ }
18549
+ }
18550
+ var KeyUnclaimableError = class extends Error {
18551
+ code = "key-unclaimable";
18552
+ // `cause` is installed only when there IS one. Passing { cause: undefined }
18553
+ // defines the property anyway, so an error carrying nothing would still answer
18554
+ // `'cause' in err` — a present-but-empty field reads as a diagnosis that was
18555
+ // captured and then lost, which is worse than its plain absence.
18556
+ constructor(message, cause) {
18557
+ super(message, cause === void 0 ? void 0 : { cause });
18558
+ this.name = "KeyUnclaimableError";
18559
+ }
18560
+ };
18561
+ function createOwnerOnlyFileSync(file2, data) {
18562
+ const tmp = `${file2}.${String(process.pid)}.${String(threadId)}.new`;
18563
+ try {
18564
+ rmSync(tmp, { force: true });
18565
+ } catch {
18566
+ }
18567
+ let created;
18568
+ try {
18569
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18570
+ created = publishByLink(tmp, file2, data);
18571
+ } finally {
18572
+ try {
18573
+ rmSync(tmp, { force: true });
18574
+ } catch {
18575
+ }
18576
+ }
18577
+ if (created) tightenFile(file2);
18578
+ return created;
18579
+ }
18580
+ var LINK_UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EINVAL"]);
18581
+ function publishByLink(tmp, file2, data) {
18582
+ try {
18583
+ linkSync(tmp, file2);
18584
+ return true;
18585
+ } catch (err) {
18586
+ const code = err.code;
18587
+ if (code === "EEXIST") return false;
18588
+ if (!LINK_UNSUPPORTED.has(code ?? "")) throw err;
18589
+ }
18590
+ try {
18591
+ writeFileSync(file2, data, { mode: DATA_FILE_MODE, flag: "wx" });
18592
+ return true;
18593
+ } catch (err) {
18594
+ if (err.code === "EEXIST") return false;
18595
+ throw err;
18596
+ }
18597
+ }
18598
+
18599
+ // ../../packages/persistence/src/internal/snapshot.ts
18600
+ function backupPath(file2, tag) {
18601
+ return `${file2}.${tag}.${String(Date.now())}.${randomUUID().slice(0, 8)}.bak`;
18602
+ }
18603
+ var STALE_PARTIAL_MS = 5 * 6e4;
18604
+ function reapStalePartials(file2) {
18605
+ const dir = dirname(file2);
18606
+ const prefix = `${basename(file2)}.`;
18607
+ let entries;
18608
+ try {
18609
+ entries = readdirSync(dir);
18610
+ } catch {
18611
+ return;
18612
+ }
18613
+ for (const name of entries) {
18614
+ if (!name.startsWith(prefix) || !name.endsWith(".bak.partial")) continue;
18615
+ const partial2 = join(dir, name);
18616
+ try {
18617
+ if (Date.now() - statSync(partial2).mtimeMs > STALE_PARTIAL_MS) {
18618
+ rmSync2(partial2, { force: true });
18619
+ }
18620
+ } catch {
18621
+ }
18622
+ }
18623
+ }
18624
+ function snapshotStore(db, backup) {
18625
+ const partial2 = `${backup}.partial`;
18626
+ try {
18627
+ rmSync2(partial2, { force: true });
18628
+ db.prepare("VACUUM INTO ?").run(partial2);
18629
+ tightenFile(partial2);
18630
+ renameSync2(partial2, backup);
18631
+ } catch (error51) {
18632
+ try {
18633
+ rmSync2(partial2, { force: true });
18634
+ } catch {
18635
+ }
18636
+ throw error51;
18637
+ }
18638
+ }
18639
+ function moveStoreAside(file2, backup) {
18640
+ const undo = [];
18641
+ renameSync2(file2, backup);
18642
+ undo.push([backup, file2]);
18643
+ try {
18644
+ for (const sidecar of dbSidecars(file2)) {
18645
+ const moved = `${backup}${sidecar.slice(file2.length)}`;
18646
+ try {
18647
+ renameSync2(sidecar, moved);
18648
+ undo.push([moved, sidecar]);
18649
+ } catch {
18650
+ rmSync2(sidecar, { force: true });
18651
+ }
18652
+ }
18653
+ } catch (error51) {
18654
+ for (const [from, to] of undo.reverse()) {
18655
+ try {
18656
+ renameSync2(from, to);
18657
+ } catch {
18658
+ }
18659
+ }
18660
+ throw error51;
18661
+ }
18662
+ tightenPerms(backup);
18663
+ }
18664
+ function discardStore(file2, backup) {
18665
+ try {
18666
+ rmSync2(file2, { force: true });
18667
+ for (const sidecar of dbSidecars(file2)) {
18668
+ rmSync2(sidecar, { force: true });
18669
+ }
18670
+ } catch (error51) {
18671
+ if (existsSync(file2)) {
18672
+ try {
18673
+ rmSync2(backup, { force: true });
18674
+ } catch {
18675
+ }
18676
+ }
18677
+ throw error51;
18678
+ }
18679
+ }
18680
+
17975
18681
  // ../../packages/persistence/src/internal/sql-text.ts
17976
18682
  function escapeLikePattern(s) {
17977
18683
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18113,44 +18819,12 @@ function mapRowsTolerant(rows, map2) {
18113
18819
  return out;
18114
18820
  }
18115
18821
 
18116
- // ../../packages/persistence/src/paths.ts
18117
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18118
- var DATA_DIR_MODE = 448;
18119
- var DATA_FILE_MODE = 384;
18120
- var DB_FILENAME = "aka.db";
18121
- function chmodBestEffort(path, mode) {
18122
- try {
18123
- chmodSync(path, mode);
18124
- } catch {
18125
- }
18822
+ // ../../packages/persistence/src/migrations.ts
18823
+ function describeObject(object2) {
18824
+ return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
18126
18825
  }
18127
- function tightenDir(dir) {
18128
- chmodBestEffort(dir, DATA_DIR_MODE);
18129
- }
18130
- function ensureDataDirSync(dir) {
18131
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18132
- tightenDir(dir);
18133
- }
18134
- function dbSidecars(file2) {
18135
- return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18136
- }
18137
- function tightenFile(file2) {
18138
- try {
18139
- if (lstatSync(file2).isSymbolicLink()) return;
18140
- } catch {
18141
- }
18142
- chmodBestEffort(file2, DATA_FILE_MODE);
18143
- }
18144
- function tightenPerms(file2) {
18145
- for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18146
- }
18147
-
18148
- // ../../packages/persistence/src/migrations.ts
18149
- function describeObject(object2) {
18150
- return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
18151
- }
18152
- function splitStatements(sql) {
18153
- return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
18826
+ function splitStatements(sql) {
18827
+ return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
18154
18828
  }
18155
18829
  function createdIndexName(statement) {
18156
18830
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
@@ -18260,9 +18934,9 @@ function applyLegacyDropMigration(db, file2) {
18260
18934
  }
18261
18935
  }
18262
18936
  function backupBeforeLegacyDrop(db, file2) {
18263
- const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18264
- db.prepare("VACUUM INTO ?").run(backup);
18265
- tightenFile(backup);
18937
+ reapStalePartials(file2);
18938
+ const backup = backupPath(file2, "pre-drop");
18939
+ snapshotStore(db, backup);
18266
18940
  return backup;
18267
18941
  }
18268
18942
  var TOKEN_USAGE_COLUMNS = [
@@ -18606,6 +19280,25 @@ function parseJsonObject(s) {
18606
19280
  return void 0;
18607
19281
  }
18608
19282
 
19283
+ // ../../packages/persistence/src/internal/keyset-cursor.ts
19284
+ function encodeKeysetCursor(payload) {
19285
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
19286
+ }
19287
+ function decodeKeysetCursor(cursor) {
19288
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
19289
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && // `Number.isInteger`, not `typeof === 'number'`. Every timestamp this
19290
+ // resumes from is epoch millis, and a payload carrying ±Infinity or a
19291
+ // fraction binds cleanly rather than failing — returning an EMPTY page with
19292
+ // a null cursor, which a caller reads as "end of list". That is the one
19293
+ // outcome a cursor that does not decode must never produce, since the
19294
+ // documented behaviour above is to restart from the top. (`1e999` is valid
19295
+ // JSON and parses to Infinity; a bare `NaN` is not, so it cannot arrive.)
19296
+ Number.isInteger(parsed.startedAtMs) && typeof parsed.id === "string") {
19297
+ return parsed;
19298
+ }
19299
+ return null;
19300
+ }
19301
+
18609
19302
  // ../../packages/persistence/src/repositories/activity.ts
18610
19303
  var DAY_MS = 864e5;
18611
19304
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18651,16 +19344,6 @@ function utcWindow(nowMs) {
18651
19344
  const startMs = Math.floor(nowMs / DAY_MS) * DAY_MS;
18652
19345
  return { startMs, endMs: startMs + DAY_MS };
18653
19346
  }
18654
- function encodeCursor(payload) {
18655
- return Buffer.from(JSON.stringify(payload)).toString("base64url");
18656
- }
18657
- function decodeCursor(cursor) {
18658
- const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18659
- if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18660
- return parsed;
18661
- }
18662
- return null;
18663
- }
18664
19347
  var DB_EVENT_TYPE_TO_KIND = {
18665
19348
  session: "session",
18666
19349
  prompt: "prompt",
@@ -18805,7 +19488,7 @@ var SqliteActivityRepository = class {
18805
19488
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18806
19489
  }
18807
19490
  listSessions(query) {
18808
- const cursor = query.cursor ? decodeCursor(query.cursor) : null;
19491
+ const cursor = query.cursor ? decodeKeysetCursor(query.cursor) : null;
18809
19492
  const toMs = query.to ? isoToEpochMillis(query.to) : this.now();
18810
19493
  const fromMs = query.from ? isoToEpochMillis(query.from) : void 0;
18811
19494
  const conditions = [SESSION_ROOT];
@@ -18879,7 +19562,7 @@ var SqliteActivityRepository = class {
18879
19562
  )
18880
19563
  );
18881
19564
  const last = page[page.length - 1];
18882
- const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
19565
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18883
19566
  return Promise.resolve({ items, nextCursor, emptyCount });
18884
19567
  }
18885
19568
  getSession(sessionId) {
@@ -19752,7 +20435,7 @@ var SqliteEventsRepository = class {
19752
20435
  };
19753
20436
 
19754
20437
  // ../../packages/persistence/src/repositories/exceptions.ts
19755
- import { randomUUID } from "crypto";
20438
+ import { randomUUID as randomUUID2 } from "crypto";
19756
20439
 
19757
20440
  // ../../packages/persistence/src/internal/sqlite-errors.ts
19758
20441
  var SQLITE_CONSTRAINT_UNIQUE = 2067;
@@ -19784,9 +20467,13 @@ var AmbiguousExceptionIdError = class extends Error {
19784
20467
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19785
20468
  AND (expires_at IS NULL OR expires_at > :now)
19786
20469
  AND (max_uses IS NULL OR use_count < max_uses)`;
20470
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
20471
+ AND conditions IS NULL
20472
+ AND ${ACTIVE_PREDICATE}`;
19787
20473
  var SqliteExceptionsRepository = class {
19788
- constructor(db) {
20474
+ constructor(db, now = () => Date.now()) {
19789
20475
  this.db = db;
20476
+ this.now = now;
19790
20477
  this.consumeStmt = db.prepare(
19791
20478
  `UPDATE exceptions
19792
20479
  SET use_count = use_count + 1, last_used_at = :now, updated_at = :now
@@ -19804,6 +20491,7 @@ var SqliteExceptionsRepository = class {
19804
20491
  );
19805
20492
  }
19806
20493
  db;
20494
+ now;
19807
20495
  consumeStmt;
19808
20496
  insertBlockedStmt;
19809
20497
  sweepBlockedStmt;
@@ -19830,8 +20518,8 @@ var SqliteExceptionsRepository = class {
19830
20518
  "provider conditions are not supported yet \u2014 a grant with one would never apply"
19831
20519
  );
19832
20520
  }
19833
- const id = randomUUID();
19834
- const now = Date.now();
20521
+ const id = randomUUID2();
20522
+ const now = this.now();
19835
20523
  try {
19836
20524
  this.insertExceptionRow(id, input, now);
19837
20525
  } catch (err) {
@@ -19875,11 +20563,11 @@ var SqliteExceptionsRepository = class {
19875
20563
  this.db.prepare(
19876
20564
  `INSERT INTO exceptions (
19877
20565
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19878
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19879
- conditions, created_by, created_via, created_at, updated_at
20566
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20567
+ justification, conditions, created_by, created_via, created_at, updated_at
19880
20568
  ) VALUES (
19881
20569
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19882
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20570
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19883
20571
  :conditions, :createdBy, :createdVia, :now, :now
19884
20572
  )`
19885
20573
  ).run({
@@ -19889,6 +20577,7 @@ var SqliteExceptionsRepository = class {
19889
20577
  valueFingerprint: input.valueFingerprint,
19890
20578
  keyVersion: input.keyVersion,
19891
20579
  maskedValue: input.maskedValue,
20580
+ capability: input.capability ?? "suppress",
19892
20581
  scope: input.scope,
19893
20582
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19894
20583
  maxUses: input.maxUses,
@@ -19908,7 +20597,7 @@ var SqliteExceptionsRepository = class {
19908
20597
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19909
20598
  const rows = allRows(
19910
20599
  this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19911
- opts?.includeTerminal ? {} : { now: Date.now() }
20600
+ opts?.includeTerminal ? {} : { now: this.now() }
19912
20601
  );
19913
20602
  const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19914
20603
  return Promise.resolve(exceptions);
@@ -19943,7 +20632,7 @@ var SqliteExceptionsRepository = class {
19943
20632
  * already revoked.
19944
20633
  */
19945
20634
  revoke(id, revokedBy, reason) {
19946
- const now = Date.now();
20635
+ const now = this.now();
19947
20636
  const result = this.db.prepare(
19948
20637
  `UPDATE exceptions
19949
20638
  SET revoked_at = :now, revoked_by = :revokedBy, revoke_reason = :reason, updated_at = :now
@@ -19957,7 +20646,7 @@ var SqliteExceptionsRepository = class {
19957
20646
  * callers must treat identically — means it does not and the detection is
19958
20647
  * enforced as usual. Deliberately NOT wrapped in try/catch.
19959
20648
  */
19960
- consume(id, now = Date.now()) {
20649
+ consume(id, now = this.now()) {
19961
20650
  const result = this.consumeStmt.run({ id, now });
19962
20651
  return Promise.resolve(Number(result.changes) === 1);
19963
20652
  }
@@ -19966,7 +20655,7 @@ var SqliteExceptionsRepository = class {
19966
20655
  * version — what rides the policy bundle to the hook. Grants written under
19967
20656
  * a different (rotated-away) key never match, so they are excluded at read.
19968
20657
  */
19969
- activeBundleEntries(keyVersion, now = Date.now()) {
20658
+ activeBundleEntries(keyVersion, now = this.now()) {
19970
20659
  const rows = allRows(
19971
20660
  this.db.prepare(
19972
20661
  `SELECT * FROM exceptions
@@ -19982,6 +20671,7 @@ var SqliteExceptionsRepository = class {
19982
20671
  ruleId: row.rule_id,
19983
20672
  valueFingerprint: row.value_fingerprint,
19984
20673
  keyVersion: row.key_version,
20674
+ capability: row.capability,
19985
20675
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19986
20676
  maxUses: row.max_uses,
19987
20677
  useCount: row.use_count,
@@ -19997,7 +20687,7 @@ var SqliteExceptionsRepository = class {
19997
20687
  * than the retention window on every write, so the ledger self-limits.
19998
20688
  */
19999
20689
  recordBlocked(entry) {
20000
- const now = Date.now();
20690
+ const now = this.now();
20001
20691
  this.sweepBlockedStmt.run({ cutoff: now - BLOCKED_DETECTIONS_RETENTION_MS });
20002
20692
  this.insertBlockedStmt.run({
20003
20693
  reference: entry.reference,
@@ -20020,7 +20710,7 @@ var SqliteExceptionsRepository = class {
20020
20710
  WHERE blocked_at > :cutoff
20021
20711
  ORDER BY blocked_at DESC, rowid DESC`
20022
20712
  ),
20023
- { cutoff: Date.now() - windowMs }
20713
+ { cutoff: this.now() - windowMs }
20024
20714
  );
20025
20715
  return Promise.resolve(
20026
20716
  rows.map((row) => ({
@@ -20036,6 +20726,36 @@ var SqliteExceptionsRepository = class {
20036
20726
  }))
20037
20727
  );
20038
20728
  }
20729
+ /**
20730
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20731
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20732
+ * suppression uses — plus the capability: a suppression grant must never
20733
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20734
+ * revealed value re-enters the detection scan immediately afterward and the
20735
+ * suppression match there claims the use — one crossing, one use.
20736
+ *
20737
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20738
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20739
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20740
+ */
20741
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now) {
20742
+ try {
20743
+ const at = now ?? this.now();
20744
+ const row = getRow(
20745
+ this.db.prepare(
20746
+ `SELECT id FROM exceptions
20747
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20748
+ AND key_version = :keyVersion
20749
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20750
+ LIMIT 1`
20751
+ ),
20752
+ { ruleId, valueFingerprint, keyVersion, now: at }
20753
+ );
20754
+ return Promise.resolve(row ?? null);
20755
+ } catch (err) {
20756
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20757
+ }
20758
+ }
20039
20759
  /**
20040
20760
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20041
20761
  * exhausted) whose last transition is older than the retention window.
@@ -20043,7 +20763,7 @@ var SqliteExceptionsRepository = class {
20043
20763
  * predicate, so correctness never depends on this sweep; it only bounds how
20044
20764
  * long the audit evidence is kept locally. Returns the deleted count.
20045
20765
  */
20046
- sweepTerminal(retentionMs, now = Date.now()) {
20766
+ sweepTerminal(retentionMs, now = this.now()) {
20047
20767
  const result = this.db.prepare(
20048
20768
  `DELETE FROM exceptions
20049
20769
  WHERE updated_at < :cutoff
@@ -20063,6 +20783,7 @@ function parseExceptionRow(row) {
20063
20783
  valueFingerprint: row.value_fingerprint,
20064
20784
  keyVersion: row.key_version,
20065
20785
  maskedValue: row.masked_value,
20786
+ capability: row.capability,
20066
20787
  scope: row.scope,
20067
20788
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20068
20789
  maxUses: row.max_uses,
@@ -20105,6 +20826,23 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20105
20826
 
20106
20827
  // ../../packages/persistence/src/repositories/findings.ts
20107
20828
  var PREVIEW_INSTANCES_PER_GROUP = 200;
20829
+ var SCAN_BATCH_ROWS = 1e3;
20830
+ var DEFAULT_LOCATIONS_LIMIT = 100;
20831
+ var LOCATION_RULE_IDS_CAP = 20;
20832
+ function compareLocationOrder(a, b) {
20833
+ return compareFindingGroupOrder(
20834
+ {
20835
+ severity: a.maxSeverity,
20836
+ latestDetectedAt: a.latestDetectedAt,
20837
+ id: ""
20838
+ },
20839
+ {
20840
+ severity: b.maxSeverity,
20841
+ latestDetectedAt: b.latestDetectedAt,
20842
+ id: ""
20843
+ }
20844
+ );
20845
+ }
20108
20846
  var CONCAT_SEP = ",";
20109
20847
  var TUPLE_SEP = "|";
20110
20848
  function splitConcat(value) {
@@ -20117,6 +20855,33 @@ function deriveInstanceStatus(row) {
20117
20855
  latestResolutionStatus: row.latest_status
20118
20856
  });
20119
20857
  }
20858
+ function encodeGroupCursor(group) {
20859
+ const payload = {
20860
+ sev: group.severity,
20861
+ t: group.latestDetectedAt,
20862
+ id: group.id
20863
+ };
20864
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
20865
+ }
20866
+ function decodeGroupCursor(cursor) {
20867
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
20868
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.id === "string") {
20869
+ return {
20870
+ severity: parsed.sev,
20871
+ latestDetectedAt: parsed.t,
20872
+ id: parsed.id
20873
+ };
20874
+ }
20875
+ return null;
20876
+ }
20877
+ function firstAfter(sorted, cursor) {
20878
+ const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
20879
+ return index === -1 ? sorted.length : index;
20880
+ }
20881
+ function findDeepLinked(sorted, page, id) {
20882
+ if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
20883
+ return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
20884
+ }
20120
20885
  var DAY_MS3 = 864e5;
20121
20886
  var SqliteFindingsRepository = class {
20122
20887
  constructor(db) {
@@ -20226,8 +20991,13 @@ var SqliteFindingsRepository = class {
20226
20991
  */
20227
20992
  listGroupedFindings(query) {
20228
20993
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20229
- const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20230
- const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20994
+ const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
20995
+ const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
20996
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}${fromPredicate}`;
20997
+ const sessionParams = {
20998
+ ...query.sessionId ? { sessionId: query.sessionId } : {},
20999
+ ...fromMs === void 0 ? {} : { fromMs }
21000
+ };
20231
21001
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20232
21002
  predicate,
20233
21003
  params: sessionParams
@@ -20235,7 +21005,8 @@ var SqliteFindingsRepository = class {
20235
21005
  const rows = allRows(
20236
21006
  this.db.prepare(
20237
21007
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20238
- occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
21008
+ occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
21009
+ kind, finding_key, latest_status
20239
21010
  FROM (
20240
21011
  SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20241
21012
  d.severity AS severity, f.masked_match AS masked_match,
@@ -20245,6 +21016,7 @@ var SqliteFindingsRepository = class {
20245
21016
  json_extract(e.attributes, '$.repo') AS repo,
20246
21017
  json_extract(e.attributes, '$.file_path') AS file,
20247
21018
  json_extract(e.attributes, '$.tool_name') AS tool_name,
21019
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
20248
21020
  e.event_type AS kind, f.finding_key AS finding_key,
20249
21021
  latest.status AS latest_status,
20250
21022
  ROW_NUMBER() OVER (
@@ -20276,6 +21048,8 @@ var SqliteFindingsRepository = class {
20276
21048
  repo: r.repo ?? "",
20277
21049
  file: r.file ?? "",
20278
21050
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
21051
+ eventId: r.event_id,
21052
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
20279
21053
  status: deriveInstanceStatus(r)
20280
21054
  }));
20281
21055
  const allGroups = buildFindingGroups(groupable, { aggregates });
@@ -20299,18 +21073,23 @@ var SqliteFindingsRepository = class {
20299
21073
  groups: sorted.length
20300
21074
  };
20301
21075
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
21076
+ const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
21077
+ const start = cursor === null ? 0 : firstAfter(sorted, cursor);
21078
+ const page = sorted.slice(start, start + limit);
21079
+ const lastOnPage = page.at(-1);
21080
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
21081
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
20302
21082
  const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20303
- const items = sorted.slice(0, limit).map(
20304
- (g) => statusSet ? {
20305
- ...g,
20306
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20307
- } : g
20308
- );
21083
+ const narrow = (g) => statusSet ? {
21084
+ ...g,
21085
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
21086
+ } : g;
21087
+ const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
20309
21088
  return Promise.resolve({
20310
21089
  totals,
20311
21090
  facets,
20312
21091
  items,
20313
- nextCursor: null,
21092
+ nextCursor,
20314
21093
  ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
20315
21094
  });
20316
21095
  }
@@ -20342,6 +21121,266 @@ var SqliteFindingsRepository = class {
20342
21121
  * request actually carries a `q`. (Substring matching is unaffected by a
20343
21122
  * path repeating across tuples.)
20344
21123
  */
21124
+ /**
21125
+ * The instance-level (flat) findings list: one row per finding, newest first,
21126
+ * paged by keyset.
21127
+ *
21128
+ * SQL owns SCOPE, JS owns every FILTER DIMENSION. The session and the time
21129
+ * bound are SQL predicates: nothing counts them, so narrowing the scan by
21130
+ * them changes no reported number. Severity, subtype, provider, action,
21131
+ * status, tool, repo, file and `q` all stay in JS — each has a facet, and a
21132
+ * facet excludes its own filter, so a row the filter rejects still has to be
21133
+ * counted. Pushing any of them into SQL would silently empty its own facet.
21134
+ * Several could not be expressed there anyway: status comes from the one
21135
+ * shared classifier (deriveFindingStatus), and provider 'api' means "a tool
21136
+ * none of the mappers names", which no IN-list can say.
21137
+ *
21138
+ * The scan runs from the top of the scope on every request, not from the
21139
+ * cursor: `totals` and `facets` describe the whole filtered scope and must not
21140
+ * move as the caller pages. Rows are pulled in batches so memory stays flat
21141
+ * while the counting runs, and only the page itself is retained.
21142
+ */
21143
+ listFindingInstances(query) {
21144
+ const opts = {
21145
+ severity: query.severity,
21146
+ subtype: query.subtype,
21147
+ providers: query.provider,
21148
+ actions: query.action,
21149
+ statuses: query.status,
21150
+ tools: query.tool,
21151
+ repo: query.repo,
21152
+ file: query.file,
21153
+ q: query.q
21154
+ };
21155
+ const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
21156
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
21157
+ const accumulator = createInstanceFacetAccumulator(opts);
21158
+ const items = [];
21159
+ let total = 0;
21160
+ let last;
21161
+ let hasMore = false;
21162
+ for (const row of this.scanFindingRows({
21163
+ sessionId: query.sessionId,
21164
+ from: query.from
21165
+ })) {
21166
+ accumulator.add(row);
21167
+ if (!matchesInstanceFilters(row, opts)) continue;
21168
+ total += 1;
21169
+ if (items.length < limit) {
21170
+ items.push(toInstanceDetail(row));
21171
+ last = row;
21172
+ } else {
21173
+ hasMore = true;
21174
+ }
21175
+ }
21176
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
21177
+ if (cursor !== null) {
21178
+ const resumed = this.pageAfter(cursor, opts, limit, query);
21179
+ return Promise.resolve({
21180
+ totals: { findings: total },
21181
+ facets: accumulator.facets(),
21182
+ items: resumed.items,
21183
+ nextCursor: resumed.nextCursor
21184
+ });
21185
+ }
21186
+ return Promise.resolve({
21187
+ totals: { findings: total },
21188
+ facets: accumulator.facets(),
21189
+ items,
21190
+ nextCursor
21191
+ });
21192
+ }
21193
+ /**
21194
+ * The page of matching rows strictly after `cursor`. Separate from the
21195
+ * counting pass because that one starts at the top of the scope by design;
21196
+ * this one narrows the scan with the same keyset predicate the activity list
21197
+ * uses, so a later page costs less than the first rather than more.
21198
+ */
21199
+ pageAfter(cursor, opts, limit, query) {
21200
+ const items = [];
21201
+ let last;
21202
+ let hasMore = false;
21203
+ for (const row of this.scanFindingRows({
21204
+ sessionId: query.sessionId,
21205
+ from: query.from,
21206
+ after: cursor
21207
+ })) {
21208
+ if (!matchesInstanceFilters(row, opts)) continue;
21209
+ if (items.length < limit) {
21210
+ items.push(toInstanceDetail(row));
21211
+ last = row;
21212
+ } else {
21213
+ hasMore = true;
21214
+ break;
21215
+ }
21216
+ }
21217
+ return {
21218
+ items,
21219
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
21220
+ };
21221
+ }
21222
+ /**
21223
+ * The same findings folded by location: repository, then file within it.
21224
+ *
21225
+ * The grouping keys come from the capturing event's attributes, which is what
21226
+ * the local store relates a finding to — there is no finding↔asset row to
21227
+ * group by instead. A repo or file the event did not record folds into the
21228
+ * empty-string bucket, which the view renders but does not link, since no
21229
+ * filter can name it.
21230
+ */
21231
+ listFindingLocations(query) {
21232
+ const opts = {
21233
+ severity: query.severity,
21234
+ subtype: query.subtype,
21235
+ providers: query.provider,
21236
+ actions: query.action,
21237
+ statuses: query.status,
21238
+ tools: query.tool,
21239
+ q: query.q
21240
+ };
21241
+ const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
21242
+ const byRepo = /* @__PURE__ */ new Map();
21243
+ let total = 0;
21244
+ for (const row of this.scanFindingRows({
21245
+ sessionId: query.sessionId,
21246
+ from: query.from
21247
+ })) {
21248
+ if (!matchesInstanceFilters(row, opts)) continue;
21249
+ total += 1;
21250
+ let files = byRepo.get(row.repo);
21251
+ if (files === void 0) {
21252
+ files = /* @__PURE__ */ new Map();
21253
+ byRepo.set(row.repo, files);
21254
+ }
21255
+ let acc = files.get(row.file);
21256
+ if (acc === void 0) {
21257
+ acc = newLocationAccumulator();
21258
+ files.set(row.file, acc);
21259
+ }
21260
+ addToLocation(acc, row);
21261
+ }
21262
+ let fileCount = 0;
21263
+ const repos = [...byRepo.entries()].map(([repo, files]) => {
21264
+ fileCount += files.size;
21265
+ const fileRows = [...files.entries()].map(([file2, acc]) => ({
21266
+ file: file2,
21267
+ instanceCount: acc.instanceCount,
21268
+ maxSeverity: acc.maxSeverity,
21269
+ latestDetectedAt: acc.latestDetectedAt,
21270
+ ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
21271
+ ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
21272
+ })).sort(compareLocationOrder);
21273
+ const rollup = fileRows.reduce(
21274
+ (a, f) => ({
21275
+ instanceCount: a.instanceCount + f.instanceCount,
21276
+ maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
21277
+ latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
21278
+ }),
21279
+ {
21280
+ instanceCount: 0,
21281
+ maxSeverity: fileRows[0]?.maxSeverity ?? "low",
21282
+ latestDetectedAt: ""
21283
+ }
21284
+ );
21285
+ const statuses = fileRows.map((f) => f.status);
21286
+ const folded = foldGroupStatus(statuses);
21287
+ return {
21288
+ repo,
21289
+ instanceCount: rollup.instanceCount,
21290
+ maxSeverity: rollup.maxSeverity,
21291
+ latestDetectedAt: rollup.latestDetectedAt,
21292
+ ...folded === void 0 ? {} : { status: folded },
21293
+ files: fileRows
21294
+ };
21295
+ });
21296
+ repos.sort(compareLocationOrder);
21297
+ return Promise.resolve({
21298
+ totals: { findings: total, repos: repos.length, files: fileCount },
21299
+ items: repos.slice(0, limit),
21300
+ hasMore: repos.length > limit
21301
+ });
21302
+ }
21303
+ /**
21304
+ * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
21305
+ *
21306
+ * A generator so a caller streams the scope without it ever being an array:
21307
+ * the flat list counts and facets the whole filtered scope, which on a large
21308
+ * store is far more rows than any page. Each batch advances the same keyset
21309
+ * predicate the page read uses, so the scan is a sequence of bounded reads
21310
+ * rather than one unbounded result set.
21311
+ *
21312
+ * The latest-resolution lookup is the CORRELATED form, not the derived table
21313
+ * the grouped path joins: only `status` is needed, idx_finding_resolution_key
21314
+ * makes it a point lookup per row, and the derived table would re-materialize
21315
+ * a window over the whole resolution table once per batch.
21316
+ *
21317
+ * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
21318
+ * would be missing from its own facet, which is computed by excluding that
21319
+ * dimension — see listFindingInstances.
21320
+ */
21321
+ *scanFindingRows(scope) {
21322
+ const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
21323
+ const params = [];
21324
+ if (scope.sessionId !== void 0 && scope.sessionId !== "") {
21325
+ conditions.push("e.root_session_id = ?");
21326
+ params.push(scope.sessionId);
21327
+ }
21328
+ if (scope.from !== void 0) {
21329
+ conditions.push("e.started_at >= ?");
21330
+ params.push(isoToEpochMillis(scope.from));
21331
+ }
21332
+ const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
21333
+ d.severity AS severity, f.masked_match AS masked_match,
21334
+ f.action_taken AS action_taken, f.confidence AS confidence,
21335
+ e.started_at AS occurred_at,
21336
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
21337
+ json_extract(e.attributes, '$.repo') AS repo,
21338
+ json_extract(e.attributes, '$.file_path') AS file,
21339
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
21340
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
21341
+ e.event_type AS kind, f.finding_key AS finding_key,
21342
+ ${latestResolutionStatusSql("f")} AS latest_status
21343
+ FROM inspection_findings f
21344
+ JOIN audit_events e ON e.id = f.audit_event_id
21345
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21346
+ WHERE ${conditions.join(" AND ")}
21347
+ AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
21348
+ ORDER BY e.started_at DESC, f.id DESC
21349
+ LIMIT ?`;
21350
+ let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
21351
+ for (; ; ) {
21352
+ const rows = allRows(this.db.prepare(sql), [
21353
+ ...params,
21354
+ after.startedAtMs,
21355
+ after.startedAtMs,
21356
+ after.id,
21357
+ SCAN_BATCH_ROWS
21358
+ ]);
21359
+ for (const r of rows) {
21360
+ yield {
21361
+ id: r.id,
21362
+ ruleId: r.rule_id,
21363
+ category: r.category,
21364
+ severity: r.severity,
21365
+ maskedMatch: r.masked_match,
21366
+ actionTaken: r.action_taken,
21367
+ confidence: r.confidence,
21368
+ occurredAt: epochMillisToIso(r.occurred_at),
21369
+ sourceTool: r.source_tool,
21370
+ repo: r.repo ?? "",
21371
+ file: r.file ?? "",
21372
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
21373
+ eventId: r.event_id,
21374
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
21375
+ status: deriveInstanceStatus(r)
21376
+ };
21377
+ }
21378
+ if (rows.length < SCAN_BATCH_ROWS) return;
21379
+ const lastRow = rows[rows.length - 1];
21380
+ if (lastRow === void 0) return;
21381
+ after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
21382
+ }
21383
+ }
20345
21384
  groupAggregates(withSearchText, scope) {
20346
21385
  const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20347
21386
  group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
@@ -20602,7 +21641,7 @@ var SqliteInspectionFindingsRepository = class {
20602
21641
  };
20603
21642
 
20604
21643
  // ../../packages/persistence/src/repositories/installed-packs.ts
20605
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
21644
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
20606
21645
 
20607
21646
  // ../../packages/persistence/src/semver.ts
20608
21647
  function parse3(version2) {
@@ -20753,7 +21792,7 @@ var SqliteInstalledPacksRepository = class {
20753
21792
  let behind = false;
20754
21793
  for (const row of rows) {
20755
21794
  const params = {
20756
- id: randomUUID2(),
21795
+ id: randomUUID3(),
20757
21796
  namespace: row.namespace,
20758
21797
  packId: row.packId,
20759
21798
  version: row.version,
@@ -20765,7 +21804,7 @@ var SqliteInstalledPacksRepository = class {
20765
21804
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20766
21805
  this.upsertAvailableStmt.run({
20767
21806
  ...params,
20768
- id: randomUUID2(),
21807
+ id: randomUUID3(),
20769
21808
  recordedBy: meta3?.recordedBy ?? null
20770
21809
  });
20771
21810
  } else {
@@ -21088,14 +22127,15 @@ var SqliteInventoryRepository = class {
21088
22127
  };
21089
22128
 
21090
22129
  // ../../packages/persistence/src/repositories/inventory-assets.ts
21091
- import { randomUUID as randomUUID3 } from "crypto";
22130
+ import { randomUUID as randomUUID4 } from "crypto";
21092
22131
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
21093
22132
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
21094
22133
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
21095
22134
  var HARNESS_LABELS = {
21096
22135
  claudecode: "Claude Code",
21097
22136
  cursor: "Cursor",
21098
- codex: "Codex"
22137
+ codex: "Codex",
22138
+ antigravity: "Antigravity"
21099
22139
  };
21100
22140
  var HARNESS_LIVENESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
21101
22141
  var EMPTY_PROJECT_AGG = {
@@ -21110,6 +22150,7 @@ function resolveHarnessId(attrs, row) {
21110
22150
  if (t.includes("claudecode") || t === "claude") return "claudecode";
21111
22151
  if (t.includes("cursor")) return "cursor";
21112
22152
  if (t.includes("codex")) return "codex";
22153
+ if (t.includes("antigravity")) return "antigravity";
21113
22154
  return null;
21114
22155
  }
21115
22156
  function isLiveRealClaudeCode(rows) {
@@ -21568,7 +22609,7 @@ var SqliteInventoryAssetsRepository = class {
21568
22609
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
21569
22610
  VALUES (:id, :projectId, :path, :access, :now, :now)
21570
22611
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
21571
- ).run({ id: randomUUID3(), projectId, path, access, now: Date.now() });
22612
+ ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
21572
22613
  }
21573
22614
  return true;
21574
22615
  }
@@ -21589,7 +22630,7 @@ var SqliteInventoryAssetsRepository = class {
21589
22630
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
21590
22631
  VALUES (:id, :assetId, :trust, :now, :now)
21591
22632
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
21592
- ).run({ id: randomUUID3(), assetId, trust, now: Date.now() });
22633
+ ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
21593
22634
  }
21594
22635
  this.configRowsCache = void 0;
21595
22636
  return "ok";
@@ -21886,7 +22927,7 @@ var SqliteInventoryAssetsRepository = class {
21886
22927
  };
21887
22928
 
21888
22929
  // ../../packages/persistence/src/repositories/policies.ts
21889
- import { randomUUID as randomUUID4 } from "crypto";
22930
+ import { randomUUID as randomUUID5 } from "crypto";
21890
22931
  var SqlitePoliciesRepository = class {
21891
22932
  constructor(db) {
21892
22933
  this.db = db;
@@ -21921,7 +22962,7 @@ var SqlitePoliciesRepository = class {
21921
22962
  failOpenTransaction(this.db, () => {
21922
22963
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
21923
22964
  stmt.run({
21924
- id: randomUUID4(),
22965
+ id: randomUUID5(),
21925
22966
  target: JSON.stringify({ category }),
21926
22967
  action,
21927
22968
  now: Date.now()
@@ -21941,7 +22982,7 @@ var SqlitePoliciesRepository = class {
21941
22982
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21942
22983
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
21943
22984
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21944
- ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
22985
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
21945
22986
  }
21946
22987
  // Caps every global per-category policy currently set to block/redact down
21947
22988
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -22009,7 +23050,7 @@ var SqlitePolicyCatalogRepository = class {
22009
23050
  };
22010
23051
 
22011
23052
  // ../../packages/persistence/src/repositories/project-files.ts
22012
- import { randomUUID as randomUUID5 } from "crypto";
23053
+ import { randomUUID as randomUUID6 } from "crypto";
22013
23054
  var SqliteProjectFilesRepository = class {
22014
23055
  constructor(db) {
22015
23056
  this.db = db;
@@ -22041,7 +23082,7 @@ var SqliteProjectFilesRepository = class {
22041
23082
  const stamp = Math.max(now, maxStamp + 1);
22042
23083
  for (const file2 of scan2.files) {
22043
23084
  this.upsertStmt.run({
22044
- id: randomUUID5(),
23085
+ id: randomUUID6(),
22045
23086
  projectId,
22046
23087
  path: file2.path,
22047
23088
  name: file2.name,
@@ -22055,7 +23096,7 @@ var SqliteProjectFilesRepository = class {
22055
23096
  };
22056
23097
 
22057
23098
  // ../../packages/persistence/src/repositories/resolutions.ts
22058
- import { randomUUID as randomUUID6 } from "crypto";
23099
+ import { randomUUID as randomUUID7 } from "crypto";
22059
23100
  var SqliteResolutionsRepository = class {
22060
23101
  constructor(db, now = () => Date.now()) {
22061
23102
  this.db = db;
@@ -22109,7 +23150,7 @@ var SqliteResolutionsRepository = class {
22109
23150
  */
22110
23151
  insertResolution(r) {
22111
23152
  this.insertStmt.run({
22112
- id: randomUUID6(),
23153
+ id: randomUUID7(),
22113
23154
  findingKey: r.findingKey,
22114
23155
  status: FindingStatus.parse(r.status),
22115
23156
  method: ResolutionMethod.parse(r.method),
@@ -22168,13 +23209,51 @@ var SqliteRuleProbeCacheRepository = class {
22168
23209
  this.readStmt = db.prepare(
22169
23210
  `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22170
23211
  );
23212
+ this.countQuarantinedStmt = db.prepare(
23213
+ `SELECT COUNT(*) AS n FROM rule_probe_cache WHERE verdict = 'quarantined'`
23214
+ );
23215
+ this.clearQuarantinedStmt = db.prepare(
23216
+ `DELETE FROM rule_probe_cache WHERE verdict = 'quarantined'`
23217
+ );
22171
23218
  }
22172
23219
  db;
22173
23220
  upsertStmt;
22174
23221
  readStmt;
23222
+ countQuarantinedStmt;
23223
+ clearQuarantinedStmt;
22175
23224
  getVerdict(ruleKey) {
22176
23225
  return getRow(this.readStmt, { ruleKey });
22177
23226
  }
23227
+ /** How many rules are currently excluded by a cached quarantine verdict. */
23228
+ countQuarantined() {
23229
+ return getRow(this.countQuarantinedStmt, {})?.n ?? 0;
23230
+ }
23231
+ /**
23232
+ * Forgets every quarantine verdict, so the rules behind them are measured
23233
+ * again on the next load. This is the undo for a verdict the machine reached
23234
+ * on its own: a rule terminated mid-scan is cached forever and dropped from
23235
+ * every later scan, and a timing verdict is a wall-clock judgement that a
23236
+ * loaded or slow machine can reach about a rule that is in fact fine.
23237
+ *
23238
+ * Only 'quarantined' rows go — a 'safe' verdict is a measurement worth
23239
+ * keeping, and dropping it would make every rule pay the battery again.
23240
+ *
23241
+ * Reports `refused` from the write's own result rather than inferring it from
23242
+ * the row count. The two are NOT the same answer: `failOpenTransaction`
23243
+ * swallows a contended DELETE (another writer holding the lock past
23244
+ * `busy_timeout` — reads are unaffected in WAL), and a swallowed refusal
23245
+ * leaves the count unchanged, which is indistinguishable from "there was
23246
+ * nothing to clear". An undo that reports success while the quarantines are
23247
+ * still in place is worse than one that fails, because the rules it claimed
23248
+ * to restore are silently still disabled.
23249
+ */
23250
+ clearQuarantined() {
23251
+ const before = this.countQuarantined();
23252
+ const committed = failOpenTransaction(this.db, () => {
23253
+ this.clearQuarantinedStmt.run();
23254
+ });
23255
+ return { refused: !committed, cleared: before - this.countQuarantined() };
23256
+ }
22178
23257
  setVerdict(ruleKey, verdict, worstProbeMs) {
22179
23258
  failOpenTransaction(this.db, () => {
22180
23259
  this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
@@ -22228,92 +23307,507 @@ var SqliteScanLedgerRepository = class {
22228
23307
  }
22229
23308
  };
22230
23309
 
22231
- // ../../packages/persistence/src/repositories/security.ts
22232
- var DAY_MS4 = 864e5;
22233
- var SEVERITIES = ["critical", "high", "medium", "low"];
22234
- var ACTION_TO_KIND = {
22235
- block: "blocked",
22236
- redact: "redacted",
22237
- warn: "warned"
22238
- };
22239
- var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
22240
- var SCAN_COVERAGE = [
22241
- { provider: "claudecode", coverage: 100, supported: true },
22242
- { provider: "cursor", coverage: 0, supported: false },
22243
- { provider: "codex", coverage: 0, supported: false },
22244
- { provider: "chatgpt", coverage: 0, supported: false },
22245
- { provider: "copilot", coverage: 0, supported: false },
22246
- { provider: "api", coverage: 0, supported: false }
22247
- ];
22248
- var GRANULARITY = {
22249
- "7d": "day",
22250
- "30d": "day",
22251
- "3m": "week",
22252
- "6m": "week"
22253
- };
22254
- function granularityFor(range) {
22255
- return GRANULARITY[range];
22256
- }
22257
- function startOfUtcDay2(ms) {
22258
- return Math.floor(ms / DAY_MS4) * DAY_MS4;
23310
+ // ../../packages/persistence/src/repositories/secret-vault.ts
23311
+ import { randomUUID as randomUUID8 } from "crypto";
23312
+ function pageLimit(requested, fallback) {
23313
+ if (requested === void 0) return fallback;
23314
+ return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
22259
23315
  }
22260
- function toUtcDateString(ms) {
22261
- return new Date(ms).toISOString().slice(0, 10);
23316
+ function encodeReuseCursor(payload) {
23317
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
22262
23318
  }
22263
- function isTimeseriesSeverity(s) {
22264
- return s === "critical" || s === "high" || s === "medium";
23319
+ function decodeReuseCursor(cursor) {
23320
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
23321
+ if (parsed !== void 0 && // `Number.isInteger`, not `typeof === 'number'`: a payload carrying
23322
+ // ±Infinity or a fraction binds cleanly and returns an EMPTY page with a
23323
+ // null cursor, which the caller reads as "end of list" — the one outcome a
23324
+ // malformed cursor must never produce, since restarting from the top is the
23325
+ // documented behaviour and the only recoverable one. (`1e999` is valid JSON
23326
+ // and parses to Infinity; a bare `NaN` is not, so it cannot arrive here.)
23327
+ Number.isInteger(parsed.occurrences) && typeof parsed.pointerId === "string") {
23328
+ return { occurrences: parsed.occurrences, pointerId: parsed.pointerId };
23329
+ }
23330
+ return null;
22265
23331
  }
22266
- var SqliteSecurityRepository = class {
22267
- constructor(db, now = () => Date.now()) {
23332
+ var REUSED_PREDICATE = `(v.occurrence_count > 1
23333
+ OR (SELECT count(*) FROM secret_vault_sighting s WHERE s.pointer_id = v.pointer_id) > 1)`;
23334
+ var INVENTORY_COLUMNS = `v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
23335
+ v.occurrence_count, v.first_seen, v.last_seen`;
23336
+ function toSighting(row) {
23337
+ return {
23338
+ location: row.location,
23339
+ kind: row.kind,
23340
+ firstSeen: new Date(row.first_seen).toISOString(),
23341
+ lastSeen: new Date(row.last_seen).toISOString()
23342
+ };
23343
+ }
23344
+ var SELECT_COLUMNS = `
23345
+ pointer_id AS pointerId,
23346
+ value_fingerprint AS valueFingerprint,
23347
+ fingerprint_key_version AS fingerprintKeyVersion,
23348
+ key_version AS keyVersion,
23349
+ format_version AS formatVersion,
23350
+ category,
23351
+ rule_id AS ruleId,
23352
+ masked_match AS maskedMatch,
23353
+ provider,
23354
+ ciphertext,
23355
+ nonce,
23356
+ auth_tag AS authTag,
23357
+ occurrence_count AS occurrenceCount,
23358
+ first_seen AS firstSeen,
23359
+ last_seen AS lastSeen`;
23360
+ function toRow(raw) {
23361
+ const { provider, ...rest } = raw;
23362
+ return provider === null ? rest : { ...rest, provider };
23363
+ }
23364
+ var SqliteSecretVaultRepository = class {
23365
+ constructor(db) {
22268
23366
  this.db = db;
22269
- this.now = now;
23367
+ this.insertStmt = db.prepare(
23368
+ `INSERT INTO secret_vault (
23369
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
23370
+ format_version, category, rule_id, masked_match, provider,
23371
+ ciphertext, nonce, auth_tag,
23372
+ occurrence_count, first_seen, last_seen
23373
+ ) VALUES (
23374
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
23375
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
23376
+ :ciphertext, :nonce, :authTag,
23377
+ 1, :now, :now
23378
+ )`
23379
+ );
23380
+ this.bumpStmt = db.prepare(
23381
+ `UPDATE secret_vault
23382
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
23383
+ WHERE value_fingerprint = :valueFingerprint`
23384
+ );
23385
+ this.byPointerStmt = db.prepare(
23386
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
23387
+ );
23388
+ this.byFingerprintStmt = db.prepare(
23389
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
23390
+ );
23391
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
23392
+ this.replaceCiphertextStmt = db.prepare(
23393
+ `UPDATE secret_vault
23394
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
23395
+ WHERE pointer_id = :pointerId`
23396
+ );
23397
+ this.refreshFingerprintStmt = db.prepare(
23398
+ `UPDATE secret_vault
23399
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
23400
+ WHERE pointer_id = :pointerId`
23401
+ );
23402
+ this.derefStmt = db.prepare(
23403
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
23404
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
23405
+ );
22270
23406
  }
22271
23407
  db;
22272
- now;
22273
- // Status-aware: every finding is classified by origin (its parent event's
22274
- // kind — 'code_change' is at-rest, everything else is in-flight) and, for
22275
- // at-rest findings, whether its finding_key's LATEST finding_resolution row
22276
- // (max created_at, not "does ANY row exist") has status 'resolved' — mirrors
22277
- // SqliteResolutionsRepository's LATEST-RESOLUTION-WINS convention. "Any row
22278
- // exists" would let a fixed-at-source key that is later redetected (the same
22279
- // secret re-added) stay silently "caught" forever under its stale resolved
22280
- // row; latest-wins lets the scanner supersede it with a fresh status:'open'
22281
- // row (see scan.ts's reopenRedetectedFindings) so the invariant holds: a
22282
- // finding_key present in the current scan is OPEN, regardless of history.
22283
- // In-flight findings are born caught (enforcement already ran); at-rest
22284
- // findings are caught only once their latest disposition is resolved,
22285
- // otherwise they are open-at-rest.
22286
- //
22287
- // NOTE for future manual-resolution writers: only latest status
22288
- // 'resolved' counts as caught above. When acknowledged/dismissed/
22289
- // false-positive manual dispositions land, this must keep filtering by
22290
- // status/method — 'acknowledged' is accepted risk, not a fix, and must NOT
22291
- // be bucketed as caught alongside 'resolved'.
22292
- //
22293
- // Legacy at-rest findings from pre-branch scans carry finding_key = NULL —
22294
- // the resolution lifecycle is keyed by finding_key, so it can never attach a
22295
- // disposition to (or clear) one of these on re-scan. They are excluded from
22296
- // both caught and openAtRest (untracked, not "needs remediation forever"),
22297
- // but still counted in total/count below — this keeps this predicate
22298
- // consistent with SqliteResolutionsRepository.openAtRestKeysForPath, which
22299
- // already filters `finding_key IS NOT NULL`.
22300
- //
22301
- // One GROUP BY aggregate: the result set stays O(distinct severities) no
22302
- // matter how many findings the store has accumulated (this backs `aka stats`
22303
- // and the dashboard severity card, both hot paths on a table that only
22304
- // grows). The latest-resolution status comes from the shared derived-table
22305
- // fragment (see resolution-sql.ts) rather than a correlated subquery per
22306
- // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
22307
- // double-counting a key that accumulated several append-only rows.
22308
- severitySummary() {
22309
- const rows = allRows(
22310
- this.db.prepare(
22311
- `SELECT d.severity AS severity,
22312
- COUNT(*) AS count,
22313
- SUM(CASE
22314
- WHEN e.event_type != 'code_change' THEN 1
22315
- WHEN f.finding_key IS NULL THEN 0
22316
- WHEN latest.status = 'resolved' THEN 1
23408
+ insertStmt;
23409
+ bumpStmt;
23410
+ byPointerStmt;
23411
+ byFingerprintStmt;
23412
+ listStmt;
23413
+ replaceCiphertextStmt;
23414
+ refreshFingerprintStmt;
23415
+ derefStmt;
23416
+ /**
23417
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
23418
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
23419
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
23420
+ * pointer, category and ciphertext, so the same secret always resolves to one
23421
+ * wire token. `minted` is true only when this call created the row.
23422
+ *
23423
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
23424
+ * writers cannot both decide they are minting.
23425
+ */
23426
+ upsert(input, now) {
23427
+ let minted = false;
23428
+ withTransaction(
23429
+ this.db,
23430
+ () => {
23431
+ const existing = getRow(this.byFingerprintStmt, {
23432
+ valueFingerprint: input.valueFingerprint
23433
+ });
23434
+ if (existing === void 0) {
23435
+ this.insertStmt.run(
23436
+ bindParams({
23437
+ pointerId: input.pointerId,
23438
+ valueFingerprint: input.valueFingerprint,
23439
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
23440
+ keyVersion: input.keyVersion,
23441
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
23442
+ category: input.category,
23443
+ ruleId: input.ruleId,
23444
+ maskedMatch: input.maskedMatch,
23445
+ provider: input.provider,
23446
+ ciphertext: input.ciphertext,
23447
+ nonce: input.nonce,
23448
+ authTag: input.authTag,
23449
+ now
23450
+ })
23451
+ );
23452
+ minted = true;
23453
+ return;
23454
+ }
23455
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
23456
+ },
23457
+ "IMMEDIATE"
23458
+ );
23459
+ const row = getRow(this.byFingerprintStmt, {
23460
+ valueFingerprint: input.valueFingerprint
23461
+ });
23462
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
23463
+ return { row: toRow(row), minted };
23464
+ }
23465
+ byPointerId(pointerId) {
23466
+ const raw = getRow(this.byPointerStmt, { pointerId });
23467
+ return raw === void 0 ? null : toRow(raw);
23468
+ }
23469
+ byValueFingerprint(fingerprint) {
23470
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
23471
+ return raw === void 0 ? null : toRow(raw);
23472
+ }
23473
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
23474
+ recordDeref(entry) {
23475
+ this.derefStmt.run(
23476
+ bindParams({
23477
+ id: entry.id,
23478
+ pointerId: entry.pointerId,
23479
+ at: entry.at,
23480
+ target: entry.target,
23481
+ reason: entry.reason,
23482
+ outcome: entry.outcome,
23483
+ grantId: entry.grantId,
23484
+ pointerCount: entry.pointerCount ?? 1
23485
+ })
23486
+ );
23487
+ }
23488
+ listAll() {
23489
+ return allRows(this.listStmt).map(toRow);
23490
+ }
23491
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
23492
+ replaceCiphertext(pointerId, next) {
23493
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
23494
+ }
23495
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
23496
+ refreshFingerprint(pointerId, next) {
23497
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
23498
+ }
23499
+ /**
23500
+ * Destroy every vaulted value and report how many were destroyed. The deref
23501
+ * audit is left alone on purpose — see the table note above.
23502
+ */
23503
+ purgeAll() {
23504
+ let destroyed = 0;
23505
+ withTransaction(
23506
+ this.db,
23507
+ () => {
23508
+ destroyed = this.countEntries();
23509
+ this.db.exec("DELETE FROM secret_vault");
23510
+ },
23511
+ "IMMEDIATE"
23512
+ );
23513
+ return destroyed;
23514
+ }
23515
+ /**
23516
+ * Record (or re-stamp) one place a pointer has been written. One row per
23517
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
23518
+ * on hook paths — a failure must never affect the rewrite that triggered it,
23519
+ * so callers wrap this, not the other way around.
23520
+ */
23521
+ recordSighting(entry, now) {
23522
+ this.db.prepare(
23523
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
23524
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
23525
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
23526
+ ).run({
23527
+ id: randomUUID8(),
23528
+ pointerId: entry.pointerId,
23529
+ location: entry.location,
23530
+ kind: entry.kind,
23531
+ now
23532
+ });
23533
+ }
23534
+ /**
23535
+ * Sightings for a whole page of pointers, in ONE query grouped in JS rather
23536
+ * than one query per row. A pointer with no sightings still gets an entry, so
23537
+ * the caller never has to distinguish "none" from "missing".
23538
+ *
23539
+ * The `IN` list is sized to the page, so this statement cannot be cached on
23540
+ * the instance the way the fixed-shape ones in the constructor are.
23541
+ */
23542
+ sightingsFor(pointerIds) {
23543
+ const byPointer = new Map(pointerIds.map((id) => [id, []]));
23544
+ if (pointerIds.length === 0) return byPointer;
23545
+ const rows = allRows(
23546
+ this.db.prepare(
23547
+ `SELECT pointer_id, location, kind, first_seen, last_seen
23548
+ FROM secret_vault_sighting
23549
+ WHERE pointer_id IN (${placeholders(pointerIds.length)})
23550
+ ORDER BY last_seen DESC`
23551
+ ),
23552
+ pointerIds
23553
+ );
23554
+ for (const row of rows) byPointer.get(row.pointer_id)?.push(toSighting(row));
23555
+ return byPointer;
23556
+ }
23557
+ /** Hydrate a page of raw inventory rows with their sightings, batched. */
23558
+ toInventoryEntries(rows) {
23559
+ const sightings = this.sightingsFor(rows.map((r) => r.pointer_id));
23560
+ return rows.map((r) => ({
23561
+ pointerId: r.pointer_id,
23562
+ category: r.category,
23563
+ ...r.provider === null ? {} : { provider: r.provider },
23564
+ maskedMatch: r.masked_match,
23565
+ occurrences: r.occurrence_count,
23566
+ firstSeen: new Date(r.first_seen).toISOString(),
23567
+ lastSeen: new Date(r.last_seen).toISOString(),
23568
+ revealGrantId: r.grant_id,
23569
+ sightings: sightings.get(r.pointer_id) ?? []
23570
+ }));
23571
+ }
23572
+ /**
23573
+ * The dashboard inventory, newest-first, ONE PAGE at a time: every vaulted
23574
+ * value's descriptor data joined with its sightings and the active
23575
+ * reveal-to-model grant when one exists. Raw-free by construction — neither
23576
+ * the fingerprint nor the ciphertext columns are selected.
23577
+ *
23578
+ * `totals.values` counts the whole store, not the page, so the count a reader
23579
+ * sees never depends on how far they have paged.
23580
+ */
23581
+ listInventory(query = {}, now = Date.now()) {
23582
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23583
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23584
+ const where = cursor === null ? "" : "WHERE (v.last_seen < :cursorLastSeen OR (v.last_seen = :cursorLastSeen AND v.pointer_id < :cursorPointerId))";
23585
+ const rows = allRows(
23586
+ this.db.prepare(
23587
+ `SELECT ${INVENTORY_COLUMNS},
23588
+ (SELECT e.id FROM exceptions e
23589
+ WHERE e.rule_id = v.rule_id
23590
+ AND e.value_fingerprint = v.value_fingerprint
23591
+ AND e.key_version = v.fingerprint_key_version
23592
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23593
+ LIMIT 1) AS grant_id
23594
+ FROM secret_vault v
23595
+ ${where}
23596
+ ORDER BY v.last_seen DESC, v.pointer_id DESC
23597
+ LIMIT :limit`
23598
+ ),
23599
+ bindParams({
23600
+ now,
23601
+ limit: limit + 1,
23602
+ ...cursor === null ? {} : { cursorLastSeen: cursor.startedAtMs, cursorPointerId: cursor.id }
23603
+ })
23604
+ );
23605
+ const hasMore = rows.length > limit;
23606
+ const page = hasMore ? rows.slice(0, limit) : rows;
23607
+ const last = page[page.length - 1];
23608
+ return {
23609
+ totals: { values: this.countEntries() },
23610
+ items: this.toInventoryEntries(page),
23611
+ // Minted from the last row of the PAGE, never the extra probe row.
23612
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.last_seen, id: last.pointer_id }) : null
23613
+ };
23614
+ }
23615
+ /**
23616
+ * Values reused on this machine — detected more than once, or written to more
23617
+ * than one location — most-reused first, one page at a time.
23618
+ *
23619
+ * Its own read rather than a filter over an inventory page: reuse is a
23620
+ * property of the whole store, and deriving it from 50 newest rows would
23621
+ * under-report exactly the values a reader most needs to see.
23622
+ */
23623
+ listReuse(query = {}, now = Date.now()) {
23624
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_INVENTORY_LIMIT);
23625
+ const cursor = query.cursor === void 0 ? null : decodeReuseCursor(query.cursor);
23626
+ const after = cursor === null ? "" : `AND (v.occurrence_count < :cursorOccurrences
23627
+ OR (v.occurrence_count = :cursorOccurrences AND v.pointer_id < :cursorPointerId))`;
23628
+ const rows = allRows(
23629
+ this.db.prepare(
23630
+ `SELECT ${INVENTORY_COLUMNS},
23631
+ (SELECT e.id FROM exceptions e
23632
+ WHERE e.rule_id = v.rule_id
23633
+ AND e.value_fingerprint = v.value_fingerprint
23634
+ AND e.key_version = v.fingerprint_key_version
23635
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
23636
+ LIMIT 1) AS grant_id
23637
+ FROM secret_vault v
23638
+ WHERE ${REUSED_PREDICATE} ${after}
23639
+ ORDER BY v.occurrence_count DESC, v.pointer_id DESC
23640
+ LIMIT :limit`
23641
+ ),
23642
+ bindParams({
23643
+ now,
23644
+ limit: limit + 1,
23645
+ ...cursor === null ? {} : { cursorOccurrences: cursor.occurrences, cursorPointerId: cursor.pointerId }
23646
+ })
23647
+ );
23648
+ const hasMore = rows.length > limit;
23649
+ const page = hasMore ? rows.slice(0, limit) : rows;
23650
+ const last = page[page.length - 1];
23651
+ return {
23652
+ totals: { reused: this.countReused() },
23653
+ items: this.toInventoryEntries(page),
23654
+ nextCursor: hasMore && last ? encodeReuseCursor({ occurrences: last.occurrence_count, pointerId: last.pointer_id }) : null
23655
+ };
23656
+ }
23657
+ /**
23658
+ * The de-reference trail, newest first, one page at a time. By default the
23659
+ * batched, high-volume reasons (display, view-render) are hidden and counted
23660
+ * instead — the rows that matter as a signal are the model crossings, and
23661
+ * burying them under render noise would defeat the audit's purpose.
23662
+ *
23663
+ * `hiddenBatched` counts the whole trail rather than the page: it is what the
23664
+ * view's "N hidden" line and its toggle speak for, so it must not shrink as
23665
+ * the reader pages.
23666
+ */
23667
+ listDerefs(query = {}) {
23668
+ const limit = pageLimit(query.limit, DEFAULT_VAULT_DEREFS_LIMIT);
23669
+ const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
23670
+ const conditions = [];
23671
+ if (query.includeBatched !== true) {
23672
+ conditions.push(`reason NOT IN ('display', 'view-render')`);
23673
+ }
23674
+ if (cursor !== null) {
23675
+ conditions.push("(at < :cursorAt OR (at = :cursorAt AND id < :cursorId))");
23676
+ }
23677
+ const where = conditions.length === 0 ? "" : `WHERE ${conditions.join(" AND ")}`;
23678
+ const rows = allRows(
23679
+ this.db.prepare(
23680
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
23681
+ FROM secret_vault_deref ${where}
23682
+ ORDER BY at DESC, id DESC LIMIT :limit`
23683
+ ),
23684
+ bindParams({
23685
+ limit: limit + 1,
23686
+ ...cursor === null ? {} : { cursorAt: cursor.startedAtMs, cursorId: cursor.id }
23687
+ })
23688
+ );
23689
+ const hasMore = rows.length > limit;
23690
+ const page = hasMore ? rows.slice(0, limit) : rows;
23691
+ const last = page[page.length - 1];
23692
+ const hiddenBatched = query.includeBatched === true ? 0 : countScalar(
23693
+ this.db,
23694
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
23695
+ );
23696
+ return {
23697
+ items: page.map((r) => ({
23698
+ id: r.id,
23699
+ pointerId: r.pointer_id,
23700
+ at: new Date(r.at).toISOString(),
23701
+ target: r.target,
23702
+ reason: r.reason,
23703
+ outcome: r.outcome,
23704
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
23705
+ pointerCount: r.pointer_count
23706
+ })),
23707
+ nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: last.at, id: last.id }) : null,
23708
+ hiddenBatched
23709
+ };
23710
+ }
23711
+ countEntries() {
23712
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
23713
+ }
23714
+ /** Values reused on this machine — the reuse list's page-independent total. */
23715
+ countReused() {
23716
+ return countScalar(
23717
+ this.db,
23718
+ `SELECT COUNT(*) AS n FROM secret_vault v WHERE ${REUSED_PREDICATE}`
23719
+ );
23720
+ }
23721
+ };
23722
+
23723
+ // ../../packages/persistence/src/repositories/security.ts
23724
+ var DAY_MS4 = 864e5;
23725
+ var SEVERITIES = ["critical", "high", "medium", "low"];
23726
+ var ACTION_TO_KIND = {
23727
+ block: "blocked",
23728
+ redact: "redacted",
23729
+ warn: "warned"
23730
+ };
23731
+ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
23732
+ var SCAN_COVERAGE = [
23733
+ { provider: "claudecode", coverage: 100, supported: true },
23734
+ { provider: "cursor", coverage: 0, supported: false },
23735
+ { provider: "codex", coverage: 80, supported: true },
23736
+ { provider: "antigravity", coverage: 60, supported: true },
23737
+ { provider: "claudeai", coverage: 0, supported: false },
23738
+ { provider: "chatgpt", coverage: 0, supported: false },
23739
+ { provider: "copilot", coverage: 0, supported: false },
23740
+ { provider: "api", coverage: 0, supported: false }
23741
+ ];
23742
+ var GRANULARITY = {
23743
+ "7d": "day",
23744
+ "30d": "day",
23745
+ "3m": "week",
23746
+ "6m": "week"
23747
+ };
23748
+ function granularityFor(range) {
23749
+ return GRANULARITY[range];
23750
+ }
23751
+ function startOfUtcDay2(ms) {
23752
+ return Math.floor(ms / DAY_MS4) * DAY_MS4;
23753
+ }
23754
+ function toUtcDateString(ms) {
23755
+ return new Date(ms).toISOString().slice(0, 10);
23756
+ }
23757
+ function isTimeseriesSeverity(s) {
23758
+ return s === "critical" || s === "high" || s === "medium";
23759
+ }
23760
+ var SqliteSecurityRepository = class {
23761
+ constructor(db, now = () => Date.now()) {
23762
+ this.db = db;
23763
+ this.now = now;
23764
+ }
23765
+ db;
23766
+ now;
23767
+ // Status-aware: every finding is classified by origin (its parent event's
23768
+ // kind — 'code_change' is at-rest, everything else is in-flight) and, for
23769
+ // at-rest findings, whether its finding_key's LATEST finding_resolution row
23770
+ // (max created_at, not "does ANY row exist") has status 'resolved' — mirrors
23771
+ // SqliteResolutionsRepository's LATEST-RESOLUTION-WINS convention. "Any row
23772
+ // exists" would let a fixed-at-source key that is later redetected (the same
23773
+ // secret re-added) stay silently "caught" forever under its stale resolved
23774
+ // row; latest-wins lets the scanner supersede it with a fresh status:'open'
23775
+ // row (see scan.ts's reopenRedetectedFindings) so the invariant holds: a
23776
+ // finding_key present in the current scan is OPEN, regardless of history.
23777
+ // In-flight findings are born caught (enforcement already ran); at-rest
23778
+ // findings are caught only once their latest disposition is resolved,
23779
+ // otherwise they are open-at-rest.
23780
+ //
23781
+ // NOTE for future manual-resolution writers: only latest status
23782
+ // 'resolved' counts as caught above. When acknowledged/dismissed/
23783
+ // false-positive manual dispositions land, this must keep filtering by
23784
+ // status/method — 'acknowledged' is accepted risk, not a fix, and must NOT
23785
+ // be bucketed as caught alongside 'resolved'.
23786
+ //
23787
+ // Legacy at-rest findings from pre-branch scans carry finding_key = NULL —
23788
+ // the resolution lifecycle is keyed by finding_key, so it can never attach a
23789
+ // disposition to (or clear) one of these on re-scan. They are excluded from
23790
+ // both caught and openAtRest (untracked, not "needs remediation forever"),
23791
+ // but still counted in total/count below — this keeps this predicate
23792
+ // consistent with SqliteResolutionsRepository.openAtRestKeysForPath, which
23793
+ // already filters `finding_key IS NOT NULL`.
23794
+ //
23795
+ // One GROUP BY aggregate: the result set stays O(distinct severities) no
23796
+ // matter how many findings the store has accumulated (this backs `aka stats`
23797
+ // and the dashboard severity card, both hot paths on a table that only
23798
+ // grows). The latest-resolution status comes from the shared derived-table
23799
+ // fragment (see resolution-sql.ts) rather than a correlated subquery per
23800
+ // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
23801
+ // double-counting a key that accumulated several append-only rows.
23802
+ severitySummary() {
23803
+ const rows = allRows(
23804
+ this.db.prepare(
23805
+ `SELECT d.severity AS severity,
23806
+ COUNT(*) AS count,
23807
+ SUM(CASE
23808
+ WHEN e.event_type != 'code_change' THEN 1
23809
+ WHEN f.finding_key IS NULL THEN 0
23810
+ WHEN latest.status = 'resolved' THEN 1
22317
23811
  ELSE 0
22318
23812
  END) AS caught,
22319
23813
  SUM(CASE
@@ -22573,7 +24067,7 @@ var SqliteSecurityRepository = class {
22573
24067
  };
22574
24068
 
22575
24069
  // ../../packages/persistence/src/repositories/shares.ts
22576
- import { randomUUID as randomUUID7 } from "crypto";
24070
+ import { randomUUID as randomUUID9 } from "crypto";
22577
24071
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22578
24072
  var IN_CHUNK = 500;
22579
24073
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22829,7 +24323,7 @@ var SqliteSharesRepository = class {
22829
24323
  (id, destination_id, host, decision, created_at, updated_at)
22830
24324
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22831
24325
  ).run({
22832
- id: randomUUID7(),
24326
+ id: randomUUID9(),
22833
24327
  destinationId,
22834
24328
  host: dest.host,
22835
24329
  decision,
@@ -22978,7 +24472,7 @@ var SqliteSharesRepository = class {
22978
24472
  let destinationId = destIds.get(hit.host);
22979
24473
  if (destinationId === void 0) {
22980
24474
  destStmt.run({
22981
- id: randomUUID7(),
24475
+ id: randomUUID9(),
22982
24476
  kind: hit.kind,
22983
24477
  name: hit.name,
22984
24478
  host: hit.host,
@@ -22994,7 +24488,7 @@ var SqliteSharesRepository = class {
22994
24488
  let endpointId = endpointIds.get(endpointKey);
22995
24489
  if (endpointId === void 0) {
22996
24490
  endpointStmt.run({
22997
- id: randomUUID7(),
24491
+ id: randomUUID9(),
22998
24492
  destinationId,
22999
24493
  method: hit.method,
23000
24494
  transport: hit.transport,
@@ -23007,7 +24501,7 @@ var SqliteSharesRepository = class {
23007
24501
  endpointIds.set(endpointKey, endpointId);
23008
24502
  }
23009
24503
  siteStmt.run({
23010
- id: randomUUID7(),
24504
+ id: randomUUID9(),
23011
24505
  endpointId,
23012
24506
  project: input.project,
23013
24507
  projectKey: input.projectKey,
@@ -23372,6 +24866,9 @@ function purgeSampleData(db) {
23372
24866
  }
23373
24867
 
23374
24868
  // ../../packages/persistence/src/database.ts
24869
+ var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
24870
+ "aka.persistence.unsafeTestOnlyRawHandle"
24871
+ );
23375
24872
  function linkHost(input, hostId) {
23376
24873
  return hostId ? { ...input, hostId } : input;
23377
24874
  }
@@ -23393,21 +24890,34 @@ function openWithPragmas(file2) {
23393
24890
  }
23394
24891
  return db;
23395
24892
  }
23396
- function backupLegacyStore(file2) {
23397
- const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23398
- renameSync2(file2, backup);
23399
- tightenFile(backup);
23400
- for (const sidecar of dbSidecars(file2)) {
23401
- if (existsSync(sidecar)) rmSync2(sidecar);
24893
+ function backupLegacyStore(db, file2) {
24894
+ reapStalePartials(file2);
24895
+ const backup = backupPath(file2, "legacy");
24896
+ let snapshotted = false;
24897
+ let snapshotError;
24898
+ try {
24899
+ snapshotStore(db, backup);
24900
+ snapshotted = true;
24901
+ } catch (error51) {
24902
+ snapshotError = error51;
24903
+ } finally {
24904
+ db.close();
23402
24905
  }
24906
+ if (!snapshotted) {
24907
+ akaWarn(
24908
+ `Could not snapshot the incompatible ${DB_FILENAME} (${String(snapshotError)}); moving the store aside with its sidecars instead.`
24909
+ );
24910
+ moveStoreAside(file2, backup);
24911
+ return backup;
24912
+ }
24913
+ discardStore(file2, backup);
23403
24914
  return backup;
23404
24915
  }
23405
24916
  function openAndInitialize(file2) {
23406
24917
  let db = openWithPragmas(file2);
23407
24918
  try {
23408
24919
  if (isForeignSqliteLineage(db)) {
23409
- db.close();
23410
- const backup = backupLegacyStore(file2);
24920
+ const backup = backupLegacyStore(db, file2);
23411
24921
  db = openWithPragmas(file2);
23412
24922
  akaWarn(
23413
24923
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
@@ -23423,6 +24933,7 @@ function openAndInitialize(file2) {
23423
24933
  policies,
23424
24934
  installedPacks,
23425
24935
  scanLedger: new SqliteScanLedgerRepository(db),
24936
+ secretVault: new SqliteSecretVaultRepository(db),
23426
24937
  exceptions: new SqliteExceptionsRepository(db),
23427
24938
  resolutions: new SqliteResolutionsRepository(db),
23428
24939
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23450,7 +24961,7 @@ function openAndInitialize(file2) {
23450
24961
  }
23451
24962
  function openLocalDatabase(dir) {
23452
24963
  ensureDataDirSync(dir);
23453
- const file2 = join(dir, DB_FILENAME);
24964
+ const file2 = join2(dir, DB_FILENAME);
23454
24965
  const {
23455
24966
  db,
23456
24967
  events,
@@ -23458,6 +24969,7 @@ function openLocalDatabase(dir) {
23458
24969
  policies,
23459
24970
  installedPacks,
23460
24971
  scanLedger,
24972
+ secretVault,
23461
24973
  exceptions,
23462
24974
  resolutions,
23463
24975
  ruleProbeCache,
@@ -23566,7 +25078,7 @@ function openLocalDatabase(dir) {
23566
25078
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23567
25079
  if (!definitionId) continue;
23568
25080
  inspectionFindings.insertFinding({
23569
- id: randomUUID8(),
25081
+ id: randomUUID10(),
23570
25082
  auditEventId: record2.scanEvent.id,
23571
25083
  inspectionDefinitionId: definitionId,
23572
25084
  span: finding.span,
@@ -23643,6 +25155,7 @@ function openLocalDatabase(dir) {
23643
25155
  policies,
23644
25156
  installedPacks,
23645
25157
  scanLedger,
25158
+ secretVault,
23646
25159
  exceptions,
23647
25160
  resolutions,
23648
25161
  ruleProbeCache,
@@ -23671,22 +25184,58 @@ function openLocalDatabase(dir) {
23671
25184
  transaction,
23672
25185
  close: () => {
23673
25186
  db.close();
23674
- }
25187
+ },
25188
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
25189
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
23675
25190
  };
23676
25191
  }
23677
25192
 
25193
+ // ../../packages/persistence/src/exception-policy.ts
25194
+ var UserGrantPolicyProvider = class {
25195
+ #exceptions;
25196
+ constructor(exceptions) {
25197
+ this.#exceptions = exceptions;
25198
+ }
25199
+ async decideReveal(identity) {
25200
+ try {
25201
+ const grant = await this.#exceptions.activeRevealGrant(
25202
+ identity.ruleId,
25203
+ identity.valueFingerprint,
25204
+ identity.fingerprintKeyVersion
25205
+ );
25206
+ return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
25207
+ } catch {
25208
+ return { allow: false };
25209
+ }
25210
+ }
25211
+ };
25212
+
25213
+ // ../../packages/persistence/src/file-lock.ts
25214
+ import { randomUUID as randomUUID11 } from "crypto";
25215
+ import {
25216
+ closeSync,
25217
+ existsSync as existsSync2,
25218
+ openSync,
25219
+ readFileSync,
25220
+ rmSync as rmSync3,
25221
+ statSync as statSync2,
25222
+ writeFileSync as writeFileSync2
25223
+ } from "fs";
25224
+ import { hostname as hostname3 } from "os";
25225
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
25226
+
23678
25227
  // ../../packages/persistence/src/finding-key.ts
23679
25228
  import { createHash as createHash3 } from "crypto";
23680
25229
 
23681
25230
  // ../../packages/persistence/src/fingerprint.ts
23682
25231
  import { createHmac, randomBytes } from "crypto";
23683
- import { existsSync as existsSync2, readFileSync } from "fs";
23684
- import { join as join2 } from "path";
25232
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25233
+ import { join as join3 } from "path";
23685
25234
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23686
- var KEY_FILENAME = "exception.key";
25235
+ var EXCEPTION_KEY_FILENAME = "exception.key";
23687
25236
  var KEY_MATERIAL_BYTES = 32;
23688
25237
  function keyFilePath(dataDir2) {
23689
- return join2(dataDir2, KEY_FILENAME);
25238
+ return join3(dataDir2, EXCEPTION_KEY_FILENAME);
23690
25239
  }
23691
25240
  function parseKeyFile(raw) {
23692
25241
  const parsed = JSON.parse(raw);
@@ -23706,33 +25255,120 @@ function parseKeyFile(raw) {
23706
25255
  }
23707
25256
  return { version: version2, material: bytes };
23708
25257
  }
25258
+ var KEY_VERSION_COLUMNS = {
25259
+ exceptions: "key_version",
25260
+ blocked_detections: "key_version",
25261
+ secret_vault: "fingerprint_key_version"
25262
+ };
25263
+ var SQLITE_ERROR = 1;
25264
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
25265
+ var FloorUnreadableError = class extends Error {
25266
+ code = "floor-unreadable";
25267
+ constructor(cause) {
25268
+ super(
25269
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
25270
+ { cause }
25271
+ );
25272
+ this.name = "FloorUnreadableError";
25273
+ }
25274
+ };
25275
+ function storedKeyVersionFloor(dataDir2) {
25276
+ const file2 = join3(dataDir2, DB_FILENAME);
25277
+ if (!existsSync3(file2)) return 0;
25278
+ let db;
25279
+ try {
25280
+ db = new DatabaseSync2(file2, { readOnly: true });
25281
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
25282
+ let floor = 0;
25283
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
25284
+ try {
25285
+ const row = getRow(
25286
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
25287
+ );
25288
+ floor = Math.max(floor, row?.v ?? 0);
25289
+ } catch (err) {
25290
+ if (err.errcode !== SQLITE_ERROR) {
25291
+ throw new FloorUnreadableError(err);
25292
+ }
25293
+ }
25294
+ }
25295
+ return floor;
25296
+ } catch (err) {
25297
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
25298
+ } finally {
25299
+ db?.close();
25300
+ }
25301
+ }
25302
+ function serializeKey(key) {
25303
+ return JSON.stringify({ version: key.version, material: key.material.toString("base64") });
25304
+ }
25305
+ function createKeyFile(dataDir2, key) {
25306
+ ensureDataDirSync(dataDir2);
25307
+ const file2 = keyFilePath(dataDir2);
25308
+ if (createOwnerOnlyFileSync(file2, `${serializeKey(key)}
25309
+ `)) return key;
25310
+ const winner = readFingerprintKey(dataDir2);
25311
+ if (winner) {
25312
+ tightenFile(file2);
25313
+ return winner;
25314
+ }
25315
+ const occupant = classifyOccupant(file2);
25316
+ throw new KeyUnclaimableError(occupantMessage(file2, occupant.kind), occupant.cause);
25317
+ }
25318
+ function occupantMessage(file2, kind) {
25319
+ switch (kind) {
25320
+ case "symlink":
25321
+ return `exception key file is a symlink (${file2}); remove it so a key can be created`;
25322
+ case "gone":
25323
+ return "exception key file was removed while it was being created";
25324
+ case "unknown":
25325
+ return `exception key file (${file2}) is occupied but cannot be inspected; check the permissions on its directory`;
25326
+ }
25327
+ }
23709
25328
  function readFingerprintKey(dataDir2) {
23710
25329
  let raw;
23711
25330
  try {
23712
- raw = readFileSync(keyFilePath(dataDir2), "utf8");
25331
+ raw = readFileSync2(keyFilePath(dataDir2), "utf8");
23713
25332
  } catch (err) {
23714
25333
  if (err.code === "ENOENT") return null;
23715
25334
  throw err instanceof Error ? err : new Error(String(err));
23716
25335
  }
23717
25336
  return parseKeyFile(raw);
23718
25337
  }
25338
+ function loadOrCreateFingerprintKey(dataDir2) {
25339
+ const existing = readFingerprintKey(dataDir2);
25340
+ if (existing) {
25341
+ tightenFile(keyFilePath(dataDir2));
25342
+ return existing;
25343
+ }
25344
+ return createKeyFile(dataDir2, {
25345
+ version: storedKeyVersionFloor(dataDir2) + 1,
25346
+ material: randomBytes(KEY_MATERIAL_BYTES)
25347
+ });
25348
+ }
25349
+ function fingerprintValue(key, raw) {
25350
+ return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
25351
+ }
23719
25352
 
23720
25353
  // ../../packages/persistence/src/local-layout.ts
23721
25354
  import { renameSync as renameSync3 } from "fs";
23722
25355
  import { mkdir } from "fs/promises";
23723
25356
  import { homedir } from "os";
23724
- import { join as join3 } from "path";
25357
+ import { join as join4 } from "path";
23725
25358
  function defaultDataDir() {
23726
- return join3(homedir(), ".aka");
25359
+ return join4(homedir(), ".aka");
23727
25360
  }
23728
25361
  function settingsDir(base = defaultDataDir()) {
23729
- return join3(base, "settings");
25362
+ return join4(base, "settings");
23730
25363
  }
23731
25364
  function dataDir(base = defaultDataDir()) {
23732
- return join3(base, "data");
25365
+ return join4(base, "data");
23733
25366
  }
23734
25367
  function dbPath(base = defaultDataDir()) {
23735
- return join3(dataDir(base), "aka.db");
25368
+ return join4(dataDir(base), "aka.db");
25369
+ }
25370
+ function keysDir(base = defaultDataDir()) {
25371
+ return join4(base, "keys");
23736
25372
  }
23737
25373
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23738
25374
  ensureDataDirSync(dir);
@@ -23745,8 +25381,8 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23745
25381
  for (const { name, dest } of moves) {
23746
25382
  try {
23747
25383
  ensureDataDirSync(dest);
23748
- const moved = join3(dest, name);
23749
- renameSync3(join3(base, name), moved);
25384
+ const moved = join4(dest, name);
25385
+ renameSync3(join4(base, name), moved);
23750
25386
  tightenFile(moved);
23751
25387
  } catch {
23752
25388
  }
@@ -23754,10 +25390,11 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23754
25390
  }
23755
25391
 
23756
25392
  // ../../packages/persistence/src/settings.ts
23757
- import { readFileSync as readFileSync2 } from "fs";
23758
- import { join as join4 } from "path";
25393
+ import { readFileSync as readFileSync3 } from "fs";
25394
+ import { join as join5 } from "path";
25395
+ var SETTINGS_FILENAME = "settings.json";
23759
25396
  function readWorkspaceSettings(base = defaultDataDir()) {
23760
- const record2 = readJson(join4(settingsDir(base), "settings.json"));
25397
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
23761
25398
  if (!record2) return defaultWorkspaceSettings();
23762
25399
  try {
23763
25400
  return WorkspaceSettings.parse(record2);
@@ -23768,23 +25405,898 @@ function readWorkspaceSettings(base = defaultDataDir()) {
23768
25405
  function readJson(file2) {
23769
25406
  let text;
23770
25407
  try {
23771
- text = readFileSync2(file2, "utf8");
25408
+ text = readFileSync3(file2, "utf8");
23772
25409
  } catch {
23773
25410
  return null;
23774
25411
  }
23775
25412
  return parseJsonObject(text) ?? null;
23776
25413
  }
23777
25414
 
25415
+ // ../../packages/persistence/src/vault/crypto.ts
25416
+ import {
25417
+ createCipheriv,
25418
+ createDecipheriv,
25419
+ createHmac as createHmac2,
25420
+ hkdfSync,
25421
+ timingSafeEqual
25422
+ } from "crypto";
25423
+ var POINTER_ID_BYTES = 16;
25424
+ var NONCE_BYTES = 12;
25425
+ var TAG_BYTES = 10;
25426
+ var SUBKEY_BYTES = 32;
25427
+ var HKDF_INFO_ENC = "aka:vault:enc:v1";
25428
+ var HKDF_INFO_SIGN = "aka:vault:sign:v1";
25429
+ var HKDF_SALT = "aka:vault:v1";
25430
+ var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
25431
+ function base32Encode(bytes) {
25432
+ let out = "";
25433
+ let buffer = 0;
25434
+ let bits = 0;
25435
+ for (const byte of bytes) {
25436
+ buffer = buffer << 8 | byte;
25437
+ bits += 8;
25438
+ while (bits >= 5) {
25439
+ out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
25440
+ bits -= 5;
25441
+ }
25442
+ }
25443
+ if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
25444
+ return out;
25445
+ }
25446
+ function base32Decode(text) {
25447
+ const out = [];
25448
+ let buffer = 0;
25449
+ let bits = 0;
25450
+ for (const char of text) {
25451
+ const value = B32_ALPHABET.indexOf(char);
25452
+ if (value < 0) throw new Error("base32: character outside the alphabet");
25453
+ buffer = buffer << 5 | value;
25454
+ bits += 5;
25455
+ if (bits >= 8) {
25456
+ out.push(buffer >>> bits - 8 & 255);
25457
+ bits -= 8;
25458
+ }
25459
+ }
25460
+ return Buffer.from(out);
25461
+ }
25462
+ function encodeKeyVersion(version2) {
25463
+ if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
25464
+ throw new Error("vault: key version out of range");
25465
+ }
25466
+ const bytes = [];
25467
+ let remaining = version2;
25468
+ while (remaining > 0) {
25469
+ bytes.unshift(remaining & 255);
25470
+ remaining = Math.floor(remaining / 256);
25471
+ }
25472
+ return base32Encode(Uint8Array.from(bytes));
25473
+ }
25474
+ function decodeKeyVersion(encoded) {
25475
+ const bytes = base32Decode(encoded);
25476
+ if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
25477
+ let version2 = 0;
25478
+ for (const byte of bytes) version2 = version2 * 256 + byte;
25479
+ if (version2 < 1) throw new Error("vault: bad key version");
25480
+ return version2;
25481
+ }
25482
+ function deriveSubkeys(master) {
25483
+ const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
25484
+ return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
25485
+ }
25486
+ function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
25487
+ if (pointerId.length !== POINTER_ID_BYTES) {
25488
+ throw new Error("vault: pointer id must be 16 bytes");
25489
+ }
25490
+ const head = Buffer.alloc(6);
25491
+ head.writeUInt16BE(formatVersion, 0);
25492
+ head.writeUInt32BE(keyVersion, 2);
25493
+ return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
25494
+ }
25495
+ function seal(encKey, plaintext, aad, nonce) {
25496
+ const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
25497
+ cipher.setAAD(aad);
25498
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
25499
+ return { ciphertext, nonce, authTag: cipher.getAuthTag() };
25500
+ }
25501
+ function open(encKey, sealed, aad) {
25502
+ try {
25503
+ const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
25504
+ decipher.setAAD(aad);
25505
+ decipher.setAuthTag(sealed.authTag);
25506
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
25507
+ } catch {
25508
+ return null;
25509
+ }
25510
+ }
25511
+ function signPointer(signKey, keyVersion, pointerId, category) {
25512
+ return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
25513
+ }
25514
+ function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
25515
+ if (tag.length !== TAG_BYTES) return false;
25516
+ const expected = signPointer(signKey, keyVersion, pointerId, category);
25517
+ return timingSafeEqual(expected, Buffer.from(tag));
25518
+ }
25519
+ function formatPointer(category, keyVersion, pointerId, tag) {
25520
+ return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
25521
+ }
25522
+
25523
+ // ../../packages/persistence/src/vault/key-provider.ts
25524
+ import { execFileSync } from "child_process";
25525
+ import { randomBytes as randomBytes2 } from "crypto";
25526
+ import {
25527
+ chmodSync as chmodSync2,
25528
+ mkdirSync as mkdirSync2,
25529
+ readFileSync as readFileSync4,
25530
+ renameSync as renameSync4,
25531
+ rmSync as rmSync4,
25532
+ statSync as statSync3,
25533
+ writeFileSync as writeFileSync3
25534
+ } from "fs";
25535
+ import { join as join6 } from "path";
25536
+ var VAULT_OCCUPANT_REASON = {
25537
+ symlink: "the path is a symlink; remove it so a keyring can be created",
25538
+ gone: "the path was occupied but holds no keyring (removed while it was being created)",
25539
+ unknown: "the path is occupied but cannot be inspected; check the permissions on its directory"
25540
+ };
25541
+ var VaultKeyEpochMissingError = class extends Error {
25542
+ version;
25543
+ constructor(version2) {
25544
+ super(`vault: key epoch ${String(version2)} is not present in the keyring`);
25545
+ this.name = "VaultKeyEpochMissingError";
25546
+ this.version = version2;
25547
+ }
25548
+ };
25549
+ var VAULT_KEY_FILENAME = "vault.key";
25550
+ var KEY_MATERIAL_BYTES2 = 32;
25551
+ var KEYCHAIN_SERVICE = "aka-vault";
25552
+ var KEYCHAIN_ACCOUNT = "keyring";
25553
+ function parseKeyring(raw) {
25554
+ const parsed = JSON.parse(raw);
25555
+ if (typeof parsed !== "object" || parsed === null) {
25556
+ throw new Error("vault key file is corrupt: not a JSON object");
25557
+ }
25558
+ const { current, keys } = parsed;
25559
+ if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
25560
+ throw new Error("vault key file is corrupt: bad current version");
25561
+ }
25562
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
25563
+ throw new Error("vault key file is corrupt: bad keys map");
25564
+ }
25565
+ const map2 = /* @__PURE__ */ new Map();
25566
+ for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
25567
+ const version2 = Number(rawVersion);
25568
+ if (!Number.isInteger(version2) || version2 < 1) {
25569
+ throw new Error("vault key file is corrupt: bad key version");
25570
+ }
25571
+ if (typeof rawMaterial !== "string") {
25572
+ throw new Error("vault key file is corrupt: bad key material");
25573
+ }
25574
+ const bytes = Buffer.from(rawMaterial, "base64");
25575
+ if (bytes.length !== KEY_MATERIAL_BYTES2) {
25576
+ throw new Error("vault key file is corrupt: bad key material length");
25577
+ }
25578
+ map2.set(version2, bytes);
25579
+ }
25580
+ if (!map2.has(current)) {
25581
+ throw new Error("vault key file is corrupt: current version has no material");
25582
+ }
25583
+ return { current, keys: map2 };
25584
+ }
25585
+ function serializeKeyring(keyring) {
25586
+ const keys = {};
25587
+ for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
25588
+ const material = keyring.keys.get(version2);
25589
+ if (material) keys[String(version2)] = material.toString("base64");
25590
+ }
25591
+ return JSON.stringify({ current: keyring.current, keys });
25592
+ }
25593
+ function mintKeyring() {
25594
+ return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
25595
+ }
25596
+ function withNextEpoch(keyring) {
25597
+ const next = Math.max(...keyring.keys.keys()) + 1;
25598
+ const keys = new Map(keyring.keys);
25599
+ keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
25600
+ return { current: next, keys };
25601
+ }
25602
+ function currentOf(keyring) {
25603
+ const material = keyring.keys.get(keyring.current);
25604
+ if (!material) throw new VaultKeyEpochMissingError(keyring.current);
25605
+ return { material, version: keyring.current };
25606
+ }
25607
+ function epochOf(keyring, version2) {
25608
+ const material = keyring.keys.get(version2);
25609
+ if (!material) throw new VaultKeyEpochMissingError(version2);
25610
+ return { material, version: version2 };
25611
+ }
25612
+ function asAsync(work) {
25613
+ try {
25614
+ return Promise.resolve(work());
25615
+ } catch (err) {
25616
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
25617
+ }
25618
+ }
25619
+ function asError(err) {
25620
+ return err instanceof Error ? err : new Error(String(err));
25621
+ }
25622
+ var ROTATION_LOCK_STALE_MS = 6e4;
25623
+ var LOCK_OWNER_FILE = "owner";
25624
+ var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
25625
+ function claimRotationLock(lock, owner) {
25626
+ try {
25627
+ mkdirSync2(lock);
25628
+ } catch (err) {
25629
+ if (err.code === "EEXIST") return false;
25630
+ throw asError(err);
25631
+ }
25632
+ try {
25633
+ writeFileSync3(join6(lock, LOCK_OWNER_FILE), `${owner}
25634
+ `, { mode: DATA_FILE_MODE });
25635
+ return true;
25636
+ } catch (err) {
25637
+ rmSync4(lock, { recursive: true, force: true });
25638
+ throw asError(err);
25639
+ }
25640
+ }
25641
+ function acquireRotationLock(keysDir2) {
25642
+ const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
25643
+ const owner = randomBytes2(16).toString("hex");
25644
+ if (claimRotationLock(lock, owner)) return { lock, owner };
25645
+ let held;
25646
+ try {
25647
+ held = statSync3(lock);
25648
+ } catch {
25649
+ throw new Error(ROTATION_IN_PROGRESS);
25650
+ }
25651
+ if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
25652
+ const aside = `${lock}.stale.${owner}`;
25653
+ try {
25654
+ const now = statSync3(lock);
25655
+ if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
25656
+ throw new Error(ROTATION_IN_PROGRESS);
25657
+ }
25658
+ renameSync4(lock, aside);
25659
+ } catch (err) {
25660
+ if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
25661
+ throw new Error(ROTATION_IN_PROGRESS, { cause: err });
25662
+ }
25663
+ rmSync4(aside, { recursive: true, force: true });
25664
+ if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
25665
+ return { lock, owner };
25666
+ }
25667
+ function releaseRotationLock(lease) {
25668
+ try {
25669
+ if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
25670
+ } catch {
25671
+ return;
25672
+ }
25673
+ rmSync4(lease.lock, { recursive: true, force: true });
25674
+ }
25675
+ function withRotationLock(keysDir2, work) {
25676
+ ensureDataDirSync(keysDir2);
25677
+ const lease = acquireRotationLock(keysDir2);
25678
+ try {
25679
+ return work();
25680
+ } finally {
25681
+ releaseRotationLock(lease);
25682
+ }
25683
+ }
25684
+ var FileKeyProvider = class {
25685
+ #keysDir;
25686
+ constructor(keysDir2) {
25687
+ this.#keysDir = keysDir2;
25688
+ }
25689
+ get filePath() {
25690
+ return join6(this.#keysDir, VAULT_KEY_FILENAME);
25691
+ }
25692
+ loadOrCreate() {
25693
+ return asAsync(() => {
25694
+ const existing = this.#read();
25695
+ if (!existing) return currentOf(this.#createExclusive());
25696
+ tightenFileMode(this.filePath);
25697
+ return currentOf(existing);
25698
+ });
25699
+ }
25700
+ rotate() {
25701
+ return asAsync(
25702
+ () => withRotationLock(this.#keysDir, () => {
25703
+ const existing = this.#read();
25704
+ if (!existing) return currentOf(this.#createExclusive());
25705
+ return currentOf(this.#write(withNextEpoch(existing)));
25706
+ })
25707
+ );
25708
+ }
25709
+ materialFor(version2) {
25710
+ return asAsync(() => {
25711
+ const existing = this.#read();
25712
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
25713
+ return epochOf(existing, version2);
25714
+ });
25715
+ }
25716
+ /** The keyring, or null when the file is ABSENT. A corrupt file throws. */
25717
+ #read() {
25718
+ let raw;
25719
+ try {
25720
+ raw = readFileSync4(this.filePath, "utf8");
25721
+ } catch (err) {
25722
+ if (err.code === "ENOENT") return null;
25723
+ throw err instanceof Error ? err : new Error(String(err));
25724
+ }
25725
+ return parseKeyring(raw);
25726
+ }
25727
+ /**
25728
+ * First mint: the keyring is CREATED, never replaced, so two processes racing
25729
+ * a fresh machine cannot each mint a different epoch 1 — with tmp + rename
25730
+ * the loser's replace would orphan everything the winner had already sealed.
25731
+ * The loser re-reads and adopts the winner's keyring; it minted nothing.
25732
+ *
25733
+ * `createOwnerOnlyFileSync` publishes by link rather than by an exclusive open
25734
+ * at the final path, so the keyring never exists at zero length: a reader —
25735
+ * including the loser, re-reading in order to adopt — sees the file absent or
25736
+ * whole, and never mistakes a live keyring for a corrupt one. A corrupt file
25737
+ * still throws from the parse and is never re-minted over.
25738
+ */
25739
+ #createExclusive() {
25740
+ ensureDataDirSync(this.#keysDir);
25741
+ const keyring = mintKeyring();
25742
+ if (createOwnerOnlyFileSync(this.filePath, `${serializeKeyring(keyring)}
25743
+ `)) return keyring;
25744
+ const winner = this.#read();
25745
+ if (!winner) {
25746
+ const occupant = classifyOccupant(this.filePath);
25747
+ throw new KeyUnclaimableError(
25748
+ `vault: cannot create a key file at ${this.filePath} \u2014 ${VAULT_OCCUPANT_REASON[occupant.kind]}`,
25749
+ occupant.cause
25750
+ );
25751
+ }
25752
+ tightenFileMode(this.filePath);
25753
+ return winner;
25754
+ }
25755
+ /**
25756
+ * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
25757
+ * Used only for rotation, under the rotation lock — first creation goes
25758
+ * through the creation-exclusive path instead.
25759
+ */
25760
+ #write(keyring) {
25761
+ ensureDataDirSync(this.#keysDir);
25762
+ const file2 = this.filePath;
25763
+ const tmp = `${file2}.tmp`;
25764
+ writeFileSync3(tmp, `${serializeKeyring(keyring)}
25765
+ `, { mode: DATA_FILE_MODE });
25766
+ renameSync4(tmp, file2);
25767
+ tightenFileMode(file2);
25768
+ return keyring;
25769
+ }
25770
+ };
25771
+ function tightenFileMode(file2) {
25772
+ try {
25773
+ chmodSync2(file2, DATA_FILE_MODE);
25774
+ } catch {
25775
+ }
25776
+ }
25777
+ var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
25778
+ encoding: "utf8",
25779
+ stdio: ["ignore", "pipe", "ignore"]
25780
+ });
25781
+ var SECURITY_ITEM_NOT_FOUND = 44;
25782
+ var KeychainKeyProvider = class {
25783
+ #keysDir;
25784
+ #exec;
25785
+ constructor(keysDir2, exec = runSecurity) {
25786
+ if (exec === runSecurity && process.platform !== "darwin") {
25787
+ throw new Error(
25788
+ `keychain custody is not available on this platform (${process.platform}); use file custody`
25789
+ );
25790
+ }
25791
+ this.#keysDir = keysDir2;
25792
+ this.#exec = exec;
25793
+ }
25794
+ /** Where a fallback file provider for the same vault would keep its keyring. */
25795
+ get keysDir() {
25796
+ return this.#keysDir;
25797
+ }
25798
+ loadOrCreate() {
25799
+ return asAsync(() => {
25800
+ const existing = this.#read();
25801
+ if (existing) return currentOf(existing);
25802
+ return currentOf(this.#create(mintKeyring()));
25803
+ });
25804
+ }
25805
+ rotate() {
25806
+ return asAsync(
25807
+ () => withRotationLock(this.#keysDir, () => {
25808
+ const existing = this.#read();
25809
+ if (!existing) return currentOf(this.#create(mintKeyring()));
25810
+ return currentOf(this.#replace(withNextEpoch(existing)));
25811
+ })
25812
+ );
25813
+ }
25814
+ materialFor(version2) {
25815
+ return asAsync(() => {
25816
+ const existing = this.#read();
25817
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
25818
+ return epochOf(existing, version2);
25819
+ });
25820
+ }
25821
+ /** The keyring, or null when no item exists yet. A corrupt item throws. */
25822
+ #read() {
25823
+ let raw;
25824
+ try {
25825
+ raw = this.#exec([
25826
+ "find-generic-password",
25827
+ "-s",
25828
+ KEYCHAIN_SERVICE,
25829
+ "-a",
25830
+ KEYCHAIN_ACCOUNT,
25831
+ "-w"
25832
+ ]);
25833
+ } catch (err) {
25834
+ if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
25835
+ throw new Error(
25836
+ `vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
25837
+ { cause: err }
25838
+ );
25839
+ }
25840
+ const body = raw.trim();
25841
+ if (body.length === 0) return null;
25842
+ return parseKeyring(body);
25843
+ }
25844
+ /**
25845
+ * First mint: a plain `add-generic-password` (no `-U`) fails when an item
25846
+ * already exists, so a concurrent first mint cannot overwrite the winner's
25847
+ * keyring — the loser re-reads and adopts it instead.
25848
+ */
25849
+ #create(keyring) {
25850
+ const args = [
25851
+ "add-generic-password",
25852
+ "-s",
25853
+ KEYCHAIN_SERVICE,
25854
+ "-a",
25855
+ KEYCHAIN_ACCOUNT,
25856
+ "-w",
25857
+ serializeKeyring(keyring)
25858
+ ];
25859
+ try {
25860
+ this.#exec(args);
25861
+ } catch (err) {
25862
+ const winner = this.#read();
25863
+ if (winner) return winner;
25864
+ throw asError(err);
25865
+ }
25866
+ return keyring;
25867
+ }
25868
+ // `-U` updates the item in place, deliberately replacing the stored map with
25869
+ // one that contains it — used only for rotation, under the rotation lock.
25870
+ #replace(keyring) {
25871
+ this.#exec([
25872
+ "add-generic-password",
25873
+ "-U",
25874
+ "-s",
25875
+ KEYCHAIN_SERVICE,
25876
+ "-a",
25877
+ KEYCHAIN_ACCOUNT,
25878
+ "-w",
25879
+ serializeKeyring(keyring)
25880
+ ]);
25881
+ return keyring;
25882
+ }
25883
+ };
25884
+ function createKeyProvider(custody, keysDir2) {
25885
+ if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
25886
+ return new FileKeyProvider(keysDir2);
25887
+ }
25888
+
25889
+ // ../../packages/persistence/src/vault/vault.ts
25890
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
25891
+ var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
25892
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
25893
+ var VAULT_PURGE_POINTER_ID = "*";
25894
+ function parsePointer(token) {
25895
+ if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
25896
+ const body = token.slice("[[aka:".length, -"]]".length);
25897
+ const colon = body.indexOf(":");
25898
+ if (colon < 0) return null;
25899
+ const category = body.slice(0, colon);
25900
+ const [kv, id, tag] = body.slice(colon + 1).split(".");
25901
+ if (kv === void 0 || id === void 0 || tag === void 0) return null;
25902
+ try {
25903
+ const keyVersion = decodeKeyVersion(kv);
25904
+ const pointerId = base32Decode(id);
25905
+ const tagBytes = base32Decode(tag);
25906
+ if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
25907
+ return null;
25908
+ }
25909
+ return { category, keyVersion, pointerId, tag: tagBytes };
25910
+ } catch {
25911
+ return null;
25912
+ }
25913
+ }
25914
+ var SecretVault = class {
25915
+ #repo;
25916
+ #keys;
25917
+ #isConsented;
25918
+ #verifyGrant;
25919
+ #now;
25920
+ constructor(deps) {
25921
+ this.#repo = deps.repo;
25922
+ this.#keys = deps.keys;
25923
+ this.#isConsented = deps.isConsented;
25924
+ this.#verifyGrant = deps.verifyGrant;
25925
+ this.#now = deps.now ?? (() => Date.now());
25926
+ }
25927
+ /**
25928
+ * Store a value and return the pointer that stands for it. The same value
25929
+ * always yields the same pointer on this machine — one row, one pointer id,
25930
+ * one category — which is what makes dedup and reuse counting work.
25931
+ *
25932
+ * `fingerprintKey` is the exception-key epoch this value's fingerprint is
25933
+ * derived under — a different key from the vault's, with different rotation
25934
+ * semantics. It is a parameter of the WRITE rather than a constructor dep,
25935
+ * and a thunk rather than a value, so that the only way to reach a key is to
25936
+ * store something: a read-only caller never names it, and a caller whose
25937
+ * source mints on absence mints only once consent has actually opened the
25938
+ * write. `refreshFingerprints` takes its key the same way, for the same
25939
+ * reason.
25940
+ *
25941
+ * Resolved once per call, so the fingerprint and the version it is recorded
25942
+ * under can never come from two different epochs.
25943
+ */
25944
+ async tokenize(raw, meta3, fingerprintKey) {
25945
+ if (!this.#isConsented()) return CONSENT_ABSENT;
25946
+ const fpKey = fingerprintKey();
25947
+ const valueFingerprint = fingerprintValue(fpKey, raw);
25948
+ const existing = this.#repo.byValueFingerprint(valueFingerprint);
25949
+ const now = this.#now();
25950
+ if (existing) {
25951
+ this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
25952
+ return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
25953
+ }
25954
+ const { material, version: version2 } = await this.#keys.loadOrCreate();
25955
+ const subkeys = deriveSubkeys(material);
25956
+ const pointerId = randomBytes3(POINTER_ID_BYTES);
25957
+ const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
25958
+ const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
25959
+ const { row } = this.#repo.upsert(
25960
+ {
25961
+ pointerId: base32Encode(pointerId),
25962
+ valueFingerprint,
25963
+ fingerprintKeyVersion: fpKey.version,
25964
+ keyVersion: version2,
25965
+ // Recorded so the row stays OPENABLE if the wire-format constant ever
25966
+ // moves: it is part of this row's AEAD AAD. It is not a tag input —
25967
+ // tags are pinned to the constant on both sides.
25968
+ formatVersion: POINTER_FORMAT_VERSION,
25969
+ category: meta3.category,
25970
+ ruleId: meta3.ruleId,
25971
+ maskedMatch: meta3.maskedMatch,
25972
+ provider: meta3.provider,
25973
+ ciphertext: sealed.ciphertext.toString("base64"),
25974
+ nonce: sealed.nonce.toString("base64"),
25975
+ authTag: sealed.authTag.toString("base64")
25976
+ },
25977
+ now
25978
+ );
25979
+ return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
25980
+ }
25981
+ /**
25982
+ * Resolve a pointer back to its value, for a human or (with a grant) for the
25983
+ * model. Every call that gets as far as an identified row writes an audit row.
25984
+ */
25985
+ async detokenize(token, opts) {
25986
+ const parsed = parsePointer(token);
25987
+ if (!parsed) return UNAVAILABLE;
25988
+ let signKey;
25989
+ try {
25990
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
25991
+ signKey = deriveSubkeys(epoch.material).sign;
25992
+ } catch {
25993
+ return UNAVAILABLE;
25994
+ }
25995
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
25996
+ return UNAVAILABLE;
25997
+ }
25998
+ const pointerId = base32Encode(parsed.pointerId);
25999
+ const row = this.#repo.byPointerId(pointerId);
26000
+ if (!row) {
26001
+ this.#audit(pointerId, opts, "unavailable");
26002
+ return UNAVAILABLE;
26003
+ }
26004
+ if (row.category !== parsed.category) return UNAVAILABLE;
26005
+ if (opts.target === "model") {
26006
+ const grantId = opts.grantId;
26007
+ const verify = this.#verifyGrant;
26008
+ if (verify === void 0 || grantId === void 0 || grantId === "") {
26009
+ this.#audit(pointerId, opts, "refused");
26010
+ return UNAVAILABLE;
26011
+ }
26012
+ let covered;
26013
+ try {
26014
+ covered = await verify(grantId, {
26015
+ ruleId: row.ruleId,
26016
+ valueFingerprint: row.valueFingerprint,
26017
+ fingerprintKeyVersion: row.fingerprintKeyVersion
26018
+ });
26019
+ } catch {
26020
+ covered = false;
26021
+ }
26022
+ if (!covered) {
26023
+ this.#audit(pointerId, opts, "refused");
26024
+ return UNAVAILABLE;
26025
+ }
26026
+ }
26027
+ let raw;
26028
+ try {
26029
+ const epoch = await this.#keys.materialFor(row.keyVersion);
26030
+ raw = open(
26031
+ deriveSubkeys(epoch.material).enc,
26032
+ {
26033
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
26034
+ nonce: Buffer.from(row.nonce, "base64"),
26035
+ authTag: Buffer.from(row.authTag, "base64")
26036
+ },
26037
+ // Sealed under the ROW's epoch and format version. Rotation may have
26038
+ // moved the epoch past the one this token names, and a format bump may
26039
+ // have moved the constant past the generation this row was sealed
26040
+ // under — the AAD follows the row in both cases, never the token.
26041
+ bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
26042
+ );
26043
+ } catch {
26044
+ raw = null;
26045
+ }
26046
+ if (raw === null) {
26047
+ this.#audit(pointerId, opts, "unavailable");
26048
+ return UNAVAILABLE;
26049
+ }
26050
+ this.#audit(pointerId, opts, "revealed");
26051
+ return raw;
26052
+ }
26053
+ /**
26054
+ * Owner-surface reveal by row id: the dashboard shows a row the owner can
26055
+ * already see and asks for its value. There is no wire token here to verify —
26056
+ * the tag exists to stop FORGED tokens arriving in untrusted text, and a row
26057
+ * id selected server-side from the owner's own store is not that — so this
26058
+ * loads the row directly, opens its ciphertext under the row's epoch, and
26059
+ * audits exactly like a human-target de-reference. Never callable with
26060
+ * target 'model': the wire-token path with its grant gate is the only road
26061
+ * raw travels toward the model.
26062
+ */
26063
+ async revealEntry(pointerId, opts) {
26064
+ const row = this.#repo.byPointerId(pointerId);
26065
+ if (!row) {
26066
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
26067
+ return UNAVAILABLE;
26068
+ }
26069
+ const raw = await this.#openRow(row);
26070
+ if (raw === null) {
26071
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
26072
+ return UNAVAILABLE;
26073
+ }
26074
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
26075
+ return raw;
26076
+ }
26077
+ /** Badge and listing data. No raw value, no fingerprint, and no audit row. */
26078
+ async describePointer(token) {
26079
+ const row = await this.#rowFor(token);
26080
+ if (!row) return null;
26081
+ return {
26082
+ category: row.category,
26083
+ ...row.provider === void 0 ? {} : { provider: row.provider },
26084
+ maskedMatch: row.maskedMatch,
26085
+ occurrences: row.occurrenceCount,
26086
+ firstSeen: new Date(row.firstSeen).toISOString(),
26087
+ lastSeen: new Date(row.lastSeen).toISOString()
26088
+ };
26089
+ }
26090
+ /**
26091
+ * The raw-free row identity a reveal grant matches on. Deliberately not fed to
26092
+ * view surfaces: the keyed fingerprint is a correlation key and must not reach
26093
+ * a presentation layer.
26094
+ */
26095
+ async resolvePointerIdentity(token) {
26096
+ const row = await this.#rowFor(token);
26097
+ if (!row) return null;
26098
+ return {
26099
+ ruleId: row.ruleId,
26100
+ valueFingerprint: row.valueFingerprint,
26101
+ fingerprintKeyVersion: row.fingerprintKeyVersion
26102
+ };
26103
+ }
26104
+ /**
26105
+ * Mint the next vault key epoch and re-encrypt every entry under it. Pointers
26106
+ * already emitted keep verifying: their tag is checked against the historical
26107
+ * epoch they name, which the key provider retains.
26108
+ *
26109
+ * Safe to interrupt — each row carries the epoch its ciphertext is sealed
26110
+ * under, so a half-finished pass leaves every row openable.
26111
+ *
26112
+ * The rotation lock covers only the keyring mint inside `rotate()`; the
26113
+ * re-seal pass below runs unlocked. Two concurrent rotations therefore
26114
+ * serialize on the keyring but interleave over the rows, so a slower pass can
26115
+ * re-seal a row back to an epoch a faster one already moved past, and
26116
+ * `reEncrypted` can double-count. No value is lost either way — every epoch is
26117
+ * retained and every row stays openable — but "after rotation every row sits
26118
+ * at the newest epoch" does not hold under concurrency. Holding the lock
26119
+ * across the whole pass requires an async-aware lock, since a callback that
26120
+ * awaits would release the lock at its first suspension.
26121
+ */
26122
+ async rotateVaultKey() {
26123
+ const next = await this.#keys.rotate();
26124
+ const nextEnc = deriveSubkeys(next.material).enc;
26125
+ let reEncrypted = 0;
26126
+ for (const row of this.#repo.listAll()) {
26127
+ if (row.keyVersion === next.version) continue;
26128
+ const pointerId = base32Decode(row.pointerId);
26129
+ let raw;
26130
+ try {
26131
+ const epoch = await this.#keys.materialFor(row.keyVersion);
26132
+ raw = open(
26133
+ deriveSubkeys(epoch.material).enc,
26134
+ {
26135
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
26136
+ nonce: Buffer.from(row.nonce, "base64"),
26137
+ authTag: Buffer.from(row.authTag, "base64")
26138
+ },
26139
+ bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
26140
+ );
26141
+ } catch {
26142
+ raw = null;
26143
+ }
26144
+ if (raw === null) continue;
26145
+ const sealed = seal(
26146
+ nextEnc,
26147
+ raw,
26148
+ bindingInput(next.version, pointerId, row.category, row.formatVersion),
26149
+ randomBytes3(NONCE_BYTES)
26150
+ );
26151
+ this.#repo.replaceCiphertext(row.pointerId, {
26152
+ keyVersion: next.version,
26153
+ ciphertext: sealed.ciphertext.toString("base64"),
26154
+ nonce: sealed.nonce.toString("base64"),
26155
+ authTag: sealed.authTag.toString("base64")
26156
+ });
26157
+ reEncrypted += 1;
26158
+ }
26159
+ return { version: next.version, reEncrypted };
26160
+ }
26161
+ /**
26162
+ * Re-key every entry's value fingerprint after the exception key rotates,
26163
+ * PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
26164
+ * because the raw values are gone — the vault still holds the values, so
26165
+ * determinism, dedup, and every outstanding pointer survive the rotation.
26166
+ *
26167
+ * Every fingerprint-key rotation must run this: a row left at the old epoch
26168
+ * still resolves, but the same value detected again fingerprints under the
26169
+ * NEW key, misses the dedup lookup, and mints a second row and a second
26170
+ * pointer — one value, two tokens in circulation.
26171
+ *
26172
+ * Per-row best-effort: a row that cannot open, or whose refreshed
26173
+ * fingerprint collides with a row already refreshed, is skipped rather than
26174
+ * aborting the pass — one damaged entry must not strand the re-key of every
26175
+ * other. A skipped row keeps resolving under its old fingerprint epoch.
26176
+ */
26177
+ async refreshFingerprints(next) {
26178
+ let refreshed = 0;
26179
+ for (const row of this.#repo.listAll()) {
26180
+ try {
26181
+ const raw = await this.#openRow(row);
26182
+ if (raw === null) continue;
26183
+ this.#repo.refreshFingerprint(row.pointerId, {
26184
+ valueFingerprint: fingerprintValue(next, raw),
26185
+ fingerprintKeyVersion: next.version
26186
+ });
26187
+ refreshed += 1;
26188
+ } catch {
26189
+ continue;
26190
+ }
26191
+ }
26192
+ return refreshed;
26193
+ }
26194
+ /**
26195
+ * Destroy every entry, making all outstanding pointers permanently
26196
+ * unresolvable.
26197
+ *
26198
+ * The count comes from `purgeAll` rather than a separate `countEntries` —
26199
+ * `purgeAll` counts inside the same transaction that deletes, so the audit row
26200
+ * reports what was actually destroyed. Counting beforehand would let a
26201
+ * concurrent write land between the two statements and put a number in the
26202
+ * durable record that never matched reality.
26203
+ */
26204
+ purgeVault() {
26205
+ const destroyed = this.#repo.purgeAll();
26206
+ this.#repo.recordDeref({
26207
+ id: randomUUID12(),
26208
+ pointerId: VAULT_PURGE_POINTER_ID,
26209
+ at: this.#now(),
26210
+ target: "human",
26211
+ reason: "purge",
26212
+ outcome: "unavailable",
26213
+ pointerCount: Math.max(destroyed, 1)
26214
+ });
26215
+ return destroyed;
26216
+ }
26217
+ // Sign under the epoch the token names — which for a re-detected value is the
26218
+ // epoch its row currently sits at rather than whatever is current.
26219
+ //
26220
+ // The row's format version is NOT a tag input. It binds the row's ciphertext
26221
+ // (it is part of the AEAD AAD, so an old row stays openable) but never the
26222
+ // wire tag, which verification checks against POINTER_FORMAT_VERSION without
26223
+ // knowing any row. Signing a token here under a row's own generation is what
26224
+ // would make the vault emit tokens it then refuses.
26225
+ async #emitToken(keyVersion, pointerIdB32, category) {
26226
+ const pointerId = base32Decode(pointerIdB32);
26227
+ const epoch = await this.#keys.materialFor(keyVersion);
26228
+ const signKey = deriveSubkeys(epoch.material).sign;
26229
+ return formatPointer(
26230
+ category,
26231
+ keyVersion,
26232
+ pointerId,
26233
+ signPointer(signKey, keyVersion, pointerId, category)
26234
+ );
26235
+ }
26236
+ async #openRow(row) {
26237
+ try {
26238
+ const epoch = await this.#keys.materialFor(row.keyVersion);
26239
+ return open(
26240
+ deriveSubkeys(epoch.material).enc,
26241
+ {
26242
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
26243
+ nonce: Buffer.from(row.nonce, "base64"),
26244
+ authTag: Buffer.from(row.authTag, "base64")
26245
+ },
26246
+ bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
26247
+ );
26248
+ } catch {
26249
+ return null;
26250
+ }
26251
+ }
26252
+ // Shared lookup for the read-only surfaces. It verifies the tag exactly as
26253
+ // detokenize does: a descriptor is not raw, but a token nobody can vouch for
26254
+ // should not resolve to anything at all — otherwise a fabricated pointer, or a
26255
+ // lookalike planted in a file, would still yield a category and a masked
26256
+ // preview. Verifying needs the historical epoch's key, which is why these
26257
+ // surfaces are async.
26258
+ async #rowFor(token) {
26259
+ const parsed = parsePointer(token);
26260
+ if (!parsed) return null;
26261
+ try {
26262
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
26263
+ const signKey = deriveSubkeys(epoch.material).sign;
26264
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
26265
+ return null;
26266
+ }
26267
+ } catch {
26268
+ return null;
26269
+ }
26270
+ const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
26271
+ if (row?.category !== parsed.category) return null;
26272
+ return row;
26273
+ }
26274
+ #audit(pointerId, opts, outcome) {
26275
+ this.#repo.recordDeref({
26276
+ id: randomUUID12(),
26277
+ pointerId,
26278
+ at: this.#now(),
26279
+ target: opts.target,
26280
+ reason: opts.reason,
26281
+ outcome,
26282
+ ...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
26283
+ // Only the batched reasons carry a count above one; a model crossing is
26284
+ // always its own row.
26285
+ pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
26286
+ });
26287
+ }
26288
+ };
26289
+
23778
26290
  // ../../packages/persistence/src/warn-era-cap.ts
23779
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
23780
- import { join as join5 } from "path";
26291
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
26292
+ import { join as join7 } from "path";
23781
26293
  var MARKER = "warn-era-capped";
23782
26294
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23783
26295
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23784
- const marker = join5(dataDir2, MARKER);
23785
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
26296
+ const marker = join7(dataDir2, MARKER);
26297
+ if (existsSync4(marker)) return { capped: 0, skipped: "already-run" };
23786
26298
  const capped = db.policies.capCategoryActions();
23787
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
26299
+ writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
23788
26300
  `, { mode: DATA_FILE_MODE });
23789
26301
  return { capped };
23790
26302
  }
@@ -23848,11 +26360,11 @@ function providerFromModelId(modelId) {
23848
26360
  }
23849
26361
 
23850
26362
  // ../../packages/plugin-sdk/src/config.ts
23851
- function loadConfig(base = defaultDataDir()) {
26363
+ function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
23852
26364
  try {
23853
26365
  ensureLayoutDirSync(base);
23854
- const settingsFile = join6(settingsDir(base), "settings.json");
23855
- if (existsSync4(settingsFile)) tightenFile(settingsFile);
26366
+ const settingsFile = join8(settingsDir(base), "settings.json");
26367
+ if (existsSync5(settingsFile)) tightenFile(settingsFile);
23856
26368
  } catch {
23857
26369
  }
23858
26370
  migrateLegacyLayout(base);
@@ -23863,21 +26375,21 @@ function loadConfig(base = defaultDataDir()) {
23863
26375
  dbPath: dbPath(base),
23864
26376
  settingsDir: settingsDir(base),
23865
26377
  onboarded: settings.onboardedAt != null,
23866
- provider: resolveProviderSafe()
26378
+ provider: resolveProviderSafe(resolveProviderFn)
23867
26379
  };
23868
26380
  }
23869
- function resolveProviderSafe() {
26381
+ function resolveProviderSafe(resolveProviderFn) {
23870
26382
  try {
23871
- return resolveProvider();
26383
+ return resolveProviderFn();
23872
26384
  } catch {
23873
26385
  return { provider: "anthropic" };
23874
26386
  }
23875
26387
  }
23876
26388
 
23877
26389
  // ../../packages/plugin-sdk/src/config-inventory.ts
23878
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
26390
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync5 } from "fs";
23879
26391
  import { homedir as homedir2 } from "os";
23880
- import { basename as basename2, join as join8 } from "path";
26392
+ import { basename as basename3, join as join10 } from "path";
23881
26393
 
23882
26394
  // ../../packages/detections/src/egress/registry.ts
23883
26395
  var EXTRACTOR_VERSION = "1";
@@ -24665,12 +27177,12 @@ function redact(text, findings) {
24665
27177
  const regions = [];
24666
27178
  for (const f of sorted) {
24667
27179
  const rank = SEVERITY_RANK2[f.severity];
24668
- const open = regions[regions.length - 1];
24669
- if (open && f.span.start < open.end) {
24670
- open.end = Math.max(open.end, f.span.end);
24671
- if (rank > open.rank) {
24672
- open.rank = rank;
24673
- open.category = f.category;
27180
+ const open2 = regions[regions.length - 1];
27181
+ if (open2 && f.span.start < open2.end) {
27182
+ open2.end = Math.max(open2.end, f.span.end);
27183
+ if (rank > open2.rank) {
27184
+ open2.rank = rank;
27185
+ open2.category = f.category;
24674
27186
  }
24675
27187
  } else {
24676
27188
  regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
@@ -24699,6 +27211,24 @@ function maskMatch(raw) {
24699
27211
  return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
24700
27212
  }
24701
27213
 
27214
+ // ../../packages/detections/src/pointer-shield.ts
27215
+ function shieldPointers(text) {
27216
+ const spans = [];
27217
+ let out = null;
27218
+ for (const match of text.matchAll(pointerTokenScanner())) {
27219
+ spans.push({ start: match.index, end: match.index + match[0].length });
27220
+ out ??= text;
27221
+ out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
27222
+ }
27223
+ return { text: out ?? text, spans };
27224
+ }
27225
+ function dropShieldedFindings(findings, spans) {
27226
+ if (spans.length === 0) return findings;
27227
+ return findings.filter(
27228
+ (finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
27229
+ );
27230
+ }
27231
+
24702
27232
  // ../../packages/detections/src/posture/config-posture.ts
24703
27233
  var RULE_VERSION = "1";
24704
27234
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
@@ -26435,7 +28965,7 @@ var gcp_service_account_default = {
26435
28965
  severity: "critical",
26436
28966
  matcher: {
26437
28967
  type: "regex",
26438
- pattern: "\\b[a-z0-9][a-z0-9-]{2,60}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
28968
+ pattern: "(?<![a-z0-9-])[a-z0-9-]{3,}@[a-z0-9][a-z0-9-]{2,60}\\.iam\\.gserviceaccount\\.com\\b",
26439
28969
  flags: "g"
26440
28970
  },
26441
28971
  examples: [
@@ -26832,7 +29362,8 @@ function scanText(text, ruleVersions) {
26832
29362
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
26833
29363
  try {
26834
29364
  const rules = getLoadedRules();
26835
- const matches = scan(text, rules);
29365
+ const shielded = shieldPointers(text);
29366
+ const matches = dropShieldedFindings(scan(shielded.text, rules), shielded.spans);
26836
29367
  if (matches.length === 0) return { masked: text, findings: [] };
26837
29368
  const byId = new Map(rules.map((r) => [r.id, r]));
26838
29369
  const findings = matches.map((m) => {
@@ -26855,8 +29386,8 @@ function scanText(text, ruleVersions) {
26855
29386
  }
26856
29387
 
26857
29388
  // ../../packages/plugin-sdk/src/repo.ts
26858
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
26859
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
29389
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
29390
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
26860
29391
  function resolveRepoIdentity(cwd) {
26861
29392
  try {
26862
29393
  const root = findGitRoot(cwd);
@@ -26869,7 +29400,7 @@ function resolveRepoIdentity(cwd) {
26869
29400
  // win32) so the persistence layer's `/`-separated checkout-path patterns
26870
29401
  // (the ghost sweep + the read-side worktree filter) match it as written.
26871
29402
  url: url2 ?? headRoot.split(sep2).join("/"),
26872
- name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
29403
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename2(headRoot)
26873
29404
  };
26874
29405
  } catch {
26875
29406
  return void 0;
@@ -26889,36 +29420,36 @@ function resolveRepoNwo(cwd) {
26889
29420
  function findGitRoot(start) {
26890
29421
  let dir = start;
26891
29422
  for (; ; ) {
26892
- if (existsSync5(join7(dir, ".git"))) return dir;
26893
- const parent = dirname(dir);
29423
+ if (existsSync6(join9(dir, ".git"))) return dir;
29424
+ const parent = dirname2(dir);
26894
29425
  if (parent === dir) return void 0;
26895
29426
  dir = parent;
26896
29427
  }
26897
29428
  }
26898
29429
  function resolveGitContext(root) {
26899
- const dotGit = join7(root, ".git");
29430
+ const dotGit = join9(root, ".git");
26900
29431
  try {
26901
- if (statSync(dotGit).isDirectory()) {
26902
- return { configPath: join7(dotGit, "config"), headRoot: root };
29432
+ if (statSync4(dotGit).isDirectory()) {
29433
+ return { configPath: join9(dotGit, "config"), headRoot: root };
26903
29434
  }
26904
29435
  } catch {
26905
29436
  return void 0;
26906
29437
  }
26907
29438
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
26908
29439
  if (!target) return void 0;
26909
- const gitdir = isAbsolute(target) ? target : join7(root, target);
26910
- if (existsSync5(join7(gitdir, "config"))) {
26911
- return { configPath: join7(gitdir, "config"), headRoot: root };
29440
+ const gitdir = isAbsolute(target) ? target : join9(root, target);
29441
+ if (existsSync6(join9(gitdir, "config"))) {
29442
+ return { configPath: join9(gitdir, "config"), headRoot: root };
26912
29443
  }
26913
- const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
29444
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
26914
29445
  if (!commonRaw) return void 0;
26915
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
26916
- const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
26917
- return { configPath: join7(commonGitDir, "config"), headRoot };
29446
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
29447
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
29448
+ return { configPath: join9(commonGitDir, "config"), headRoot };
26918
29449
  }
26919
29450
  function safeRead(path) {
26920
29451
  try {
26921
- return readFileSync3(path, "utf8");
29452
+ return readFileSync5(path, "utf8");
26922
29453
  } catch {
26923
29454
  return void 0;
26924
29455
  }
@@ -26969,18 +29500,23 @@ function nwoFromUrl(url2) {
26969
29500
  }
26970
29501
 
26971
29502
  // ../../packages/plugin-sdk/src/events.ts
26972
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
29503
+ import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
29504
+
29505
+ // ../../packages/plugin-sdk/src/isolated-scan.ts
29506
+ import { existsSync as existsSync7 } from "fs";
29507
+ import { fileURLToPath } from "url";
29508
+ import { Worker } from "worker_threads";
26973
29509
 
26974
29510
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
26975
- import { arch, hostname as hostname3, platform, release } from "os";
29511
+ import { arch, hostname as hostname4, platform, release } from "os";
26976
29512
  function resolveInventoryContext(input) {
26977
29513
  const host = {
26978
29514
  objectType: "host",
26979
29515
  // Stable-ish machine id; os/arch live in the descriptive bag (a
26980
29516
  // harder machine id can replace this without a schema change).
26981
- identityKey: hostname3(),
26982
- title: hostname3(),
26983
- attributes: { host_name: hostname3(), os: platform(), os_version: release(), arch: arch() }
29517
+ identityKey: hostname4(),
29518
+ title: hostname4(),
29519
+ attributes: { host_name: hostname4(), os: platform(), os_version: release(), arch: arch() }
26984
29520
  };
26985
29521
  const harnessAttributes = {};
26986
29522
  if (input.harnessVersion != null) harnessAttributes.harness_version = input.harnessVersion;
@@ -27001,30 +29537,352 @@ function resolveInventoryContext(input) {
27001
29537
  }
27002
29538
 
27003
29539
  // ../../packages/plugin-sdk/src/nudge.ts
27004
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27005
- import { join as join9 } from "path";
29540
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
29541
+ import { join as join11 } from "path";
27006
29542
 
27007
29543
  // ../../packages/plugin-sdk/src/paths.ts
27008
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
27009
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
29544
+ import { readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
29545
+ import { basename as basename4, dirname as dirname3, sep as sep3 } from "path";
27010
29546
 
27011
29547
  // ../../packages/plugin-sdk/src/project-files.ts
27012
29548
  var import_ignore = __toESM(require_ignore(), 1);
27013
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27014
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
29549
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
29550
+ import { basename as basename5, join as join12, relative, sep as sep4 } from "path";
29551
+
29552
+ // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
29553
+ var optionalBaseUrl2 = external_exports.preprocess((v) => {
29554
+ if (typeof v === "string" && v.trim() === "") return void 0;
29555
+ return v;
29556
+ }, external_exports.string().optional()).catch(void 0);
29557
+ var optionalFlag = external_exports.preprocess((v) => {
29558
+ if (typeof v !== "string") return false;
29559
+ const normalized = v.trim().toLowerCase();
29560
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
29561
+ }, external_exports.boolean()).catch(false);
29562
+ var antigravityProviderEnvShape = {
29563
+ GOOGLE_GENAI_USE_VERTEXAI: optionalFlag,
29564
+ GOOGLE_GEMINI_BASE_URL: optionalBaseUrl2
29565
+ };
29566
+ var AntigravityProviderEnvSchema = external_exports.object(antigravityProviderEnvShape);
29567
+
29568
+ // ../../packages/plugin-sdk/src/provider-env-codex.ts
29569
+ var optionalBaseUrl3 = external_exports.preprocess((v) => {
29570
+ if (typeof v === "string" && v.trim() === "") return void 0;
29571
+ return v;
29572
+ }, external_exports.string().optional()).catch(void 0);
29573
+ var codexProviderEnvShape = {
29574
+ OPENAI_BASE_URL: optionalBaseUrl3
29575
+ };
29576
+ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
27015
29577
 
27016
29578
  // ../../packages/plugin-sdk/src/runtime.ts
27017
- import { randomUUID as randomUUID10 } from "crypto";
29579
+ import { randomUUID as randomUUID14 } from "crypto";
27018
29580
 
27019
29581
  // ../../packages/plugin-sdk/src/suppressions.ts
27020
29582
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27021
29583
 
27022
29584
  // ../../packages/plugin-sdk/src/throttle.ts
27023
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27024
- import { join as join11 } from "path";
29585
+ import { mkdirSync as mkdirSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
29586
+ import { join as join13 } from "path";
29587
+
29588
+ // ../../packages/plugin-sdk/src/tokenize.ts
29589
+ function redactedPlaceholder(category) {
29590
+ return `[REDACTED:${category.toUpperCase()}]`;
29591
+ }
29592
+ var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
29593
+ var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
29594
+ function groupSpans(text, findings) {
29595
+ const sorted = [...findings].filter((f) => f.span.start >= 0 && f.span.end <= text.length && f.span.start < f.span.end).sort((a, b) => a.span.start - b.span.start || b.span.end - a.span.end);
29596
+ const groups = [];
29597
+ for (const finding of sorted) {
29598
+ const last = groups[groups.length - 1];
29599
+ if (last && finding.span.start < last.end) {
29600
+ last.end = Math.max(last.end, finding.span.end);
29601
+ if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
29602
+ last.category = finding.category;
29603
+ last.severity = finding.severity;
29604
+ }
29605
+ delete last.finding;
29606
+ continue;
29607
+ }
29608
+ groups.push({
29609
+ start: finding.span.start,
29610
+ end: finding.span.end,
29611
+ finding,
29612
+ category: finding.category,
29613
+ severity: finding.severity
29614
+ });
29615
+ }
29616
+ return groups;
29617
+ }
29618
+ var NULL_RESOLVER = () => Promise.resolve(null);
29619
+ var SecretVaultGlue = class {
29620
+ #vault;
29621
+ revealGrantResolver;
29622
+ // Set only when THIS glue opened the store, so a glue over an injected vault
29623
+ // never closes a handle it does not own.
29624
+ #release;
29625
+ constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
29626
+ this.#vault = vault;
29627
+ this.revealGrantResolver = revealGrantResolver;
29628
+ this.#release = release2;
29629
+ }
29630
+ close() {
29631
+ const release2 = this.#release;
29632
+ this.#release = void 0;
29633
+ try {
29634
+ release2?.();
29635
+ } catch {
29636
+ }
29637
+ }
29638
+ async tokenizeValue(raw, meta3) {
29639
+ try {
29640
+ const result = await this.#vault.tokenize(raw, meta3);
29641
+ return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
29642
+ } catch {
29643
+ return redactedPlaceholder(meta3.category);
29644
+ }
29645
+ }
29646
+ async tokenizeText(text, opts) {
29647
+ try {
29648
+ const findings = opts?.findings ?? this.#selfScan(text);
29649
+ if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
29650
+ if (findings.length === 0) return { text, pointers: [], degraded: [] };
29651
+ const groups = groupSpans(text, findings);
29652
+ const pointers = [];
29653
+ const degraded = [];
29654
+ let out = text;
29655
+ for (const group of [...groups].reverse()) {
29656
+ const original = text.slice(group.start, group.end);
29657
+ const finding = group.finding;
29658
+ let replacement;
29659
+ if (finding === void 0) {
29660
+ replacement = redactedPlaceholder(group.category);
29661
+ degraded.unshift({ category: group.category });
29662
+ } else if (original !== finding.rawMatch) {
29663
+ replacement = redactedPlaceholder(group.category);
29664
+ degraded.unshift({ category: group.category });
29665
+ } else {
29666
+ replacement = await this.tokenizeValue(finding.rawMatch, {
29667
+ ruleId: finding.ruleId,
29668
+ category: finding.category,
29669
+ maskedMatch: maskMatch(finding.rawMatch)
29670
+ });
29671
+ if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
29672
+ else degraded.unshift({ category: finding.category });
29673
+ }
29674
+ out = out.slice(0, group.start) + replacement + out.slice(group.end);
29675
+ }
29676
+ if (opts?.sighting && pointers.length > 0) {
29677
+ for (const pointer of pointers) {
29678
+ try {
29679
+ const id = pointer.split(".")[1];
29680
+ if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
29681
+ } catch {
29682
+ }
29683
+ }
29684
+ }
29685
+ return { text: out, pointers, degraded };
29686
+ } catch {
29687
+ return { text: "[REDACTED]", pointers: [], degraded: [] };
29688
+ }
29689
+ }
29690
+ async detokenizeText(text, opts) {
29691
+ try {
29692
+ const matches = [...text.matchAll(pointerTokenScanner())];
29693
+ if (matches.length === 0) return { text, revealed: 0 };
29694
+ const occurrences = /* @__PURE__ */ new Map();
29695
+ for (const match of matches) {
29696
+ occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
29697
+ }
29698
+ const resolved = /* @__PURE__ */ new Map();
29699
+ for (const [pointer, count] of occurrences) {
29700
+ try {
29701
+ const value = await this.#vault.detokenize(pointer, {
29702
+ target: "human",
29703
+ reason: opts.reason,
29704
+ pointerCount: count
29705
+ });
29706
+ resolved.set(pointer, typeof value === "string" ? value : null);
29707
+ } catch {
29708
+ resolved.set(pointer, null);
29709
+ }
29710
+ }
29711
+ let out = text;
29712
+ let revealed = 0;
29713
+ for (const match of [...matches].reverse()) {
29714
+ const value = resolved.get(match[0]);
29715
+ const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
29716
+ if (value !== null && value !== void 0) revealed += 1;
29717
+ out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
29718
+ }
29719
+ return { text: out, revealed };
29720
+ } catch {
29721
+ return { text, revealed: 0 };
29722
+ }
29723
+ }
29724
+ // Scan with the bundled packs, as the mask path does. Pointers already in the
29725
+ // text are blanked first so a pointer is never re-tokenized. Returns null
29726
+ // when the registry or the scan itself failed — the caller must then treat
29727
+ // the whole text as unclassifiable.
29728
+ #selfScan(text) {
29729
+ try {
29730
+ registerBundledPacks();
29731
+ const shielded = shieldPointers(text);
29732
+ return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
29733
+ } catch {
29734
+ return null;
29735
+ }
29736
+ }
29737
+ async describePointerSafe(token) {
29738
+ try {
29739
+ return await this.#vault.describePointer(token);
29740
+ } catch {
29741
+ return null;
29742
+ }
29743
+ }
29744
+ async probeModelPointers(text, opts) {
29745
+ const granted = /* @__PURE__ */ new Map();
29746
+ const ungranted = [];
29747
+ try {
29748
+ for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
29749
+ try {
29750
+ const grantId = await opts.resolveGrant(pointer);
29751
+ if (grantId === null) ungranted.push(pointer);
29752
+ else granted.set(pointer, grantId);
29753
+ } catch {
29754
+ ungranted.push(pointer);
29755
+ }
29756
+ }
29757
+ return { granted, ungranted };
29758
+ } catch {
29759
+ return { granted: /* @__PURE__ */ new Map(), ungranted };
29760
+ }
29761
+ }
29762
+ async substituteModelPointers(text, opts) {
29763
+ try {
29764
+ const matches = [...text.matchAll(pointerTokenScanner())];
29765
+ if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
29766
+ const resolved = /* @__PURE__ */ new Map();
29767
+ for (const pointer of new Set(matches.map((m) => m[0]))) {
29768
+ try {
29769
+ const grantId = await opts.resolveGrant(pointer);
29770
+ if (grantId === null) {
29771
+ await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
29772
+ resolved.set(pointer, null);
29773
+ continue;
29774
+ }
29775
+ const value = await this.#vault.detokenize(pointer, {
29776
+ target: "model",
29777
+ reason: "model-input",
29778
+ grantId
29779
+ });
29780
+ resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
29781
+ } catch {
29782
+ resolved.set(pointer, null);
29783
+ }
29784
+ }
29785
+ const spentGrants = /* @__PURE__ */ new Set();
29786
+ for (const entry of resolved.values()) {
29787
+ if (entry === null || spentGrants.has(entry.grantId)) continue;
29788
+ spentGrants.add(entry.grantId);
29789
+ try {
29790
+ await this.#vault.consumeGrant?.(entry.grantId);
29791
+ } catch {
29792
+ }
29793
+ }
29794
+ let out = text;
29795
+ const revealed = /* @__PURE__ */ new Set();
29796
+ const unresolved = /* @__PURE__ */ new Set();
29797
+ for (const match of [...matches].reverse()) {
29798
+ const entry = resolved.get(match[0]);
29799
+ if (entry === null || entry === void 0) {
29800
+ unresolved.add(match[0]);
29801
+ continue;
29802
+ }
29803
+ revealed.add(match[0]);
29804
+ out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
29805
+ }
29806
+ return {
29807
+ text: out,
29808
+ revealed: [...revealed],
29809
+ unresolved: [...unresolved],
29810
+ grantIds: [...spentGrants]
29811
+ };
29812
+ } catch {
29813
+ return { text, revealed: [], unresolved: [], grantIds: [] };
29814
+ }
29815
+ }
29816
+ };
29817
+ function createVaultGlue(options) {
29818
+ if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
29819
+ const base = options?.base ?? defaultDataDir();
29820
+ try {
29821
+ const dir = dataDir(base);
29822
+ const db = openLocalDatabase(dir);
29823
+ const settings = readWorkspaceSettings(base);
29824
+ const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
29825
+ const vault = new SecretVault({
29826
+ repo: db.secretVault,
29827
+ keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
29828
+ // Read live so a revocation applies to the very next call, not the next
29829
+ // process.
29830
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
29831
+ // This is the one construction site that reveals to the model, so it is
29832
+ // the one that supplies the last gate. The decision is re-taken from the
29833
+ // ROW's identity at the moment of crossing, which closes the window
29834
+ // between resolving a grant and spending it: a grant revoked in between
29835
+ // refuses here.
29836
+ //
29837
+ // The re-decision is on the identity alone, never on the grant id
29838
+ // matching the one the resolver returned. ExceptionPolicyProvider
29839
+ // promises no id stability across calls — a provider deciding from
29840
+ // external policy may well mint a fresh id each time — so comparing ids
29841
+ // would silently refuse every crossing for such a provider while looking
29842
+ // like a security check. `allow` for this row is the whole question.
29843
+ verifyGrant: async (_grantId, identity) => {
29844
+ const decision = await provider.decideReveal(identity);
29845
+ return decision.allow;
29846
+ }
29847
+ });
29848
+ let fingerprintKey;
29849
+ const fingerprintKeyForWrite = () => fingerprintKey ??= loadOrCreateFingerprintKey(dir);
29850
+ const vaultWithSightings = {
29851
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3, fingerprintKeyForWrite),
29852
+ detokenize: (token, opts) => vault.detokenize(token, opts),
29853
+ describePointer: (token) => vault.describePointer(token),
29854
+ resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
29855
+ recordSighting: (pointerId, sighting) => {
29856
+ db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
29857
+ },
29858
+ consumeGrant: (grantId) => db.exceptions.consume(grantId)
29859
+ };
29860
+ const revealGrantResolver = async (pointer) => {
29861
+ try {
29862
+ const identity = await vault.resolvePointerIdentity(pointer);
29863
+ if (identity === null) return null;
29864
+ const decision = await provider.decideReveal(identity);
29865
+ return decision.allow ? decision.grantId : null;
29866
+ } catch {
29867
+ return null;
29868
+ }
29869
+ };
29870
+ return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
29871
+ db.close();
29872
+ });
29873
+ } catch {
29874
+ return new SecretVaultGlue(UNOPENABLE_VAULT);
29875
+ }
29876
+ }
29877
+ var UNOPENABLE_VAULT = {
29878
+ tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29879
+ detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29880
+ describePointer: () => Promise.resolve(null),
29881
+ resolvePointerIdentity: () => Promise.resolve(null)
29882
+ };
27025
29883
 
27026
29884
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
27027
- import { randomUUID as randomUUID11 } from "crypto";
29885
+ import { randomUUID as randomUUID15 } from "crypto";
27028
29886
 
27029
29887
  // ../../packages/plugin-runtime/src/recorder.ts
27030
29888
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -27063,12 +29921,12 @@ var StandaloneDataGateway = class {
27063
29921
  // reconciler drops the whole pass and recovers it idempotently on the next read.
27064
29922
  recordLlmCalls(inputs) {
27065
29923
  if (inputs.length === 0) return Promise.resolve();
27066
- return new Promise((resolve, reject) => {
29924
+ return new Promise((resolve2, reject) => {
27067
29925
  try {
27068
29926
  this.db.auditEvents.runInTransaction(() => {
27069
29927
  for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
27070
29928
  });
27071
- resolve();
29929
+ resolve2();
27072
29930
  } catch (err) {
27073
29931
  reject(err instanceof Error ? err : new Error(String(err)));
27074
29932
  }
@@ -27080,12 +29938,12 @@ var StandaloneDataGateway = class {
27080
29938
  // drops the whole pass and recovers it idempotently next time.
27081
29939
  recordToolCalls(inputs) {
27082
29940
  if (inputs.length === 0) return Promise.resolve();
27083
- return new Promise((resolve, reject) => {
29941
+ return new Promise((resolve2, reject) => {
27084
29942
  try {
27085
29943
  this.db.auditEvents.runInTransaction(() => {
27086
29944
  for (const input of inputs) this.writeToolCall(input);
27087
29945
  });
27088
- resolve();
29946
+ resolve2();
27089
29947
  } catch (err) {
27090
29948
  reject(err instanceof Error ? err : new Error(String(err)));
27091
29949
  }
@@ -27186,7 +30044,7 @@ var StandaloneDataGateway = class {
27186
30044
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
27187
30045
  const installed = this.installedScanRules();
27188
30046
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
27189
- id: randomUUID11(),
30047
+ id: randomUUID15(),
27190
30048
  scope: "global",
27191
30049
  target: { ruleId },
27192
30050
  action,
@@ -27339,97 +30197,20 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
27339
30197
  }
27340
30198
 
27341
30199
  // ../../packages/plugin-runtime/src/handle-session-start.ts
27342
- import { randomUUID as randomUUID12 } from "crypto";
30200
+ import { randomUUID as randomUUID16 } from "crypto";
27343
30201
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
27344
30202
 
27345
- // src/history/tail.ts
27346
- import { createHash as createHash5 } from "crypto";
27347
- import {
27348
- closeSync,
27349
- fstatSync,
27350
- mkdirSync as mkdirSync4,
27351
- openSync,
27352
- readFileSync as readFileSync7,
27353
- readSync,
27354
- writeFileSync as writeFileSync5
27355
- } from "fs";
27356
- import { join as join12 } from "path";
27357
- function offsetsDir(dataDir2) {
27358
- return join12(dataDir2, "usage-offsets");
27359
- }
27360
- var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
27361
- function safeSessionId(sessionId) {
27362
- if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
27363
- return sessionId;
27364
- }
27365
- return createHash5("sha256").update(sessionId).digest("hex");
27366
- }
27367
- function offsetPath(dataDir2, sessionId) {
27368
- return join12(offsetsDir(dataDir2), safeSessionId(sessionId));
27369
- }
27370
- function readOffset(dataDir2, sessionId) {
27371
- try {
27372
- const raw = readFileSync7(offsetPath(dataDir2, sessionId), "utf8");
27373
- const parsed = JSON.parse(raw);
27374
- if (typeof parsed === "object" && parsed !== null) {
27375
- const rec = parsed;
27376
- const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
27377
- const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
27378
- return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
27379
- }
27380
- } catch {
27381
- }
27382
- return { offset: 0 };
27383
- }
27384
- function writeOffset(dataDir2, sessionId, value) {
27385
- try {
27386
- mkdirSync4(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
27387
- const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
27388
- writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
27389
- mode: DATA_FILE_MODE
27390
- });
27391
- } catch {
27392
- }
27393
- }
27394
- function readTail(transcriptPath, startOffset) {
27395
- let fd;
27396
- try {
27397
- fd = openSync(transcriptPath, "r");
27398
- } catch {
27399
- return { chunk: "", nextOffset: startOffset };
27400
- }
27401
- try {
27402
- const size = fstatSync(fd).size;
27403
- const from = size < startOffset ? 0 : startOffset;
27404
- if (from >= size) return { chunk: "", nextOffset: from };
27405
- const length = size - from;
27406
- const buf = Buffer.allocUnsafe(length);
27407
- let filled = 0;
27408
- while (filled < length) {
27409
- const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
27410
- if (bytesRead === 0) break;
27411
- filled += bytesRead;
27412
- }
27413
- const slice = buf.subarray(0, filled);
27414
- const lastNl = slice.lastIndexOf(10);
27415
- if (lastNl === -1) return { chunk: "", nextOffset: from };
27416
- const consumedBytes = lastNl + 1;
27417
- const chunk = slice.subarray(0, consumedBytes).toString("utf8");
27418
- return { chunk, nextOffset: from + consumedBytes };
27419
- } catch {
27420
- return { chunk: "", nextOffset: startOffset };
27421
- } finally {
27422
- try {
27423
- closeSync(fd);
27424
- } catch {
27425
- }
27426
- }
27427
- }
30203
+ // src/remediation/redact.ts
30204
+ import { readFileSync as readFileSync10, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
30205
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
27428
30206
 
27429
30207
  // src/history/transcripts.ts
27430
- import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
30208
+ import { readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
27431
30209
  import { homedir as homedir3 } from "os";
27432
- import { join as join13 } from "path";
30210
+ import { join as join14 } from "path";
30211
+ function transcriptsDir(home) {
30212
+ return join14(home ?? homedir3(), ".claude", "projects");
30213
+ }
27433
30214
  function isRecord(value) {
27434
30215
  return typeof value === "object" && value !== null;
27435
30216
  }
@@ -27631,6 +30412,156 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
27631
30412
  }
27632
30413
  var DAY_MS5 = 24 * 60 * 60 * 1e3;
27633
30414
 
30415
+ // src/remediation/redact.ts
30416
+ function platformRedactionScope(home) {
30417
+ return { artifactRoots: [transcriptsDir(home)] };
30418
+ }
30419
+ function realPathOrNull(path) {
30420
+ try {
30421
+ return realpathSync3(path);
30422
+ } catch {
30423
+ return null;
30424
+ }
30425
+ }
30426
+ function isWithinRoot(realTarget, root) {
30427
+ const realRoot = realPathOrNull(root);
30428
+ if (realRoot === null) return false;
30429
+ const rel = relative2(realRoot, realTarget);
30430
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
30431
+ }
30432
+ function resolveRedactableArtifact(filePath, scope) {
30433
+ const realTarget = realPathOrNull(resolve(filePath));
30434
+ if (realTarget === null) return null;
30435
+ return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
30436
+ }
30437
+
30438
+ // src/history/tail.ts
30439
+ import { createHash as createHash5 } from "crypto";
30440
+ import {
30441
+ closeSync as closeSync2,
30442
+ fstatSync,
30443
+ mkdirSync as mkdirSync5,
30444
+ openSync as openSync2,
30445
+ readFileSync as readFileSync11,
30446
+ readSync,
30447
+ writeFileSync as writeFileSync8
30448
+ } from "fs";
30449
+ import { join as join15 } from "path";
30450
+ function offsetsDir(dataDir2) {
30451
+ return join15(dataDir2, "usage-offsets");
30452
+ }
30453
+ var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
30454
+ function safeSessionId(sessionId) {
30455
+ if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
30456
+ return sessionId;
30457
+ }
30458
+ return createHash5("sha256").update(sessionId).digest("hex");
30459
+ }
30460
+ function offsetPath(dataDir2, sessionId) {
30461
+ return join15(offsetsDir(dataDir2), safeSessionId(sessionId));
30462
+ }
30463
+ function readOffset(dataDir2, sessionId) {
30464
+ try {
30465
+ const raw = readFileSync11(offsetPath(dataDir2, sessionId), "utf8");
30466
+ const parsed = JSON.parse(raw);
30467
+ if (typeof parsed === "object" && parsed !== null) {
30468
+ const rec = parsed;
30469
+ const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
30470
+ const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
30471
+ return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
30472
+ }
30473
+ } catch {
30474
+ }
30475
+ return { offset: 0 };
30476
+ }
30477
+ function writeOffset(dataDir2, sessionId, value) {
30478
+ try {
30479
+ mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
30480
+ const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
30481
+ writeFileSync8(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
30482
+ mode: DATA_FILE_MODE
30483
+ });
30484
+ } catch {
30485
+ }
30486
+ }
30487
+ function readTail(transcriptPath, startOffset) {
30488
+ let fd;
30489
+ try {
30490
+ fd = openSync2(transcriptPath, "r");
30491
+ } catch {
30492
+ return { chunk: "", nextOffset: startOffset };
30493
+ }
30494
+ try {
30495
+ const size = fstatSync(fd).size;
30496
+ const from = size < startOffset ? 0 : startOffset;
30497
+ if (from >= size) return { chunk: "", nextOffset: from };
30498
+ const length = size - from;
30499
+ const buf = Buffer.allocUnsafe(length);
30500
+ let filled = 0;
30501
+ while (filled < length) {
30502
+ const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
30503
+ if (bytesRead === 0) break;
30504
+ filled += bytesRead;
30505
+ }
30506
+ const slice = buf.subarray(0, filled);
30507
+ const lastNl = slice.lastIndexOf(10);
30508
+ if (lastNl === -1) return { chunk: "", nextOffset: from };
30509
+ const consumedBytes = lastNl + 1;
30510
+ const chunk = slice.subarray(0, consumedBytes).toString("utf8");
30511
+ return { chunk, nextOffset: from + consumedBytes };
30512
+ } catch {
30513
+ return { chunk: "", nextOffset: startOffset };
30514
+ } finally {
30515
+ try {
30516
+ closeSync2(fd);
30517
+ } catch {
30518
+ }
30519
+ }
30520
+ }
30521
+
30522
+ // src/history/tail-scrub.ts
30523
+ import { readFileSync as readFileSync12, renameSync as renameSync6, rmSync as rmSync6, statSync as statSync7, writeFileSync as writeFileSync9 } from "fs";
30524
+ var DEFAULT_MAX_SCRUB_BYTES = 32 * 1024 * 1024;
30525
+ async function scrubTranscriptTail(filePath, deps) {
30526
+ try {
30527
+ const realPath = resolveRedactableArtifact(filePath, deps.scope);
30528
+ if (realPath === null) return null;
30529
+ const statBefore = statSync7(realPath);
30530
+ if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
30531
+ const content = readFileSync12(realPath, "utf8");
30532
+ const lines = content.split("\n");
30533
+ let rewritten = 0;
30534
+ for (const [i, line] of lines.entries()) {
30535
+ if (line === "") continue;
30536
+ const result = await deps.tokenizeText(line);
30537
+ if (result.text === line) continue;
30538
+ if (result.pointers.length === 0 && result.degraded.length === 0) return null;
30539
+ lines[i] = result.text;
30540
+ rewritten += 1;
30541
+ }
30542
+ if (rewritten === 0) return { rewritten: 0 };
30543
+ const tmpPath = `${realPath}.aka-scrub.tmp`;
30544
+ try {
30545
+ writeFileSync9(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
30546
+ const statNow = statSync7(realPath);
30547
+ if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
30548
+ rmSync6(tmpPath, { force: true, recursive: true });
30549
+ return null;
30550
+ }
30551
+ renameSync6(tmpPath, realPath);
30552
+ } catch {
30553
+ try {
30554
+ rmSync6(tmpPath, { force: true, recursive: true });
30555
+ } catch {
30556
+ }
30557
+ return null;
30558
+ }
30559
+ return { rewritten };
30560
+ } catch {
30561
+ return null;
30562
+ }
30563
+ }
30564
+
27634
30565
  // src/history/usage.ts
27635
30566
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
27636
30567
  var MAX_TARGET_LEN = 500;
@@ -27760,6 +30691,24 @@ async function reconcileSessionTail(config2, sessionId, transcriptPath) {
27760
30691
  offset: nextOffset,
27761
30692
  lastPromptId: result.lastPromptId
27762
30693
  });
30694
+ if (isVaultConsentValid(config2.settings.vaultConsent)) {
30695
+ try {
30696
+ const glue = createVaultGlue();
30697
+ const scrubbed = await scrubTranscriptTail(transcriptPath, {
30698
+ tokenizeText: (text) => glue.tokenizeText(text, {
30699
+ sighting: { location: transcriptPath, kind: "transcript" }
30700
+ }),
30701
+ scope: platformRedactionScope()
30702
+ });
30703
+ if (scrubbed !== null && scrubbed.rewritten > 0) {
30704
+ writeOffset(config2.dataDir, sessionId, {
30705
+ offset: 0,
30706
+ lastPromptId: result.lastPromptId
30707
+ });
30708
+ }
30709
+ } catch {
30710
+ }
30711
+ }
27763
30712
  return { llmCalls: result.llmCalls, skipped: result.skipped, toolCalls };
27764
30713
  } finally {
27765
30714
  await gateway.close();