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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scripts/query.js CHANGED
@@ -492,7 +492,7 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/persistence/src/database.ts
495
- import { randomUUID as randomUUID8 } from "crypto";
495
+ import { randomUUID as randomUUID9 } from "crypto";
496
496
  import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
497
497
  import { join, sep } from "path";
498
498
  import { DatabaseSync } from "node:sqlite";
@@ -558,6 +558,22 @@ var SQLITE_MIGRATIONS = [
558
558
  {
559
559
  tag: "0014_drop_legacy_events_findings",
560
560
  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"
561
+ },
562
+ {
563
+ tag: "0015_busy_vengeance",
564
+ 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`);"
565
+ },
566
+ {
567
+ tag: "0016_breezy_zodiak",
568
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
569
+ },
570
+ {
571
+ tag: "0017_rainy_kat_farrell",
572
+ 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`);"
573
+ },
574
+ {
575
+ tag: "0018_serious_tana_nile",
576
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
561
577
  }
562
578
  ];
563
579
 
@@ -16274,6 +16290,7 @@ var ExceptionConditions = external_exports.object({
16274
16290
  sourceTool: external_exports.string().optional(),
16275
16291
  provider: external_exports.string().optional()
16276
16292
  }).strict();
16293
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16277
16294
  var DetectionException = external_exports.object({
16278
16295
  id: external_exports.guid(),
16279
16296
  ruleId: external_exports.string(),
@@ -16290,6 +16307,7 @@ var DetectionException = external_exports.object({
16290
16307
  keyVersion: external_exports.number().int().positive(),
16291
16308
  // maskMatch() preview of the approved value — never the raw value.
16292
16309
  maskedValue: external_exports.string(),
16310
+ capability: ExceptionCapability.default("suppress"),
16293
16311
  scope: ExceptionScope,
16294
16312
  expiresAt: external_exports.iso.datetime().nullable(),
16295
16313
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16313,6 +16331,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16313
16331
  ruleId: true,
16314
16332
  valueFingerprint: true,
16315
16333
  keyVersion: true,
16334
+ capability: true,
16316
16335
  expiresAt: true,
16317
16336
  maxUses: true,
16318
16337
  useCount: true,
@@ -17417,8 +17436,117 @@ var PatchInstalledPackRequest = external_exports.object({
17417
17436
  message: "At least one field must be provided"
17418
17437
  }).meta({ id: "PatchInstalledPackRequest" });
17419
17438
 
17439
+ // ../../packages/schema/src/zod/vault.ts
17440
+ var POINTER_FORMAT_VERSION = 2;
17441
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17442
+ var POINTER_TOKEN_PATTERN = new RegExp(
17443
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17444
+ );
17445
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17446
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17447
+ var ParsedPointer = external_exports.object({
17448
+ category: DetectionCategory,
17449
+ keyVersion: external_exports.number().int().positive(),
17450
+ pointerId: external_exports.string(),
17451
+ tag: external_exports.string()
17452
+ });
17453
+ var VaultEntry = external_exports.object({
17454
+ pointerId: external_exports.string(),
17455
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17456
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17457
+ // independently of the vault encryption key below.
17458
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17459
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17460
+ // The vault-key epoch this row's ciphertext was sealed under.
17461
+ keyVersion: external_exports.number().int().positive(),
17462
+ // Fixed at first mint and never updated: the same value detected later under a
17463
+ // different rule's category keeps the category it was minted with, so one
17464
+ // value always produces exactly one wire token.
17465
+ category: DetectionCategory,
17466
+ ruleId: external_exports.string(),
17467
+ // Partial-reveal preview for badges and listings. Never the raw value.
17468
+ maskedMatch: external_exports.string(),
17469
+ provider: external_exports.string().optional(),
17470
+ ciphertext: external_exports.string(),
17471
+ nonce: external_exports.string(),
17472
+ authTag: external_exports.string(),
17473
+ // How many times this value has been detected on this machine — the reuse
17474
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17475
+ occurrenceCount: external_exports.number().int().nonnegative(),
17476
+ firstSeen: external_exports.string(),
17477
+ lastSeen: external_exports.string()
17478
+ });
17479
+ var PointerDescriptor = external_exports.object({
17480
+ category: DetectionCategory,
17481
+ provider: external_exports.string().optional(),
17482
+ maskedMatch: external_exports.string(),
17483
+ occurrences: external_exports.number().int().nonnegative(),
17484
+ firstSeen: external_exports.string(),
17485
+ lastSeen: external_exports.string()
17486
+ });
17487
+ var PointerIdentity = external_exports.object({
17488
+ ruleId: external_exports.string(),
17489
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17490
+ fingerprintKeyVersion: external_exports.number().int().positive()
17491
+ });
17492
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17493
+ var VaultDerefReason = external_exports.enum([
17494
+ "display",
17495
+ "explicit-reveal",
17496
+ "view-render",
17497
+ "model-input",
17498
+ "remediation",
17499
+ "purge"
17500
+ ]);
17501
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17502
+ var VaultDeref = external_exports.object({
17503
+ id: external_exports.guid(),
17504
+ pointerId: external_exports.string(),
17505
+ at: external_exports.string(),
17506
+ target: DetokenizeTarget,
17507
+ reason: VaultDerefReason,
17508
+ outcome: VaultDerefOutcome,
17509
+ // Present only on a model-target crossing that a reveal grant authorized.
17510
+ grantId: external_exports.string().optional(),
17511
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17512
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17513
+ pointerCount: external_exports.number().int().positive().default(1)
17514
+ });
17515
+ var VaultSightingKind = external_exports.enum([
17516
+ "prompt",
17517
+ "tool-input",
17518
+ "tool-output",
17519
+ "file",
17520
+ "transcript"
17521
+ ]);
17522
+ var VaultSighting = external_exports.object({
17523
+ location: external_exports.string(),
17524
+ kind: VaultSightingKind,
17525
+ firstSeen: external_exports.string(),
17526
+ lastSeen: external_exports.string()
17527
+ });
17528
+ var VaultInventoryEntry = external_exports.object({
17529
+ pointerId: external_exports.string(),
17530
+ category: DetectionCategory,
17531
+ provider: external_exports.string().optional(),
17532
+ maskedMatch: external_exports.string(),
17533
+ occurrences: external_exports.number().int().nonnegative(),
17534
+ firstSeen: external_exports.string(),
17535
+ lastSeen: external_exports.string(),
17536
+ // The active reveal-to-model grant covering this value, when one exists —
17537
+ // the inventory badges it, the row links to revocation.
17538
+ revealGrantId: external_exports.string().nullable(),
17539
+ sightings: external_exports.array(VaultSighting)
17540
+ });
17541
+ var VaultKeyCustody = external_exports.string();
17542
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17543
+ var VaultConsent = external_exports.object({
17544
+ acknowledgedAt: external_exports.iso.datetime(),
17545
+ version: external_exports.number().int().positive()
17546
+ });
17547
+
17420
17548
  // ../../packages/schema/src/zod/local.ts
17421
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17549
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17422
17550
  var RunMode = external_exports.enum(["standalone"]);
17423
17551
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17424
17552
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17440,6 +17568,16 @@ var WorkspaceSettings = external_exports.object({
17440
17568
  // In-place egress extraction on the scan paths; disable to stop all Data
17441
17569
  // Shares writes.
17442
17570
  dataSharesInPlace: external_exports.boolean().default(true),
17571
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17572
+ // vault, instead of destroying them. Absent by default: this is a custody
17573
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17574
+ // Revoking stops future vaulting; it does not erase what is already stored —
17575
+ // purging the vault is the eraser.
17576
+ vaultConsent: VaultConsent.optional(),
17577
+ // Where the vault master key lives.
17578
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17579
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17580
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17443
17581
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17444
17582
  onboardedAt: external_exports.iso.datetime().optional(),
17445
17583
  // Records that the user consented to sending findings to the model API for
@@ -19837,6 +19975,9 @@ var AmbiguousExceptionIdError = class extends Error {
19837
19975
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19838
19976
  AND (expires_at IS NULL OR expires_at > :now)
19839
19977
  AND (max_uses IS NULL OR use_count < max_uses)`;
19978
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19979
+ AND conditions IS NULL
19980
+ AND ${ACTIVE_PREDICATE}`;
19840
19981
  var SqliteExceptionsRepository = class {
19841
19982
  constructor(db) {
19842
19983
  this.db = db;
@@ -19928,11 +20069,11 @@ var SqliteExceptionsRepository = class {
19928
20069
  this.db.prepare(
19929
20070
  `INSERT INTO exceptions (
19930
20071
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19931
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19932
- conditions, created_by, created_via, created_at, updated_at
20072
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20073
+ justification, conditions, created_by, created_via, created_at, updated_at
19933
20074
  ) VALUES (
19934
20075
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19935
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20076
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19936
20077
  :conditions, :createdBy, :createdVia, :now, :now
19937
20078
  )`
19938
20079
  ).run({
@@ -19942,6 +20083,7 @@ var SqliteExceptionsRepository = class {
19942
20083
  valueFingerprint: input.valueFingerprint,
19943
20084
  keyVersion: input.keyVersion,
19944
20085
  maskedValue: input.maskedValue,
20086
+ capability: input.capability ?? "suppress",
19945
20087
  scope: input.scope,
19946
20088
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19947
20089
  maxUses: input.maxUses,
@@ -20035,6 +20177,7 @@ var SqliteExceptionsRepository = class {
20035
20177
  ruleId: row.rule_id,
20036
20178
  valueFingerprint: row.value_fingerprint,
20037
20179
  keyVersion: row.key_version,
20180
+ capability: row.capability,
20038
20181
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20039
20182
  maxUses: row.max_uses,
20040
20183
  useCount: row.use_count,
@@ -20089,6 +20232,35 @@ var SqliteExceptionsRepository = class {
20089
20232
  }))
20090
20233
  );
20091
20234
  }
20235
+ /**
20236
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20237
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20238
+ * suppression uses — plus the capability: a suppression grant must never
20239
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20240
+ * revealed value re-enters the detection scan immediately afterward and the
20241
+ * suppression match there claims the use — one crossing, one use.
20242
+ *
20243
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20244
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20245
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20246
+ */
20247
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20248
+ try {
20249
+ const row = getRow(
20250
+ this.db.prepare(
20251
+ `SELECT id FROM exceptions
20252
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20253
+ AND key_version = :keyVersion
20254
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20255
+ LIMIT 1`
20256
+ ),
20257
+ { ruleId, valueFingerprint, keyVersion, now }
20258
+ );
20259
+ return Promise.resolve(row ?? null);
20260
+ } catch (err) {
20261
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20262
+ }
20263
+ }
20092
20264
  /**
20093
20265
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20094
20266
  * exhausted) whose last transition is older than the retention window.
@@ -20116,6 +20288,7 @@ function parseExceptionRow(row) {
20116
20288
  valueFingerprint: row.value_fingerprint,
20117
20289
  keyVersion: row.key_version,
20118
20290
  maskedValue: row.masked_value,
20291
+ capability: row.capability,
20119
20292
  scope: row.scope,
20120
20293
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20121
20294
  maxUses: row.max_uses,
@@ -22281,6 +22454,287 @@ var SqliteScanLedgerRepository = class {
22281
22454
  }
22282
22455
  };
22283
22456
 
22457
+ // ../../packages/persistence/src/repositories/secret-vault.ts
22458
+ import { randomUUID as randomUUID7 } from "crypto";
22459
+ var SELECT_COLUMNS = `
22460
+ pointer_id AS pointerId,
22461
+ value_fingerprint AS valueFingerprint,
22462
+ fingerprint_key_version AS fingerprintKeyVersion,
22463
+ key_version AS keyVersion,
22464
+ format_version AS formatVersion,
22465
+ category,
22466
+ rule_id AS ruleId,
22467
+ masked_match AS maskedMatch,
22468
+ provider,
22469
+ ciphertext,
22470
+ nonce,
22471
+ auth_tag AS authTag,
22472
+ occurrence_count AS occurrenceCount,
22473
+ first_seen AS firstSeen,
22474
+ last_seen AS lastSeen`;
22475
+ function toRow(raw) {
22476
+ const { provider, ...rest } = raw;
22477
+ return provider === null ? rest : { ...rest, provider };
22478
+ }
22479
+ var SqliteSecretVaultRepository = class {
22480
+ constructor(db) {
22481
+ this.db = db;
22482
+ this.insertStmt = db.prepare(
22483
+ `INSERT INTO secret_vault (
22484
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
22485
+ format_version, category, rule_id, masked_match, provider,
22486
+ ciphertext, nonce, auth_tag,
22487
+ occurrence_count, first_seen, last_seen
22488
+ ) VALUES (
22489
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
22490
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
22491
+ :ciphertext, :nonce, :authTag,
22492
+ 1, :now, :now
22493
+ )`
22494
+ );
22495
+ this.bumpStmt = db.prepare(
22496
+ `UPDATE secret_vault
22497
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
22498
+ WHERE value_fingerprint = :valueFingerprint`
22499
+ );
22500
+ this.byPointerStmt = db.prepare(
22501
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
22502
+ );
22503
+ this.byFingerprintStmt = db.prepare(
22504
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
22505
+ );
22506
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
22507
+ this.replaceCiphertextStmt = db.prepare(
22508
+ `UPDATE secret_vault
22509
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
22510
+ WHERE pointer_id = :pointerId`
22511
+ );
22512
+ this.refreshFingerprintStmt = db.prepare(
22513
+ `UPDATE secret_vault
22514
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
22515
+ WHERE pointer_id = :pointerId`
22516
+ );
22517
+ this.derefStmt = db.prepare(
22518
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
22519
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
22520
+ );
22521
+ }
22522
+ db;
22523
+ insertStmt;
22524
+ bumpStmt;
22525
+ byPointerStmt;
22526
+ byFingerprintStmt;
22527
+ listStmt;
22528
+ replaceCiphertextStmt;
22529
+ refreshFingerprintStmt;
22530
+ derefStmt;
22531
+ /**
22532
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
22533
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
22534
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
22535
+ * pointer, category and ciphertext, so the same secret always resolves to one
22536
+ * wire token. `minted` is true only when this call created the row.
22537
+ *
22538
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
22539
+ * writers cannot both decide they are minting.
22540
+ */
22541
+ upsert(input, now) {
22542
+ let minted = false;
22543
+ withTransaction(
22544
+ this.db,
22545
+ () => {
22546
+ const existing = getRow(this.byFingerprintStmt, {
22547
+ valueFingerprint: input.valueFingerprint
22548
+ });
22549
+ if (existing === void 0) {
22550
+ this.insertStmt.run(
22551
+ bindParams({
22552
+ pointerId: input.pointerId,
22553
+ valueFingerprint: input.valueFingerprint,
22554
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
22555
+ keyVersion: input.keyVersion,
22556
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
22557
+ category: input.category,
22558
+ ruleId: input.ruleId,
22559
+ maskedMatch: input.maskedMatch,
22560
+ provider: input.provider,
22561
+ ciphertext: input.ciphertext,
22562
+ nonce: input.nonce,
22563
+ authTag: input.authTag,
22564
+ now
22565
+ })
22566
+ );
22567
+ minted = true;
22568
+ return;
22569
+ }
22570
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
22571
+ },
22572
+ "IMMEDIATE"
22573
+ );
22574
+ const row = getRow(this.byFingerprintStmt, {
22575
+ valueFingerprint: input.valueFingerprint
22576
+ });
22577
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
22578
+ return { row: toRow(row), minted };
22579
+ }
22580
+ byPointerId(pointerId) {
22581
+ const raw = getRow(this.byPointerStmt, { pointerId });
22582
+ return raw === void 0 ? null : toRow(raw);
22583
+ }
22584
+ byValueFingerprint(fingerprint) {
22585
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
22586
+ return raw === void 0 ? null : toRow(raw);
22587
+ }
22588
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
22589
+ recordDeref(entry) {
22590
+ this.derefStmt.run(
22591
+ bindParams({
22592
+ id: entry.id,
22593
+ pointerId: entry.pointerId,
22594
+ at: entry.at,
22595
+ target: entry.target,
22596
+ reason: entry.reason,
22597
+ outcome: entry.outcome,
22598
+ grantId: entry.grantId,
22599
+ pointerCount: entry.pointerCount ?? 1
22600
+ })
22601
+ );
22602
+ }
22603
+ listAll() {
22604
+ return allRows(this.listStmt).map(toRow);
22605
+ }
22606
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
22607
+ replaceCiphertext(pointerId, next) {
22608
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
22609
+ }
22610
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
22611
+ refreshFingerprint(pointerId, next) {
22612
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
22613
+ }
22614
+ /**
22615
+ * Destroy every vaulted value and report how many were destroyed. The deref
22616
+ * audit is left alone on purpose — see the table note above.
22617
+ */
22618
+ purgeAll() {
22619
+ let destroyed = 0;
22620
+ withTransaction(
22621
+ this.db,
22622
+ () => {
22623
+ destroyed = this.countEntries();
22624
+ this.db.exec("DELETE FROM secret_vault");
22625
+ },
22626
+ "IMMEDIATE"
22627
+ );
22628
+ return destroyed;
22629
+ }
22630
+ /**
22631
+ * Record (or re-stamp) one place a pointer has been written. One row per
22632
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
22633
+ * on hook paths — a failure must never affect the rewrite that triggered it,
22634
+ * so callers wrap this, not the other way around.
22635
+ */
22636
+ recordSighting(entry, now) {
22637
+ this.db.prepare(
22638
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
22639
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
22640
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22641
+ ).run({
22642
+ id: randomUUID7(),
22643
+ pointerId: entry.pointerId,
22644
+ location: entry.location,
22645
+ kind: entry.kind,
22646
+ now
22647
+ });
22648
+ }
22649
+ listSightings(pointerId) {
22650
+ const rows = allRows(
22651
+ this.db.prepare(
22652
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22653
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
22654
+ ),
22655
+ { pointerId }
22656
+ );
22657
+ return rows.map((r) => ({
22658
+ location: r.location,
22659
+ kind: r.kind,
22660
+ firstSeen: new Date(r.first_seen).toISOString(),
22661
+ lastSeen: new Date(r.last_seen).toISOString()
22662
+ }));
22663
+ }
22664
+ /**
22665
+ * The dashboard inventory: every vaulted value's descriptor data joined with
22666
+ * its sightings and the active reveal-to-model grant when one exists.
22667
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
22668
+ * columns are selected.
22669
+ */
22670
+ listInventory(now = Date.now()) {
22671
+ const rows = allRows(
22672
+ this.db.prepare(
22673
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22674
+ v.occurrence_count, v.first_seen, v.last_seen,
22675
+ (SELECT e.id FROM exceptions e
22676
+ WHERE e.rule_id = v.rule_id
22677
+ AND e.value_fingerprint = v.value_fingerprint
22678
+ AND e.key_version = v.fingerprint_key_version
22679
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22680
+ LIMIT 1) AS grant_id
22681
+ FROM secret_vault v
22682
+ ORDER BY v.last_seen DESC`
22683
+ ),
22684
+ { now }
22685
+ );
22686
+ return rows.map((r) => ({
22687
+ pointerId: r.pointer_id,
22688
+ category: r.category,
22689
+ ...r.provider === null ? {} : { provider: r.provider },
22690
+ maskedMatch: r.masked_match,
22691
+ occurrences: r.occurrence_count,
22692
+ firstSeen: new Date(r.first_seen).toISOString(),
22693
+ lastSeen: new Date(r.last_seen).toISOString(),
22694
+ revealGrantId: r.grant_id,
22695
+ sightings: this.listSightings(r.pointer_id)
22696
+ }));
22697
+ }
22698
+ /**
22699
+ * The de-reference trail, newest first. By default the batched, high-volume
22700
+ * reasons (display, view-render) are hidden and counted instead — the rows
22701
+ * that matter as a signal are the model crossings, and burying them under
22702
+ * render noise would defeat the audit's purpose.
22703
+ */
22704
+ listDerefs(opts) {
22705
+ const limit = opts?.limit ?? 200;
22706
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
22707
+ const rows = allRows(
22708
+ this.db.prepare(
22709
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22710
+ FROM secret_vault_deref ${where}
22711
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
22712
+ ),
22713
+ { limit }
22714
+ );
22715
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
22716
+ this.db,
22717
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22718
+ );
22719
+ return {
22720
+ rows: rows.map((r) => ({
22721
+ id: r.id,
22722
+ pointerId: r.pointer_id,
22723
+ at: new Date(r.at).toISOString(),
22724
+ target: r.target,
22725
+ reason: r.reason,
22726
+ outcome: r.outcome,
22727
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
22728
+ pointerCount: r.pointer_count
22729
+ })),
22730
+ hiddenBatched
22731
+ };
22732
+ }
22733
+ countEntries() {
22734
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22735
+ }
22736
+ };
22737
+
22284
22738
  // ../../packages/persistence/src/repositories/security.ts
22285
22739
  var DAY_MS4 = 864e5;
22286
22740
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22626,7 +23080,7 @@ var SqliteSecurityRepository = class {
22626
23080
  };
22627
23081
 
22628
23082
  // ../../packages/persistence/src/repositories/shares.ts
22629
- import { randomUUID as randomUUID7 } from "crypto";
23083
+ import { randomUUID as randomUUID8 } from "crypto";
22630
23084
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22631
23085
  var IN_CHUNK = 500;
22632
23086
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22882,7 +23336,7 @@ var SqliteSharesRepository = class {
22882
23336
  (id, destination_id, host, decision, created_at, updated_at)
22883
23337
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22884
23338
  ).run({
22885
- id: randomUUID7(),
23339
+ id: randomUUID8(),
22886
23340
  destinationId,
22887
23341
  host: dest.host,
22888
23342
  decision,
@@ -23031,7 +23485,7 @@ var SqliteSharesRepository = class {
23031
23485
  let destinationId = destIds.get(hit.host);
23032
23486
  if (destinationId === void 0) {
23033
23487
  destStmt.run({
23034
- id: randomUUID7(),
23488
+ id: randomUUID8(),
23035
23489
  kind: hit.kind,
23036
23490
  name: hit.name,
23037
23491
  host: hit.host,
@@ -23047,7 +23501,7 @@ var SqliteSharesRepository = class {
23047
23501
  let endpointId = endpointIds.get(endpointKey);
23048
23502
  if (endpointId === void 0) {
23049
23503
  endpointStmt.run({
23050
- id: randomUUID7(),
23504
+ id: randomUUID8(),
23051
23505
  destinationId,
23052
23506
  method: hit.method,
23053
23507
  transport: hit.transport,
@@ -23060,7 +23514,7 @@ var SqliteSharesRepository = class {
23060
23514
  endpointIds.set(endpointKey, endpointId);
23061
23515
  }
23062
23516
  siteStmt.run({
23063
- id: randomUUID7(),
23517
+ id: randomUUID8(),
23064
23518
  endpointId,
23065
23519
  project: input.project,
23066
23520
  projectKey: input.projectKey,
@@ -23428,11 +23882,22 @@ function purgeSampleData(db) {
23428
23882
  function linkHost(input, hostId) {
23429
23883
  return hostId ? { ...input, hostId } : input;
23430
23884
  }
23885
+ function closeQuietly(db) {
23886
+ try {
23887
+ db.close();
23888
+ } catch {
23889
+ }
23890
+ }
23431
23891
  function openWithPragmas(file2) {
23432
23892
  const db = new DatabaseSync(file2);
23433
- db.exec("PRAGMA journal_mode = WAL");
23434
- db.exec("PRAGMA busy_timeout = 2000");
23435
- db.exec("PRAGMA foreign_keys = ON");
23893
+ try {
23894
+ db.exec("PRAGMA journal_mode = WAL");
23895
+ db.exec("PRAGMA busy_timeout = 2000");
23896
+ db.exec("PRAGMA foreign_keys = ON");
23897
+ } catch (err) {
23898
+ closeQuietly(db);
23899
+ throw err;
23900
+ }
23436
23901
  return db;
23437
23902
  }
23438
23903
  function backupLegacyStore(file2) {
@@ -23444,43 +23909,82 @@ function backupLegacyStore(file2) {
23444
23909
  }
23445
23910
  return backup;
23446
23911
  }
23912
+ function openAndInitialize(file2) {
23913
+ let db = openWithPragmas(file2);
23914
+ try {
23915
+ if (isForeignSqliteLineage(db)) {
23916
+ db.close();
23917
+ const backup = backupLegacyStore(file2);
23918
+ db = openWithPragmas(file2);
23919
+ akaWarn(
23920
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23921
+ );
23922
+ }
23923
+ applyMigrations(db, file2);
23924
+ tightenPerms(file2);
23925
+ const policies = new SqlitePoliciesRepository(db);
23926
+ const installedPacks = new SqliteInstalledPacksRepository(db);
23927
+ const repositories = {
23928
+ events: new SqliteEventsRepository(db),
23929
+ findings: new SqliteFindingsRepository(db),
23930
+ policies,
23931
+ installedPacks,
23932
+ scanLedger: new SqliteScanLedgerRepository(db),
23933
+ secretVault: new SqliteSecretVaultRepository(db),
23934
+ exceptions: new SqliteExceptionsRepository(db),
23935
+ resolutions: new SqliteResolutionsRepository(db),
23936
+ ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
23937
+ security: new SqliteSecurityRepository(db),
23938
+ detections: new SqliteDetectionsRepository(db),
23939
+ shares: new SqliteSharesRepository(db),
23940
+ policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
23941
+ inventory: new SqliteInventoryRepository(db),
23942
+ inventoryAssets: new SqliteInventoryAssetsRepository(db),
23943
+ projectFiles: new SqliteProjectFilesRepository(db),
23944
+ activity: new SqliteActivityRepository(db),
23945
+ sourceProject: new SqliteSourceProjectRepository(db),
23946
+ auditEvents: new SqliteAuditEventsRepository(db),
23947
+ classifiedData: new SqliteClassifiedDataRepository(db),
23948
+ inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
23949
+ inspectionFindings: new SqliteInspectionFindingsRepository(db),
23950
+ configInventory: new SqliteConfigInventoryRepository(db)
23951
+ };
23952
+ policies.seedDefaults();
23953
+ return { db, ...repositories };
23954
+ } catch (err) {
23955
+ closeQuietly(db);
23956
+ throw err;
23957
+ }
23958
+ }
23447
23959
  function openLocalDatabase(dir) {
23448
23960
  ensureDataDirSync(dir);
23449
23961
  const file2 = join(dir, DB_FILENAME);
23450
- let db = openWithPragmas(file2);
23451
- if (isForeignSqliteLineage(db)) {
23452
- db.close();
23453
- const backup = backupLegacyStore(file2);
23454
- db = openWithPragmas(file2);
23455
- akaWarn(
23456
- `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23457
- );
23458
- }
23459
- applyMigrations(db, file2);
23460
- tightenPerms(file2);
23461
- const events = new SqliteEventsRepository(db);
23462
- const findings = new SqliteFindingsRepository(db);
23463
- const policies = new SqlitePoliciesRepository(db);
23464
- const installedPacks = new SqliteInstalledPacksRepository(db);
23465
- const scanLedger = new SqliteScanLedgerRepository(db);
23466
- const exceptions = new SqliteExceptionsRepository(db);
23467
- const resolutions = new SqliteResolutionsRepository(db);
23468
- const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
23469
- const security = new SqliteSecurityRepository(db);
23470
- const detections = new SqliteDetectionsRepository(db);
23471
- const shares = new SqliteSharesRepository(db);
23472
- const policyCatalog = new SqlitePolicyCatalogRepository(installedPacks);
23473
- const inventory = new SqliteInventoryRepository(db);
23474
- const inventoryAssets = new SqliteInventoryAssetsRepository(db);
23475
- const projectFiles = new SqliteProjectFilesRepository(db);
23476
- const activity = new SqliteActivityRepository(db);
23477
- const sourceProject = new SqliteSourceProjectRepository(db);
23478
- const auditEvents = new SqliteAuditEventsRepository(db);
23479
- const classifiedData = new SqliteClassifiedDataRepository(db);
23480
- const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
23481
- const inspectionFindings = new SqliteInspectionFindingsRepository(db);
23482
- const configInventory = new SqliteConfigInventoryRepository(db);
23483
- policies.seedDefaults();
23962
+ const {
23963
+ db,
23964
+ events,
23965
+ findings,
23966
+ policies,
23967
+ installedPacks,
23968
+ scanLedger,
23969
+ secretVault,
23970
+ exceptions,
23971
+ resolutions,
23972
+ ruleProbeCache,
23973
+ security,
23974
+ detections,
23975
+ shares,
23976
+ policyCatalog,
23977
+ inventory,
23978
+ inventoryAssets,
23979
+ projectFiles,
23980
+ activity,
23981
+ sourceProject,
23982
+ auditEvents,
23983
+ classifiedData,
23984
+ inspectionDefinitions,
23985
+ inspectionFindings,
23986
+ configInventory
23987
+ } = openAndInitialize(file2);
23484
23988
  function recordCapture(event, detected) {
23485
23989
  failOpenTransaction(db, () => {
23486
23990
  const sessionId = event.metadata?.sessionId;
@@ -23571,7 +24075,7 @@ function openLocalDatabase(dir) {
23571
24075
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23572
24076
  if (!definitionId) continue;
23573
24077
  inspectionFindings.insertFinding({
23574
- id: randomUUID8(),
24078
+ id: randomUUID9(),
23575
24079
  auditEventId: record2.scanEvent.id,
23576
24080
  inspectionDefinitionId: definitionId,
23577
24081
  span: finding.span,
@@ -23648,6 +24152,7 @@ function openLocalDatabase(dir) {
23648
24152
  policies,
23649
24153
  installedPacks,
23650
24154
  scanLedger,
24155
+ secretVault,
23651
24156
  exceptions,
23652
24157
  resolutions,
23653
24158
  ruleProbeCache,
@@ -23685,8 +24190,9 @@ import { createHash as createHash3 } from "crypto";
23685
24190
 
23686
24191
  // ../../packages/persistence/src/fingerprint.ts
23687
24192
  import { createHmac, randomBytes } from "crypto";
23688
- import { readFileSync } from "fs";
24193
+ import { existsSync as existsSync2, readFileSync } from "fs";
23689
24194
  import { join as join2 } from "path";
24195
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23690
24196
  var KEY_FILENAME = "exception.key";
23691
24197
  var KEY_MATERIAL_BYTES = 32;
23692
24198
  function keyFilePath(dataDir2) {
@@ -23779,23 +24285,49 @@ function readJson(file2) {
23779
24285
  return parseJsonObject(text) ?? null;
23780
24286
  }
23781
24287
 
23782
- // ../../packages/persistence/src/warn-era-cap.ts
23783
- import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
24288
+ // ../../packages/persistence/src/vault/crypto.ts
24289
+ import {
24290
+ createCipheriv,
24291
+ createDecipheriv,
24292
+ createHmac as createHmac2,
24293
+ hkdfSync,
24294
+ timingSafeEqual
24295
+ } from "crypto";
24296
+
24297
+ // ../../packages/persistence/src/vault/key-provider.ts
24298
+ import { execFileSync } from "child_process";
24299
+ import { randomBytes as randomBytes2 } from "crypto";
24300
+ import {
24301
+ chmodSync as chmodSync2,
24302
+ mkdirSync as mkdirSync2,
24303
+ readFileSync as readFileSync3,
24304
+ renameSync as renameSync4,
24305
+ rmSync as rmSync3,
24306
+ statSync,
24307
+ writeFileSync as writeFileSync2
24308
+ } from "fs";
23784
24309
  import { join as join5 } from "path";
24310
+
24311
+ // ../../packages/persistence/src/vault/vault.ts
24312
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
24313
+
24314
+ // ../../packages/persistence/src/warn-era-cap.ts
24315
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
24316
+ import { join as join6 } from "path";
23785
24317
  var MARKER = "warn-era-capped";
23786
24318
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23787
24319
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23788
- const marker = join5(dataDir2, MARKER);
23789
- if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
24320
+ const marker = join6(dataDir2, MARKER);
24321
+ if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
23790
24322
  const capped = db.policies.capCategoryActions();
23791
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
24323
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23792
24324
  `, { mode: DATA_FILE_MODE });
23793
24325
  return { capped };
23794
24326
  }
23795
24327
 
23796
24328
  // ../../packages/plugin-sdk/src/config.ts
23797
- import { existsSync as existsSync3 } from "fs";
23798
- import { join as join6 } from "path";
24329
+ import { existsSync as existsSync4 } from "fs";
24330
+ import { join as join7 } from "path";
23799
24331
 
23800
24332
  // ../../packages/plugin-sdk/src/provider-env.ts
23801
24333
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -23849,8 +24381,8 @@ function resolveProvider() {
23849
24381
  function loadConfig(base = defaultDataDir()) {
23850
24382
  try {
23851
24383
  ensureLayoutDirSync(base);
23852
- const settingsFile = join6(settingsDir(base), "settings.json");
23853
- if (existsSync3(settingsFile)) tightenFile(settingsFile);
24384
+ const settingsFile = join7(settingsDir(base), "settings.json");
24385
+ if (existsSync4(settingsFile)) tightenFile(settingsFile);
23854
24386
  } catch {
23855
24387
  }
23856
24388
  migrateLegacyLayout(base);
@@ -23873,9 +24405,9 @@ function resolveProviderSafe() {
23873
24405
  }
23874
24406
 
23875
24407
  // ../../packages/plugin-sdk/src/config-inventory.ts
23876
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
24408
+ import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
23877
24409
  import { homedir as homedir2 } from "os";
23878
- import { basename as basename2, join as join8 } from "path";
24410
+ import { basename as basename2, join as join9 } from "path";
23879
24411
 
23880
24412
  // ../../packages/detections/src/egress/registry.ts
23881
24413
  var EXTRACTOR_VERSION = "1";
@@ -26611,18 +27143,18 @@ function bundledDetections() {
26611
27143
  }
26612
27144
 
26613
27145
  // ../../packages/plugin-sdk/src/repo.ts
26614
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
26615
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
27146
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
27147
+ import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
26616
27148
 
26617
27149
  // ../../packages/plugin-sdk/src/events.ts
26618
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
27150
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
26619
27151
 
26620
27152
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
26621
27153
  import { arch, hostname as hostname3, platform, release } from "os";
26622
27154
 
26623
27155
  // ../../packages/plugin-sdk/src/nudge.ts
26624
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
26625
- import { join as join9 } from "path";
27156
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
27157
+ import { join as join10 } from "path";
26626
27158
 
26627
27159
  // ../../packages/plugin-sdk/src/paths.ts
26628
27160
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -26630,21 +27162,21 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26630
27162
 
26631
27163
  // ../../packages/plugin-sdk/src/project-files.ts
26632
27164
  var import_ignore = __toESM(require_ignore(), 1);
26633
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26634
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
27165
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
27166
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
26635
27167
 
26636
27168
  // ../../packages/plugin-sdk/src/runtime.ts
26637
- import { randomUUID as randomUUID10 } from "crypto";
27169
+ import { randomUUID as randomUUID12 } from "crypto";
26638
27170
 
26639
27171
  // ../../packages/plugin-sdk/src/suppressions.ts
26640
27172
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26641
27173
 
26642
27174
  // ../../packages/plugin-sdk/src/throttle.ts
26643
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
26644
- import { join as join11 } from "path";
27175
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
27176
+ import { join as join12 } from "path";
26645
27177
 
26646
27178
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
26647
- import { randomUUID as randomUUID11 } from "crypto";
27179
+ import { randomUUID as randomUUID13 } from "crypto";
26648
27180
 
26649
27181
  // ../../packages/plugin-runtime/src/recorder.ts
26650
27182
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -26806,7 +27338,7 @@ var StandaloneDataGateway = class {
26806
27338
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
26807
27339
  const installed = this.installedScanRules();
26808
27340
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
26809
- id: randomUUID11(),
27341
+ id: randomUUID13(),
26810
27342
  scope: "global",
26811
27343
  target: { ruleId },
26812
27344
  action,
@@ -26959,7 +27491,7 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
26959
27491
  }
26960
27492
 
26961
27493
  // ../../packages/plugin-runtime/src/handle-session-start.ts
26962
- import { randomUUID as randomUUID12 } from "crypto";
27494
+ import { randomUUID as randomUUID14 } from "crypto";
26963
27495
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
26964
27496
 
26965
27497
  // src/present.ts
@@ -27168,8 +27700,8 @@ function renderStatusBar(s, opts = {}) {
27168
27700
  const score = `${dot} health ${paint.bold(String(s.score))}${paint.dim("/100")}`;
27169
27701
  const tally = `${paint.dim("unreviewed")} ${paint.critical(sq)}${String(u.critical)} ${paint.high(sq)}${String(u.high)} ${paint.medium(sq)}${String(u.medium)} ${paint.low(sq)}${String(u.low)}`;
27170
27702
  const flag = s.openFindings > 0 ? paint.critical("\u2691") : paint.dim("\u2691");
27171
- const open = `${flag} ${String(s.openFindings)} open findings`;
27172
- return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open}`;
27703
+ const open2 = `${flag} ${String(s.openFindings)} open findings`;
27704
+ return `${paint.brand("\u25B8\u25B8 AKA")}${sep5}${score}${sep5}${tally}${sep5}${open2}`;
27173
27705
  }
27174
27706
  function findingStatus(summary) {
27175
27707
  return {
@@ -27395,7 +27927,11 @@ function renderExceptions(exceptions, nowMs = Date.now()) {
27395
27927
  }
27396
27928
  const rows = exceptions.map((e) => [
27397
27929
  e.id.slice(0, 8),
27398
- e.maskedValue,
27930
+ // A reveal grant is strictly stronger than a plain suppression: while it is
27931
+ // active the model can receive this value's RAW form at tool boundaries.
27932
+ // Tag the row so it can never be mistaken for a suppress-only grant. The
27933
+ // value itself stays masked — this list shows metadata, never raw values.
27934
+ e.capability === "reveal_to_model" ? `${e.maskedValue} \xB7 REVEALS-TO-MODEL` : e.maskedValue,
27399
27935
  e.ruleId,
27400
27936
  e.scope,
27401
27937
  relativeExpiry(e.expiresAt, nowMs),