@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.
@@ -495,11 +495,11 @@ var require_ignore = __commonJS({
495
495
  import { fileURLToPath } from "url";
496
496
 
497
497
  // ../../packages/plugin-sdk/src/config.ts
498
- import { existsSync as existsSync3 } from "fs";
499
- import { join as join6 } from "path";
498
+ import { existsSync as existsSync4 } from "fs";
499
+ import { join as join7 } from "path";
500
500
 
501
501
  // ../../packages/persistence/src/database.ts
502
- import { randomUUID as randomUUID8 } from "crypto";
502
+ import { randomUUID as randomUUID9 } from "crypto";
503
503
  import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
504
504
  import { join, sep } from "path";
505
505
  import { DatabaseSync } from "node:sqlite";
@@ -565,6 +565,22 @@ var SQLITE_MIGRATIONS = [
565
565
  {
566
566
  tag: "0014_drop_legacy_events_findings",
567
567
  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"
568
+ },
569
+ {
570
+ tag: "0015_busy_vengeance",
571
+ 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`);"
572
+ },
573
+ {
574
+ tag: "0016_breezy_zodiak",
575
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
576
+ },
577
+ {
578
+ tag: "0017_rainy_kat_farrell",
579
+ 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`);"
580
+ },
581
+ {
582
+ tag: "0018_serious_tana_nile",
583
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
568
584
  }
569
585
  ];
570
586
 
@@ -16224,6 +16240,7 @@ var ExceptionConditions = external_exports.object({
16224
16240
  sourceTool: external_exports.string().optional(),
16225
16241
  provider: external_exports.string().optional()
16226
16242
  }).strict();
16243
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16227
16244
  var DetectionException = external_exports.object({
16228
16245
  id: external_exports.guid(),
16229
16246
  ruleId: external_exports.string(),
@@ -16240,6 +16257,7 @@ var DetectionException = external_exports.object({
16240
16257
  keyVersion: external_exports.number().int().positive(),
16241
16258
  // maskMatch() preview of the approved value — never the raw value.
16242
16259
  maskedValue: external_exports.string(),
16260
+ capability: ExceptionCapability.default("suppress"),
16243
16261
  scope: ExceptionScope,
16244
16262
  expiresAt: external_exports.iso.datetime().nullable(),
16245
16263
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16263,6 +16281,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16263
16281
  ruleId: true,
16264
16282
  valueFingerprint: true,
16265
16283
  keyVersion: true,
16284
+ capability: true,
16266
16285
  expiresAt: true,
16267
16286
  maxUses: true,
16268
16287
  useCount: true,
@@ -17367,8 +17386,128 @@ var PatchInstalledPackRequest = external_exports.object({
17367
17386
  message: "At least one field must be provided"
17368
17387
  }).meta({ id: "PatchInstalledPackRequest" });
17369
17388
 
17389
+ // ../../packages/schema/src/zod/vault.ts
17390
+ var POINTER_FORMAT_VERSION = 2;
17391
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17392
+ var POINTER_TOKEN_PATTERN = new RegExp(
17393
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17394
+ );
17395
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17396
+ function pointerTokenScanner() {
17397
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
17398
+ }
17399
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17400
+ var ParsedPointer = external_exports.object({
17401
+ category: DetectionCategory,
17402
+ keyVersion: external_exports.number().int().positive(),
17403
+ pointerId: external_exports.string(),
17404
+ tag: external_exports.string()
17405
+ });
17406
+ var VaultEntry = external_exports.object({
17407
+ pointerId: external_exports.string(),
17408
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17409
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17410
+ // independently of the vault encryption key below.
17411
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17412
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17413
+ // The vault-key epoch this row's ciphertext was sealed under.
17414
+ keyVersion: external_exports.number().int().positive(),
17415
+ // Fixed at first mint and never updated: the same value detected later under a
17416
+ // different rule's category keeps the category it was minted with, so one
17417
+ // value always produces exactly one wire token.
17418
+ category: DetectionCategory,
17419
+ ruleId: external_exports.string(),
17420
+ // Partial-reveal preview for badges and listings. Never the raw value.
17421
+ maskedMatch: external_exports.string(),
17422
+ provider: external_exports.string().optional(),
17423
+ ciphertext: external_exports.string(),
17424
+ nonce: external_exports.string(),
17425
+ authTag: external_exports.string(),
17426
+ // How many times this value has been detected on this machine — the reuse
17427
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17428
+ occurrenceCount: external_exports.number().int().nonnegative(),
17429
+ firstSeen: external_exports.string(),
17430
+ lastSeen: external_exports.string()
17431
+ });
17432
+ var PointerDescriptor = external_exports.object({
17433
+ category: DetectionCategory,
17434
+ provider: external_exports.string().optional(),
17435
+ maskedMatch: external_exports.string(),
17436
+ occurrences: external_exports.number().int().nonnegative(),
17437
+ firstSeen: external_exports.string(),
17438
+ lastSeen: external_exports.string()
17439
+ });
17440
+ var PointerIdentity = external_exports.object({
17441
+ ruleId: external_exports.string(),
17442
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17443
+ fingerprintKeyVersion: external_exports.number().int().positive()
17444
+ });
17445
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17446
+ var VaultDerefReason = external_exports.enum([
17447
+ "display",
17448
+ "explicit-reveal",
17449
+ "view-render",
17450
+ "model-input",
17451
+ "remediation",
17452
+ "purge"
17453
+ ]);
17454
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17455
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
17456
+ function isBatchedDerefReason(reason) {
17457
+ return BATCHED_DEREF_REASONS.includes(reason);
17458
+ }
17459
+ var VaultDeref = external_exports.object({
17460
+ id: external_exports.guid(),
17461
+ pointerId: external_exports.string(),
17462
+ at: external_exports.string(),
17463
+ target: DetokenizeTarget,
17464
+ reason: VaultDerefReason,
17465
+ outcome: VaultDerefOutcome,
17466
+ // Present only on a model-target crossing that a reveal grant authorized.
17467
+ grantId: external_exports.string().optional(),
17468
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17469
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17470
+ pointerCount: external_exports.number().int().positive().default(1)
17471
+ });
17472
+ var VaultSightingKind = external_exports.enum([
17473
+ "prompt",
17474
+ "tool-input",
17475
+ "tool-output",
17476
+ "file",
17477
+ "transcript"
17478
+ ]);
17479
+ var VaultSighting = external_exports.object({
17480
+ location: external_exports.string(),
17481
+ kind: VaultSightingKind,
17482
+ firstSeen: external_exports.string(),
17483
+ lastSeen: external_exports.string()
17484
+ });
17485
+ var VaultInventoryEntry = external_exports.object({
17486
+ pointerId: external_exports.string(),
17487
+ category: DetectionCategory,
17488
+ provider: external_exports.string().optional(),
17489
+ maskedMatch: external_exports.string(),
17490
+ occurrences: external_exports.number().int().nonnegative(),
17491
+ firstSeen: external_exports.string(),
17492
+ lastSeen: external_exports.string(),
17493
+ // The active reveal-to-model grant covering this value, when one exists —
17494
+ // the inventory badges it, the row links to revocation.
17495
+ revealGrantId: external_exports.string().nullable(),
17496
+ sightings: external_exports.array(VaultSighting)
17497
+ });
17498
+ var VaultKeyCustody = external_exports.string();
17499
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17500
+ var VAULT_CONSENT_VERSION = 1;
17501
+ var VaultConsent = external_exports.object({
17502
+ acknowledgedAt: external_exports.iso.datetime(),
17503
+ version: external_exports.number().int().positive()
17504
+ });
17505
+ function isVaultConsentValid(consent) {
17506
+ return consent?.version === VAULT_CONSENT_VERSION;
17507
+ }
17508
+
17370
17509
  // ../../packages/schema/src/zod/local.ts
17371
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17510
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17372
17511
  var RunMode = external_exports.enum(["standalone"]);
17373
17512
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17374
17513
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17390,6 +17529,16 @@ var WorkspaceSettings = external_exports.object({
17390
17529
  // In-place egress extraction on the scan paths; disable to stop all Data
17391
17530
  // Shares writes.
17392
17531
  dataSharesInPlace: external_exports.boolean().default(true),
17532
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17533
+ // vault, instead of destroying them. Absent by default: this is a custody
17534
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17535
+ // Revoking stops future vaulting; it does not erase what is already stored —
17536
+ // purging the vault is the eraser.
17537
+ vaultConsent: VaultConsent.optional(),
17538
+ // Where the vault master key lives.
17539
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17540
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17541
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17393
17542
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17394
17543
  onboardedAt: external_exports.iso.datetime().optional(),
17395
17544
  // Records that the user consented to sending findings to the model API for
@@ -19804,6 +19953,9 @@ var AmbiguousExceptionIdError = class extends Error {
19804
19953
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19805
19954
  AND (expires_at IS NULL OR expires_at > :now)
19806
19955
  AND (max_uses IS NULL OR use_count < max_uses)`;
19956
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19957
+ AND conditions IS NULL
19958
+ AND ${ACTIVE_PREDICATE}`;
19807
19959
  var SqliteExceptionsRepository = class {
19808
19960
  constructor(db) {
19809
19961
  this.db = db;
@@ -19895,11 +20047,11 @@ var SqliteExceptionsRepository = class {
19895
20047
  this.db.prepare(
19896
20048
  `INSERT INTO exceptions (
19897
20049
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19898
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19899
- conditions, created_by, created_via, created_at, updated_at
20050
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20051
+ justification, conditions, created_by, created_via, created_at, updated_at
19900
20052
  ) VALUES (
19901
20053
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19902
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20054
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19903
20055
  :conditions, :createdBy, :createdVia, :now, :now
19904
20056
  )`
19905
20057
  ).run({
@@ -19909,6 +20061,7 @@ var SqliteExceptionsRepository = class {
19909
20061
  valueFingerprint: input.valueFingerprint,
19910
20062
  keyVersion: input.keyVersion,
19911
20063
  maskedValue: input.maskedValue,
20064
+ capability: input.capability ?? "suppress",
19912
20065
  scope: input.scope,
19913
20066
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19914
20067
  maxUses: input.maxUses,
@@ -20002,6 +20155,7 @@ var SqliteExceptionsRepository = class {
20002
20155
  ruleId: row.rule_id,
20003
20156
  valueFingerprint: row.value_fingerprint,
20004
20157
  keyVersion: row.key_version,
20158
+ capability: row.capability,
20005
20159
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20006
20160
  maxUses: row.max_uses,
20007
20161
  useCount: row.use_count,
@@ -20056,6 +20210,35 @@ var SqliteExceptionsRepository = class {
20056
20210
  }))
20057
20211
  );
20058
20212
  }
20213
+ /**
20214
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20215
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20216
+ * suppression uses — plus the capability: a suppression grant must never
20217
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20218
+ * revealed value re-enters the detection scan immediately afterward and the
20219
+ * suppression match there claims the use — one crossing, one use.
20220
+ *
20221
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20222
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20223
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20224
+ */
20225
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20226
+ try {
20227
+ const row = getRow(
20228
+ this.db.prepare(
20229
+ `SELECT id FROM exceptions
20230
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20231
+ AND key_version = :keyVersion
20232
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20233
+ LIMIT 1`
20234
+ ),
20235
+ { ruleId, valueFingerprint, keyVersion, now }
20236
+ );
20237
+ return Promise.resolve(row ?? null);
20238
+ } catch (err) {
20239
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20240
+ }
20241
+ }
20059
20242
  /**
20060
20243
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20061
20244
  * exhausted) whose last transition is older than the retention window.
@@ -20083,6 +20266,7 @@ function parseExceptionRow(row) {
20083
20266
  valueFingerprint: row.value_fingerprint,
20084
20267
  keyVersion: row.key_version,
20085
20268
  maskedValue: row.masked_value,
20269
+ capability: row.capability,
20086
20270
  scope: row.scope,
20087
20271
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20088
20272
  maxUses: row.max_uses,
@@ -22248,6 +22432,287 @@ var SqliteScanLedgerRepository = class {
22248
22432
  }
22249
22433
  };
22250
22434
 
22435
+ // ../../packages/persistence/src/repositories/secret-vault.ts
22436
+ import { randomUUID as randomUUID7 } from "crypto";
22437
+ var SELECT_COLUMNS = `
22438
+ pointer_id AS pointerId,
22439
+ value_fingerprint AS valueFingerprint,
22440
+ fingerprint_key_version AS fingerprintKeyVersion,
22441
+ key_version AS keyVersion,
22442
+ format_version AS formatVersion,
22443
+ category,
22444
+ rule_id AS ruleId,
22445
+ masked_match AS maskedMatch,
22446
+ provider,
22447
+ ciphertext,
22448
+ nonce,
22449
+ auth_tag AS authTag,
22450
+ occurrence_count AS occurrenceCount,
22451
+ first_seen AS firstSeen,
22452
+ last_seen AS lastSeen`;
22453
+ function toRow(raw) {
22454
+ const { provider, ...rest } = raw;
22455
+ return provider === null ? rest : { ...rest, provider };
22456
+ }
22457
+ var SqliteSecretVaultRepository = class {
22458
+ constructor(db) {
22459
+ this.db = db;
22460
+ this.insertStmt = db.prepare(
22461
+ `INSERT INTO secret_vault (
22462
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
22463
+ format_version, category, rule_id, masked_match, provider,
22464
+ ciphertext, nonce, auth_tag,
22465
+ occurrence_count, first_seen, last_seen
22466
+ ) VALUES (
22467
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
22468
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
22469
+ :ciphertext, :nonce, :authTag,
22470
+ 1, :now, :now
22471
+ )`
22472
+ );
22473
+ this.bumpStmt = db.prepare(
22474
+ `UPDATE secret_vault
22475
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
22476
+ WHERE value_fingerprint = :valueFingerprint`
22477
+ );
22478
+ this.byPointerStmt = db.prepare(
22479
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
22480
+ );
22481
+ this.byFingerprintStmt = db.prepare(
22482
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
22483
+ );
22484
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
22485
+ this.replaceCiphertextStmt = db.prepare(
22486
+ `UPDATE secret_vault
22487
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
22488
+ WHERE pointer_id = :pointerId`
22489
+ );
22490
+ this.refreshFingerprintStmt = db.prepare(
22491
+ `UPDATE secret_vault
22492
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
22493
+ WHERE pointer_id = :pointerId`
22494
+ );
22495
+ this.derefStmt = db.prepare(
22496
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
22497
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
22498
+ );
22499
+ }
22500
+ db;
22501
+ insertStmt;
22502
+ bumpStmt;
22503
+ byPointerStmt;
22504
+ byFingerprintStmt;
22505
+ listStmt;
22506
+ replaceCiphertextStmt;
22507
+ refreshFingerprintStmt;
22508
+ derefStmt;
22509
+ /**
22510
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
22511
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
22512
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
22513
+ * pointer, category and ciphertext, so the same secret always resolves to one
22514
+ * wire token. `minted` is true only when this call created the row.
22515
+ *
22516
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
22517
+ * writers cannot both decide they are minting.
22518
+ */
22519
+ upsert(input, now) {
22520
+ let minted = false;
22521
+ withTransaction(
22522
+ this.db,
22523
+ () => {
22524
+ const existing = getRow(this.byFingerprintStmt, {
22525
+ valueFingerprint: input.valueFingerprint
22526
+ });
22527
+ if (existing === void 0) {
22528
+ this.insertStmt.run(
22529
+ bindParams({
22530
+ pointerId: input.pointerId,
22531
+ valueFingerprint: input.valueFingerprint,
22532
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
22533
+ keyVersion: input.keyVersion,
22534
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
22535
+ category: input.category,
22536
+ ruleId: input.ruleId,
22537
+ maskedMatch: input.maskedMatch,
22538
+ provider: input.provider,
22539
+ ciphertext: input.ciphertext,
22540
+ nonce: input.nonce,
22541
+ authTag: input.authTag,
22542
+ now
22543
+ })
22544
+ );
22545
+ minted = true;
22546
+ return;
22547
+ }
22548
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
22549
+ },
22550
+ "IMMEDIATE"
22551
+ );
22552
+ const row = getRow(this.byFingerprintStmt, {
22553
+ valueFingerprint: input.valueFingerprint
22554
+ });
22555
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
22556
+ return { row: toRow(row), minted };
22557
+ }
22558
+ byPointerId(pointerId) {
22559
+ const raw = getRow(this.byPointerStmt, { pointerId });
22560
+ return raw === void 0 ? null : toRow(raw);
22561
+ }
22562
+ byValueFingerprint(fingerprint) {
22563
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
22564
+ return raw === void 0 ? null : toRow(raw);
22565
+ }
22566
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
22567
+ recordDeref(entry) {
22568
+ this.derefStmt.run(
22569
+ bindParams({
22570
+ id: entry.id,
22571
+ pointerId: entry.pointerId,
22572
+ at: entry.at,
22573
+ target: entry.target,
22574
+ reason: entry.reason,
22575
+ outcome: entry.outcome,
22576
+ grantId: entry.grantId,
22577
+ pointerCount: entry.pointerCount ?? 1
22578
+ })
22579
+ );
22580
+ }
22581
+ listAll() {
22582
+ return allRows(this.listStmt).map(toRow);
22583
+ }
22584
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
22585
+ replaceCiphertext(pointerId, next) {
22586
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
22587
+ }
22588
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
22589
+ refreshFingerprint(pointerId, next) {
22590
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
22591
+ }
22592
+ /**
22593
+ * Destroy every vaulted value and report how many were destroyed. The deref
22594
+ * audit is left alone on purpose — see the table note above.
22595
+ */
22596
+ purgeAll() {
22597
+ let destroyed = 0;
22598
+ withTransaction(
22599
+ this.db,
22600
+ () => {
22601
+ destroyed = this.countEntries();
22602
+ this.db.exec("DELETE FROM secret_vault");
22603
+ },
22604
+ "IMMEDIATE"
22605
+ );
22606
+ return destroyed;
22607
+ }
22608
+ /**
22609
+ * Record (or re-stamp) one place a pointer has been written. One row per
22610
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
22611
+ * on hook paths — a failure must never affect the rewrite that triggered it,
22612
+ * so callers wrap this, not the other way around.
22613
+ */
22614
+ recordSighting(entry, now) {
22615
+ this.db.prepare(
22616
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
22617
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
22618
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22619
+ ).run({
22620
+ id: randomUUID7(),
22621
+ pointerId: entry.pointerId,
22622
+ location: entry.location,
22623
+ kind: entry.kind,
22624
+ now
22625
+ });
22626
+ }
22627
+ listSightings(pointerId) {
22628
+ const rows = allRows(
22629
+ this.db.prepare(
22630
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22631
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
22632
+ ),
22633
+ { pointerId }
22634
+ );
22635
+ return rows.map((r) => ({
22636
+ location: r.location,
22637
+ kind: r.kind,
22638
+ firstSeen: new Date(r.first_seen).toISOString(),
22639
+ lastSeen: new Date(r.last_seen).toISOString()
22640
+ }));
22641
+ }
22642
+ /**
22643
+ * The dashboard inventory: every vaulted value's descriptor data joined with
22644
+ * its sightings and the active reveal-to-model grant when one exists.
22645
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
22646
+ * columns are selected.
22647
+ */
22648
+ listInventory(now = Date.now()) {
22649
+ const rows = allRows(
22650
+ this.db.prepare(
22651
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22652
+ v.occurrence_count, v.first_seen, v.last_seen,
22653
+ (SELECT e.id FROM exceptions e
22654
+ WHERE e.rule_id = v.rule_id
22655
+ AND e.value_fingerprint = v.value_fingerprint
22656
+ AND e.key_version = v.fingerprint_key_version
22657
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22658
+ LIMIT 1) AS grant_id
22659
+ FROM secret_vault v
22660
+ ORDER BY v.last_seen DESC`
22661
+ ),
22662
+ { now }
22663
+ );
22664
+ return rows.map((r) => ({
22665
+ pointerId: r.pointer_id,
22666
+ category: r.category,
22667
+ ...r.provider === null ? {} : { provider: r.provider },
22668
+ maskedMatch: r.masked_match,
22669
+ occurrences: r.occurrence_count,
22670
+ firstSeen: new Date(r.first_seen).toISOString(),
22671
+ lastSeen: new Date(r.last_seen).toISOString(),
22672
+ revealGrantId: r.grant_id,
22673
+ sightings: this.listSightings(r.pointer_id)
22674
+ }));
22675
+ }
22676
+ /**
22677
+ * The de-reference trail, newest first. By default the batched, high-volume
22678
+ * reasons (display, view-render) are hidden and counted instead — the rows
22679
+ * that matter as a signal are the model crossings, and burying them under
22680
+ * render noise would defeat the audit's purpose.
22681
+ */
22682
+ listDerefs(opts) {
22683
+ const limit = opts?.limit ?? 200;
22684
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
22685
+ const rows = allRows(
22686
+ this.db.prepare(
22687
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22688
+ FROM secret_vault_deref ${where}
22689
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
22690
+ ),
22691
+ { limit }
22692
+ );
22693
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
22694
+ this.db,
22695
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22696
+ );
22697
+ return {
22698
+ rows: rows.map((r) => ({
22699
+ id: r.id,
22700
+ pointerId: r.pointer_id,
22701
+ at: new Date(r.at).toISOString(),
22702
+ target: r.target,
22703
+ reason: r.reason,
22704
+ outcome: r.outcome,
22705
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
22706
+ pointerCount: r.pointer_count
22707
+ })),
22708
+ hiddenBatched
22709
+ };
22710
+ }
22711
+ countEntries() {
22712
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22713
+ }
22714
+ };
22715
+
22251
22716
  // ../../packages/persistence/src/repositories/security.ts
22252
22717
  var DAY_MS4 = 864e5;
22253
22718
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22593,7 +23058,7 @@ var SqliteSecurityRepository = class {
22593
23058
  };
22594
23059
 
22595
23060
  // ../../packages/persistence/src/repositories/shares.ts
22596
- import { randomUUID as randomUUID7 } from "crypto";
23061
+ import { randomUUID as randomUUID8 } from "crypto";
22597
23062
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22598
23063
  var IN_CHUNK = 500;
22599
23064
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22849,7 +23314,7 @@ var SqliteSharesRepository = class {
22849
23314
  (id, destination_id, host, decision, created_at, updated_at)
22850
23315
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22851
23316
  ).run({
22852
- id: randomUUID7(),
23317
+ id: randomUUID8(),
22853
23318
  destinationId,
22854
23319
  host: dest.host,
22855
23320
  decision,
@@ -22998,7 +23463,7 @@ var SqliteSharesRepository = class {
22998
23463
  let destinationId = destIds.get(hit.host);
22999
23464
  if (destinationId === void 0) {
23000
23465
  destStmt.run({
23001
- id: randomUUID7(),
23466
+ id: randomUUID8(),
23002
23467
  kind: hit.kind,
23003
23468
  name: hit.name,
23004
23469
  host: hit.host,
@@ -23014,7 +23479,7 @@ var SqliteSharesRepository = class {
23014
23479
  let endpointId = endpointIds.get(endpointKey);
23015
23480
  if (endpointId === void 0) {
23016
23481
  endpointStmt.run({
23017
- id: randomUUID7(),
23482
+ id: randomUUID8(),
23018
23483
  destinationId,
23019
23484
  method: hit.method,
23020
23485
  transport: hit.transport,
@@ -23027,7 +23492,7 @@ var SqliteSharesRepository = class {
23027
23492
  endpointIds.set(endpointKey, endpointId);
23028
23493
  }
23029
23494
  siteStmt.run({
23030
- id: randomUUID7(),
23495
+ id: randomUUID8(),
23031
23496
  endpointId,
23032
23497
  project: input.project,
23033
23498
  projectKey: input.projectKey,
@@ -23395,11 +23860,22 @@ function purgeSampleData(db) {
23395
23860
  function linkHost(input, hostId) {
23396
23861
  return hostId ? { ...input, hostId } : input;
23397
23862
  }
23863
+ function closeQuietly(db) {
23864
+ try {
23865
+ db.close();
23866
+ } catch {
23867
+ }
23868
+ }
23398
23869
  function openWithPragmas(file2) {
23399
23870
  const db = new DatabaseSync(file2);
23400
- db.exec("PRAGMA journal_mode = WAL");
23401
- db.exec("PRAGMA busy_timeout = 2000");
23402
- db.exec("PRAGMA foreign_keys = ON");
23871
+ try {
23872
+ db.exec("PRAGMA journal_mode = WAL");
23873
+ db.exec("PRAGMA busy_timeout = 2000");
23874
+ db.exec("PRAGMA foreign_keys = ON");
23875
+ } catch (err) {
23876
+ closeQuietly(db);
23877
+ throw err;
23878
+ }
23403
23879
  return db;
23404
23880
  }
23405
23881
  function backupLegacyStore(file2) {
@@ -23411,43 +23887,82 @@ function backupLegacyStore(file2) {
23411
23887
  }
23412
23888
  return backup;
23413
23889
  }
23890
+ function openAndInitialize(file2) {
23891
+ let db = openWithPragmas(file2);
23892
+ try {
23893
+ if (isForeignSqliteLineage(db)) {
23894
+ db.close();
23895
+ const backup = backupLegacyStore(file2);
23896
+ db = openWithPragmas(file2);
23897
+ akaWarn(
23898
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23899
+ );
23900
+ }
23901
+ applyMigrations(db, file2);
23902
+ tightenPerms(file2);
23903
+ const policies = new SqlitePoliciesRepository(db);
23904
+ const installedPacks = new SqliteInstalledPacksRepository(db);
23905
+ const repositories = {
23906
+ events: new SqliteEventsRepository(db),
23907
+ findings: new SqliteFindingsRepository(db),
23908
+ policies,
23909
+ installedPacks,
23910
+ scanLedger: new SqliteScanLedgerRepository(db),
23911
+ secretVault: new SqliteSecretVaultRepository(db),
23912
+ exceptions: new SqliteExceptionsRepository(db),
23913
+ resolutions: new SqliteResolutionsRepository(db),
23914
+ ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
23915
+ security: new SqliteSecurityRepository(db),
23916
+ detections: new SqliteDetectionsRepository(db),
23917
+ shares: new SqliteSharesRepository(db),
23918
+ policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
23919
+ inventory: new SqliteInventoryRepository(db),
23920
+ inventoryAssets: new SqliteInventoryAssetsRepository(db),
23921
+ projectFiles: new SqliteProjectFilesRepository(db),
23922
+ activity: new SqliteActivityRepository(db),
23923
+ sourceProject: new SqliteSourceProjectRepository(db),
23924
+ auditEvents: new SqliteAuditEventsRepository(db),
23925
+ classifiedData: new SqliteClassifiedDataRepository(db),
23926
+ inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
23927
+ inspectionFindings: new SqliteInspectionFindingsRepository(db),
23928
+ configInventory: new SqliteConfigInventoryRepository(db)
23929
+ };
23930
+ policies.seedDefaults();
23931
+ return { db, ...repositories };
23932
+ } catch (err) {
23933
+ closeQuietly(db);
23934
+ throw err;
23935
+ }
23936
+ }
23414
23937
  function openLocalDatabase(dir) {
23415
23938
  ensureDataDirSync(dir);
23416
23939
  const file2 = join(dir, DB_FILENAME);
23417
- let db = openWithPragmas(file2);
23418
- if (isForeignSqliteLineage(db)) {
23419
- db.close();
23420
- const backup = backupLegacyStore(file2);
23421
- db = openWithPragmas(file2);
23422
- akaWarn(
23423
- `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23424
- );
23425
- }
23426
- applyMigrations(db, file2);
23427
- tightenPerms(file2);
23428
- const events = new SqliteEventsRepository(db);
23429
- const findings = new SqliteFindingsRepository(db);
23430
- const policies = new SqlitePoliciesRepository(db);
23431
- const installedPacks = new SqliteInstalledPacksRepository(db);
23432
- const scanLedger = new SqliteScanLedgerRepository(db);
23433
- const exceptions = new SqliteExceptionsRepository(db);
23434
- const resolutions = new SqliteResolutionsRepository(db);
23435
- const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
23436
- const security = new SqliteSecurityRepository(db);
23437
- const detections = new SqliteDetectionsRepository(db);
23438
- const shares = new SqliteSharesRepository(db);
23439
- const policyCatalog = new SqlitePolicyCatalogRepository(installedPacks);
23440
- const inventory = new SqliteInventoryRepository(db);
23441
- const inventoryAssets = new SqliteInventoryAssetsRepository(db);
23442
- const projectFiles = new SqliteProjectFilesRepository(db);
23443
- const activity = new SqliteActivityRepository(db);
23444
- const sourceProject = new SqliteSourceProjectRepository(db);
23445
- const auditEvents = new SqliteAuditEventsRepository(db);
23446
- const classifiedData = new SqliteClassifiedDataRepository(db);
23447
- const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
23448
- const inspectionFindings = new SqliteInspectionFindingsRepository(db);
23449
- const configInventory = new SqliteConfigInventoryRepository(db);
23450
- policies.seedDefaults();
23940
+ const {
23941
+ db,
23942
+ events,
23943
+ findings,
23944
+ policies,
23945
+ installedPacks,
23946
+ scanLedger,
23947
+ secretVault,
23948
+ exceptions,
23949
+ resolutions,
23950
+ ruleProbeCache,
23951
+ security,
23952
+ detections,
23953
+ shares,
23954
+ policyCatalog,
23955
+ inventory,
23956
+ inventoryAssets,
23957
+ projectFiles,
23958
+ activity,
23959
+ sourceProject,
23960
+ auditEvents,
23961
+ classifiedData,
23962
+ inspectionDefinitions,
23963
+ inspectionFindings,
23964
+ configInventory
23965
+ } = openAndInitialize(file2);
23451
23966
  function recordCapture(event, detected) {
23452
23967
  failOpenTransaction(db, () => {
23453
23968
  const sessionId = event.metadata?.sessionId;
@@ -23538,7 +24053,7 @@ function openLocalDatabase(dir) {
23538
24053
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23539
24054
  if (!definitionId) continue;
23540
24055
  inspectionFindings.insertFinding({
23541
- id: randomUUID8(),
24056
+ id: randomUUID9(),
23542
24057
  auditEventId: record2.scanEvent.id,
23543
24058
  inspectionDefinitionId: definitionId,
23544
24059
  span: finding.span,
@@ -23615,6 +24130,7 @@ function openLocalDatabase(dir) {
23615
24130
  policies,
23616
24131
  installedPacks,
23617
24132
  scanLedger,
24133
+ secretVault,
23618
24134
  exceptions,
23619
24135
  resolutions,
23620
24136
  ruleProbeCache,
@@ -23647,6 +24163,26 @@ function openLocalDatabase(dir) {
23647
24163
  };
23648
24164
  }
23649
24165
 
24166
+ // ../../packages/persistence/src/exception-policy.ts
24167
+ var UserGrantPolicyProvider = class {
24168
+ #exceptions;
24169
+ constructor(exceptions) {
24170
+ this.#exceptions = exceptions;
24171
+ }
24172
+ async decideReveal(identity) {
24173
+ try {
24174
+ const grant = await this.#exceptions.activeRevealGrant(
24175
+ identity.ruleId,
24176
+ identity.valueFingerprint,
24177
+ identity.fingerprintKeyVersion
24178
+ );
24179
+ return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
24180
+ } catch {
24181
+ return { allow: false };
24182
+ }
24183
+ }
24184
+ };
24185
+
23650
24186
  // ../../packages/persistence/src/finding-key.ts
23651
24187
  import { createHash as createHash3 } from "crypto";
23652
24188
  function normalizeFilePath(filePath) {
@@ -23659,8 +24195,9 @@ function computeFindingKey(input) {
23659
24195
 
23660
24196
  // ../../packages/persistence/src/fingerprint.ts
23661
24197
  import { createHmac, randomBytes } from "crypto";
23662
- import { readFileSync } from "fs";
24198
+ import { existsSync as existsSync2, readFileSync } from "fs";
23663
24199
  import { join as join2 } from "path";
24200
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23664
24201
  var KEY_FILENAME = "exception.key";
23665
24202
  var KEY_MATERIAL_BYTES = 32;
23666
24203
  function keyFilePath(dataDir2) {
@@ -23684,6 +24221,50 @@ function parseKeyFile(raw) {
23684
24221
  }
23685
24222
  return { version: version2, material: bytes };
23686
24223
  }
24224
+ var KEY_VERSION_COLUMNS = {
24225
+ exceptions: "key_version",
24226
+ blocked_detections: "key_version",
24227
+ secret_vault: "fingerprint_key_version"
24228
+ };
24229
+ var SQLITE_ERROR = 1;
24230
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
24231
+ var FloorUnreadableError = class extends Error {
24232
+ code = "floor-unreadable";
24233
+ constructor(cause) {
24234
+ super(
24235
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
24236
+ { cause }
24237
+ );
24238
+ this.name = "FloorUnreadableError";
24239
+ }
24240
+ };
24241
+ function storedKeyVersionFloor(dataDir2) {
24242
+ const file2 = join2(dataDir2, DB_FILENAME);
24243
+ if (!existsSync2(file2)) return 0;
24244
+ let db;
24245
+ try {
24246
+ db = new DatabaseSync2(file2, { readOnly: true });
24247
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
24248
+ let floor = 0;
24249
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
24250
+ try {
24251
+ const row = getRow(
24252
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
24253
+ );
24254
+ floor = Math.max(floor, row?.v ?? 0);
24255
+ } catch (err) {
24256
+ if (err.errcode !== SQLITE_ERROR) {
24257
+ throw new FloorUnreadableError(err);
24258
+ }
24259
+ }
24260
+ }
24261
+ return floor;
24262
+ } catch (err) {
24263
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
24264
+ } finally {
24265
+ db?.close();
24266
+ }
24267
+ }
23687
24268
  function writeKeyFile(dataDir2, key) {
23688
24269
  ensureDataDirSync(dataDir2);
23689
24270
  const file2 = keyFilePath(dataDir2);
@@ -23708,7 +24289,10 @@ function loadOrCreateFingerprintKey(dataDir2) {
23708
24289
  tightenFile(keyFilePath(dataDir2));
23709
24290
  return existing;
23710
24291
  }
23711
- return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
24292
+ return writeKeyFile(dataDir2, {
24293
+ version: storedKeyVersionFloor(dataDir2) + 1,
24294
+ material: randomBytes(KEY_MATERIAL_BYTES)
24295
+ });
23712
24296
  }
23713
24297
  function fingerprintValue(key, raw) {
23714
24298
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
@@ -23731,6 +24315,9 @@ function dataDir(base = defaultDataDir()) {
23731
24315
  function dbPath(base = defaultDataDir()) {
23732
24316
  return join3(dataDir(base), "aka.db");
23733
24317
  }
24318
+ function keysDir(base = defaultDataDir()) {
24319
+ return join3(base, "keys");
24320
+ }
23734
24321
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23735
24322
  ensureDataDirSync(dir);
23736
24323
  }
@@ -23772,44 +24359,905 @@ function readJson(file2) {
23772
24359
  return parseJsonObject(text) ?? null;
23773
24360
  }
23774
24361
 
23775
- // ../../packages/persistence/src/warn-era-cap.ts
23776
- import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
23777
- import { join as join5 } from "path";
23778
- var MARKER = "warn-era-capped";
23779
- function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23780
- if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23781
- const marker = join5(dataDir2, MARKER);
23782
- if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
23783
- const capped = db.policies.capCategoryActions();
23784
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
23785
- `, { mode: DATA_FILE_MODE });
23786
- return { capped };
24362
+ // ../../packages/persistence/src/vault/crypto.ts
24363
+ import {
24364
+ createCipheriv,
24365
+ createDecipheriv,
24366
+ createHmac as createHmac2,
24367
+ hkdfSync,
24368
+ timingSafeEqual
24369
+ } from "crypto";
24370
+ var POINTER_ID_BYTES = 16;
24371
+ var NONCE_BYTES = 12;
24372
+ var TAG_BYTES = 10;
24373
+ var SUBKEY_BYTES = 32;
24374
+ var HKDF_INFO_ENC = "aka:vault:enc:v1";
24375
+ var HKDF_INFO_SIGN = "aka:vault:sign:v1";
24376
+ var HKDF_SALT = "aka:vault:v1";
24377
+ var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
24378
+ function base32Encode(bytes) {
24379
+ let out = "";
24380
+ let buffer = 0;
24381
+ let bits = 0;
24382
+ for (const byte of bytes) {
24383
+ buffer = buffer << 8 | byte;
24384
+ bits += 8;
24385
+ while (bits >= 5) {
24386
+ out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
24387
+ bits -= 5;
24388
+ }
24389
+ }
24390
+ if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
24391
+ return out;
23787
24392
  }
23788
-
23789
- // ../../packages/plugin-sdk/src/provider-env.ts
23790
- var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
23791
- var booleanish = external_exports.string().optional().transform((v) => {
23792
- if (v === void 0) return void 0;
23793
- const t = v.trim().toLowerCase();
23794
- if (t === "" || t === "false" || t === "0") return false;
23795
- return true;
23796
- }).catch(void 0);
23797
- var optionalBaseUrl = external_exports.preprocess((v) => {
23798
- if (typeof v === "string" && v.trim() === "") return void 0;
23799
- return v;
23800
- }, external_exports.string().optional()).catch(void 0);
23801
- var providerEnvShape = {
23802
- CLAUDE_CODE_USE_BEDROCK: booleanish,
23803
- CLAUDE_CODE_USE_VERTEX: booleanish,
23804
- ANTHROPIC_BASE_URL: optionalBaseUrl
23805
- };
23806
- var ProviderEnvSchema = external_exports.object(providerEnvShape);
23807
-
23808
- // ../../packages/plugin-sdk/src/provider.ts
23809
- function hostOf(url2) {
24393
+ function base32Decode(text) {
24394
+ const out = [];
24395
+ let buffer = 0;
24396
+ let bits = 0;
24397
+ for (const char of text) {
24398
+ const value = B32_ALPHABET.indexOf(char);
24399
+ if (value < 0) throw new Error("base32: character outside the alphabet");
24400
+ buffer = buffer << 5 | value;
24401
+ bits += 5;
24402
+ if (bits >= 8) {
24403
+ out.push(buffer >>> bits - 8 & 255);
24404
+ bits -= 8;
24405
+ }
24406
+ }
24407
+ return Buffer.from(out);
24408
+ }
24409
+ function encodeKeyVersion(version2) {
24410
+ if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
24411
+ throw new Error("vault: key version out of range");
24412
+ }
24413
+ const bytes = [];
24414
+ let remaining = version2;
24415
+ while (remaining > 0) {
24416
+ bytes.unshift(remaining & 255);
24417
+ remaining = Math.floor(remaining / 256);
24418
+ }
24419
+ return base32Encode(Uint8Array.from(bytes));
24420
+ }
24421
+ function decodeKeyVersion(encoded) {
24422
+ const bytes = base32Decode(encoded);
24423
+ if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
24424
+ let version2 = 0;
24425
+ for (const byte of bytes) version2 = version2 * 256 + byte;
24426
+ if (version2 < 1) throw new Error("vault: bad key version");
24427
+ return version2;
24428
+ }
24429
+ function deriveSubkeys(master) {
24430
+ const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
24431
+ return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
24432
+ }
24433
+ function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
24434
+ if (pointerId.length !== POINTER_ID_BYTES) {
24435
+ throw new Error("vault: pointer id must be 16 bytes");
24436
+ }
24437
+ const head = Buffer.alloc(6);
24438
+ head.writeUInt16BE(formatVersion, 0);
24439
+ head.writeUInt32BE(keyVersion, 2);
24440
+ return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
24441
+ }
24442
+ function seal(encKey, plaintext, aad, nonce) {
24443
+ const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
24444
+ cipher.setAAD(aad);
24445
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24446
+ return { ciphertext, nonce, authTag: cipher.getAuthTag() };
24447
+ }
24448
+ function open(encKey, sealed, aad) {
23810
24449
  try {
23811
- const host = new URL(url2).host;
23812
- if (host !== "") return host;
24450
+ const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
24451
+ decipher.setAAD(aad);
24452
+ decipher.setAuthTag(sealed.authTag);
24453
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
24454
+ } catch {
24455
+ return null;
24456
+ }
24457
+ }
24458
+ function signPointer(signKey, keyVersion, pointerId, category) {
24459
+ return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
24460
+ }
24461
+ function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
24462
+ if (tag.length !== TAG_BYTES) return false;
24463
+ const expected = signPointer(signKey, keyVersion, pointerId, category);
24464
+ return timingSafeEqual(expected, Buffer.from(tag));
24465
+ }
24466
+ function formatPointer(category, keyVersion, pointerId, tag) {
24467
+ return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
24468
+ }
24469
+
24470
+ // ../../packages/persistence/src/vault/key-provider.ts
24471
+ import { execFileSync } from "child_process";
24472
+ import { randomBytes as randomBytes2 } from "crypto";
24473
+ import {
24474
+ chmodSync as chmodSync2,
24475
+ mkdirSync as mkdirSync2,
24476
+ readFileSync as readFileSync3,
24477
+ renameSync as renameSync4,
24478
+ rmSync as rmSync3,
24479
+ statSync,
24480
+ writeFileSync as writeFileSync2
24481
+ } from "fs";
24482
+ import { join as join5 } from "path";
24483
+ var VaultKeyEpochMissingError = class extends Error {
24484
+ version;
24485
+ constructor(version2) {
24486
+ super(`vault: key epoch ${String(version2)} is not present in the keyring`);
24487
+ this.name = "VaultKeyEpochMissingError";
24488
+ this.version = version2;
24489
+ }
24490
+ };
24491
+ var VAULT_KEY_FILENAME = "vault.key";
24492
+ var KEY_MATERIAL_BYTES2 = 32;
24493
+ var KEYCHAIN_SERVICE = "aka-vault";
24494
+ var KEYCHAIN_ACCOUNT = "keyring";
24495
+ function parseKeyring(raw) {
24496
+ const parsed = JSON.parse(raw);
24497
+ if (typeof parsed !== "object" || parsed === null) {
24498
+ throw new Error("vault key file is corrupt: not a JSON object");
24499
+ }
24500
+ const { current, keys } = parsed;
24501
+ if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
24502
+ throw new Error("vault key file is corrupt: bad current version");
24503
+ }
24504
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
24505
+ throw new Error("vault key file is corrupt: bad keys map");
24506
+ }
24507
+ const map2 = /* @__PURE__ */ new Map();
24508
+ for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
24509
+ const version2 = Number(rawVersion);
24510
+ if (!Number.isInteger(version2) || version2 < 1) {
24511
+ throw new Error("vault key file is corrupt: bad key version");
24512
+ }
24513
+ if (typeof rawMaterial !== "string") {
24514
+ throw new Error("vault key file is corrupt: bad key material");
24515
+ }
24516
+ const bytes = Buffer.from(rawMaterial, "base64");
24517
+ if (bytes.length !== KEY_MATERIAL_BYTES2) {
24518
+ throw new Error("vault key file is corrupt: bad key material length");
24519
+ }
24520
+ map2.set(version2, bytes);
24521
+ }
24522
+ if (!map2.has(current)) {
24523
+ throw new Error("vault key file is corrupt: current version has no material");
24524
+ }
24525
+ return { current, keys: map2 };
24526
+ }
24527
+ function serializeKeyring(keyring) {
24528
+ const keys = {};
24529
+ for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
24530
+ const material = keyring.keys.get(version2);
24531
+ if (material) keys[String(version2)] = material.toString("base64");
24532
+ }
24533
+ return JSON.stringify({ current: keyring.current, keys });
24534
+ }
24535
+ function mintKeyring() {
24536
+ return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
24537
+ }
24538
+ function withNextEpoch(keyring) {
24539
+ const next = Math.max(...keyring.keys.keys()) + 1;
24540
+ const keys = new Map(keyring.keys);
24541
+ keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
24542
+ return { current: next, keys };
24543
+ }
24544
+ function currentOf(keyring) {
24545
+ const material = keyring.keys.get(keyring.current);
24546
+ if (!material) throw new VaultKeyEpochMissingError(keyring.current);
24547
+ return { material, version: keyring.current };
24548
+ }
24549
+ function epochOf(keyring, version2) {
24550
+ const material = keyring.keys.get(version2);
24551
+ if (!material) throw new VaultKeyEpochMissingError(version2);
24552
+ return { material, version: version2 };
24553
+ }
24554
+ function asAsync(work) {
24555
+ try {
24556
+ return Promise.resolve(work());
24557
+ } catch (err) {
24558
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
24559
+ }
24560
+ }
24561
+ function asError(err) {
24562
+ return err instanceof Error ? err : new Error(String(err));
24563
+ }
24564
+ var ROTATION_LOCK_STALE_MS = 6e4;
24565
+ var LOCK_OWNER_FILE = "owner";
24566
+ var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
24567
+ function claimRotationLock(lock, owner) {
24568
+ try {
24569
+ mkdirSync2(lock);
24570
+ } catch (err) {
24571
+ if (err.code === "EEXIST") return false;
24572
+ throw asError(err);
24573
+ }
24574
+ try {
24575
+ writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
24576
+ `, { mode: DATA_FILE_MODE });
24577
+ return true;
24578
+ } catch (err) {
24579
+ rmSync3(lock, { recursive: true, force: true });
24580
+ throw asError(err);
24581
+ }
24582
+ }
24583
+ function acquireRotationLock(keysDir2) {
24584
+ const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
24585
+ const owner = randomBytes2(16).toString("hex");
24586
+ if (claimRotationLock(lock, owner)) return { lock, owner };
24587
+ let held;
24588
+ try {
24589
+ held = statSync(lock);
24590
+ } catch {
24591
+ throw new Error(ROTATION_IN_PROGRESS);
24592
+ }
24593
+ if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
24594
+ const aside = `${lock}.stale.${owner}`;
24595
+ try {
24596
+ const now = statSync(lock);
24597
+ if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
24598
+ throw new Error(ROTATION_IN_PROGRESS);
24599
+ }
24600
+ renameSync4(lock, aside);
24601
+ } catch (err) {
24602
+ if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
24603
+ throw new Error(ROTATION_IN_PROGRESS, { cause: err });
24604
+ }
24605
+ rmSync3(aside, { recursive: true, force: true });
24606
+ if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
24607
+ return { lock, owner };
24608
+ }
24609
+ function releaseRotationLock(lease) {
24610
+ try {
24611
+ if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
24612
+ } catch {
24613
+ return;
24614
+ }
24615
+ rmSync3(lease.lock, { recursive: true, force: true });
24616
+ }
24617
+ function withRotationLock(keysDir2, work) {
24618
+ ensureDataDirSync(keysDir2);
24619
+ const lease = acquireRotationLock(keysDir2);
24620
+ try {
24621
+ return work();
24622
+ } finally {
24623
+ releaseRotationLock(lease);
24624
+ }
24625
+ }
24626
+ var FileKeyProvider = class {
24627
+ #keysDir;
24628
+ constructor(keysDir2) {
24629
+ this.#keysDir = keysDir2;
24630
+ }
24631
+ get filePath() {
24632
+ return join5(this.#keysDir, VAULT_KEY_FILENAME);
24633
+ }
24634
+ loadOrCreate() {
24635
+ return asAsync(() => {
24636
+ const existing = this.#read();
24637
+ if (!existing) return currentOf(this.#createExclusive());
24638
+ tightenFileMode(this.filePath);
24639
+ return currentOf(existing);
24640
+ });
24641
+ }
24642
+ rotate() {
24643
+ return asAsync(
24644
+ () => withRotationLock(this.#keysDir, () => {
24645
+ const existing = this.#read();
24646
+ if (!existing) return currentOf(this.#createExclusive());
24647
+ return currentOf(this.#write(withNextEpoch(existing)));
24648
+ })
24649
+ );
24650
+ }
24651
+ materialFor(version2) {
24652
+ return asAsync(() => {
24653
+ const existing = this.#read();
24654
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24655
+ return epochOf(existing, version2);
24656
+ });
24657
+ }
24658
+ /** The keyring, or null when the file is ABSENT. A corrupt file throws. */
24659
+ #read() {
24660
+ let raw;
24661
+ try {
24662
+ raw = readFileSync3(this.filePath, "utf8");
24663
+ } catch (err) {
24664
+ if (err.code === "ENOENT") return null;
24665
+ throw err instanceof Error ? err : new Error(String(err));
24666
+ }
24667
+ return parseKeyring(raw);
24668
+ }
24669
+ /**
24670
+ * First mint: the keyring is created at its FINAL path with a
24671
+ * creation-exclusive write, so two processes racing a fresh machine cannot
24672
+ * each mint a different epoch 1 — with tmp + rename the loser's replace
24673
+ * would orphan everything the winner had already sealed. On EEXIST the
24674
+ * loser re-reads and adopts the winner's keyring; it minted nothing.
24675
+ * Atomic replace is unnecessary here: nothing can be mid-read of a file
24676
+ * that did not exist, and a torn exclusive write parses as corrupt on the
24677
+ * next read and fails secure rather than being re-minted over.
24678
+ */
24679
+ #createExclusive() {
24680
+ ensureDataDirSync(this.#keysDir);
24681
+ const keyring = mintKeyring();
24682
+ try {
24683
+ writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
24684
+ `, {
24685
+ flag: "wx",
24686
+ mode: DATA_FILE_MODE
24687
+ });
24688
+ } catch (err) {
24689
+ if (err.code !== "EEXIST") throw asError(err);
24690
+ const winner = this.#read();
24691
+ if (!winner) {
24692
+ throw new Error("vault: key file vanished during first mint", { cause: err });
24693
+ }
24694
+ return winner;
24695
+ }
24696
+ tightenFileMode(this.filePath);
24697
+ return keyring;
24698
+ }
24699
+ /**
24700
+ * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
24701
+ * Used only for rotation, under the rotation lock — first creation goes
24702
+ * through the creation-exclusive path instead.
24703
+ */
24704
+ #write(keyring) {
24705
+ ensureDataDirSync(this.#keysDir);
24706
+ const file2 = this.filePath;
24707
+ const tmp = `${file2}.tmp`;
24708
+ writeFileSync2(tmp, `${serializeKeyring(keyring)}
24709
+ `, { mode: DATA_FILE_MODE });
24710
+ renameSync4(tmp, file2);
24711
+ tightenFileMode(file2);
24712
+ return keyring;
24713
+ }
24714
+ };
24715
+ function tightenFileMode(file2) {
24716
+ try {
24717
+ chmodSync2(file2, DATA_FILE_MODE);
24718
+ } catch {
24719
+ }
24720
+ }
24721
+ var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
24722
+ encoding: "utf8",
24723
+ stdio: ["ignore", "pipe", "ignore"]
24724
+ });
24725
+ var SECURITY_ITEM_NOT_FOUND = 44;
24726
+ var KeychainKeyProvider = class {
24727
+ #keysDir;
24728
+ #exec;
24729
+ constructor(keysDir2, exec = runSecurity) {
24730
+ if (exec === runSecurity && process.platform !== "darwin") {
24731
+ throw new Error(
24732
+ `keychain custody is not available on this platform (${process.platform}); use file custody`
24733
+ );
24734
+ }
24735
+ this.#keysDir = keysDir2;
24736
+ this.#exec = exec;
24737
+ }
24738
+ /** Where a fallback file provider for the same vault would keep its keyring. */
24739
+ get keysDir() {
24740
+ return this.#keysDir;
24741
+ }
24742
+ loadOrCreate() {
24743
+ return asAsync(() => {
24744
+ const existing = this.#read();
24745
+ if (existing) return currentOf(existing);
24746
+ return currentOf(this.#create(mintKeyring()));
24747
+ });
24748
+ }
24749
+ rotate() {
24750
+ return asAsync(
24751
+ () => withRotationLock(this.#keysDir, () => {
24752
+ const existing = this.#read();
24753
+ if (!existing) return currentOf(this.#create(mintKeyring()));
24754
+ return currentOf(this.#replace(withNextEpoch(existing)));
24755
+ })
24756
+ );
24757
+ }
24758
+ materialFor(version2) {
24759
+ return asAsync(() => {
24760
+ const existing = this.#read();
24761
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24762
+ return epochOf(existing, version2);
24763
+ });
24764
+ }
24765
+ /** The keyring, or null when no item exists yet. A corrupt item throws. */
24766
+ #read() {
24767
+ let raw;
24768
+ try {
24769
+ raw = this.#exec([
24770
+ "find-generic-password",
24771
+ "-s",
24772
+ KEYCHAIN_SERVICE,
24773
+ "-a",
24774
+ KEYCHAIN_ACCOUNT,
24775
+ "-w"
24776
+ ]);
24777
+ } catch (err) {
24778
+ if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
24779
+ throw new Error(
24780
+ `vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
24781
+ { cause: err }
24782
+ );
24783
+ }
24784
+ const body = raw.trim();
24785
+ if (body.length === 0) return null;
24786
+ return parseKeyring(body);
24787
+ }
24788
+ /**
24789
+ * First mint: a plain `add-generic-password` (no `-U`) fails when an item
24790
+ * already exists, so a concurrent first mint cannot overwrite the winner's
24791
+ * keyring — the loser re-reads and adopts it instead.
24792
+ */
24793
+ #create(keyring) {
24794
+ const args = [
24795
+ "add-generic-password",
24796
+ "-s",
24797
+ KEYCHAIN_SERVICE,
24798
+ "-a",
24799
+ KEYCHAIN_ACCOUNT,
24800
+ "-w",
24801
+ serializeKeyring(keyring)
24802
+ ];
24803
+ try {
24804
+ this.#exec(args);
24805
+ } catch (err) {
24806
+ const winner = this.#read();
24807
+ if (winner) return winner;
24808
+ throw asError(err);
24809
+ }
24810
+ return keyring;
24811
+ }
24812
+ // `-U` updates the item in place, deliberately replacing the stored map with
24813
+ // one that contains it — used only for rotation, under the rotation lock.
24814
+ #replace(keyring) {
24815
+ this.#exec([
24816
+ "add-generic-password",
24817
+ "-U",
24818
+ "-s",
24819
+ KEYCHAIN_SERVICE,
24820
+ "-a",
24821
+ KEYCHAIN_ACCOUNT,
24822
+ "-w",
24823
+ serializeKeyring(keyring)
24824
+ ]);
24825
+ return keyring;
24826
+ }
24827
+ };
24828
+ function createKeyProvider(custody, keysDir2) {
24829
+ if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
24830
+ return new FileKeyProvider(keysDir2);
24831
+ }
24832
+
24833
+ // ../../packages/persistence/src/vault/vault.ts
24834
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
24835
+ var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
24836
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
24837
+ var VAULT_PURGE_POINTER_ID = "*";
24838
+ function parsePointer(token) {
24839
+ if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
24840
+ const body = token.slice("[[aka:".length, -"]]".length);
24841
+ const colon = body.indexOf(":");
24842
+ if (colon < 0) return null;
24843
+ const category = body.slice(0, colon);
24844
+ const [kv, id, tag] = body.slice(colon + 1).split(".");
24845
+ if (kv === void 0 || id === void 0 || tag === void 0) return null;
24846
+ try {
24847
+ const keyVersion = decodeKeyVersion(kv);
24848
+ const pointerId = base32Decode(id);
24849
+ const tagBytes = base32Decode(tag);
24850
+ if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
24851
+ return null;
24852
+ }
24853
+ return { category, keyVersion, pointerId, tag: tagBytes };
24854
+ } catch {
24855
+ return null;
24856
+ }
24857
+ }
24858
+ var SecretVault = class {
24859
+ #repo;
24860
+ #keys;
24861
+ #fingerprintKey;
24862
+ #isConsented;
24863
+ #verifyGrant;
24864
+ #now;
24865
+ constructor(deps) {
24866
+ this.#repo = deps.repo;
24867
+ this.#keys = deps.keys;
24868
+ this.#fingerprintKey = deps.fingerprintKey;
24869
+ this.#isConsented = deps.isConsented;
24870
+ this.#verifyGrant = deps.verifyGrant;
24871
+ this.#now = deps.now ?? (() => Date.now());
24872
+ }
24873
+ /**
24874
+ * Store a value and return the pointer that stands for it. The same value
24875
+ * always yields the same pointer on this machine — one row, one pointer id,
24876
+ * one category — which is what makes dedup and reuse counting work.
24877
+ */
24878
+ async tokenize(raw, meta3) {
24879
+ if (!this.#isConsented()) return CONSENT_ABSENT;
24880
+ const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
24881
+ const existing = this.#repo.byValueFingerprint(valueFingerprint);
24882
+ const now = this.#now();
24883
+ if (existing) {
24884
+ this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
24885
+ return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
24886
+ }
24887
+ const { material, version: version2 } = await this.#keys.loadOrCreate();
24888
+ const subkeys = deriveSubkeys(material);
24889
+ const pointerId = randomBytes3(POINTER_ID_BYTES);
24890
+ const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
24891
+ const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
24892
+ const { row } = this.#repo.upsert(
24893
+ {
24894
+ pointerId: base32Encode(pointerId),
24895
+ valueFingerprint,
24896
+ fingerprintKeyVersion: this.#fingerprintKey.version,
24897
+ keyVersion: version2,
24898
+ // Recorded so the row stays OPENABLE if the wire-format constant ever
24899
+ // moves: it is part of this row's AEAD AAD. It is not a tag input —
24900
+ // tags are pinned to the constant on both sides.
24901
+ formatVersion: POINTER_FORMAT_VERSION,
24902
+ category: meta3.category,
24903
+ ruleId: meta3.ruleId,
24904
+ maskedMatch: meta3.maskedMatch,
24905
+ provider: meta3.provider,
24906
+ ciphertext: sealed.ciphertext.toString("base64"),
24907
+ nonce: sealed.nonce.toString("base64"),
24908
+ authTag: sealed.authTag.toString("base64")
24909
+ },
24910
+ now
24911
+ );
24912
+ return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
24913
+ }
24914
+ /**
24915
+ * Resolve a pointer back to its value, for a human or (with a grant) for the
24916
+ * model. Every call that gets as far as an identified row writes an audit row.
24917
+ */
24918
+ async detokenize(token, opts) {
24919
+ const parsed = parsePointer(token);
24920
+ if (!parsed) return UNAVAILABLE;
24921
+ let signKey;
24922
+ try {
24923
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
24924
+ signKey = deriveSubkeys(epoch.material).sign;
24925
+ } catch {
24926
+ return UNAVAILABLE;
24927
+ }
24928
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
24929
+ return UNAVAILABLE;
24930
+ }
24931
+ const pointerId = base32Encode(parsed.pointerId);
24932
+ const row = this.#repo.byPointerId(pointerId);
24933
+ if (!row) {
24934
+ this.#audit(pointerId, opts, "unavailable");
24935
+ return UNAVAILABLE;
24936
+ }
24937
+ if (row.category !== parsed.category) return UNAVAILABLE;
24938
+ if (opts.target === "model") {
24939
+ const grantId = opts.grantId;
24940
+ const verify = this.#verifyGrant;
24941
+ if (verify === void 0 || grantId === void 0 || grantId === "") {
24942
+ this.#audit(pointerId, opts, "refused");
24943
+ return UNAVAILABLE;
24944
+ }
24945
+ let covered;
24946
+ try {
24947
+ covered = await verify(grantId, {
24948
+ ruleId: row.ruleId,
24949
+ valueFingerprint: row.valueFingerprint,
24950
+ fingerprintKeyVersion: row.fingerprintKeyVersion
24951
+ });
24952
+ } catch {
24953
+ covered = false;
24954
+ }
24955
+ if (!covered) {
24956
+ this.#audit(pointerId, opts, "refused");
24957
+ return UNAVAILABLE;
24958
+ }
24959
+ }
24960
+ let raw;
24961
+ try {
24962
+ const epoch = await this.#keys.materialFor(row.keyVersion);
24963
+ raw = open(
24964
+ deriveSubkeys(epoch.material).enc,
24965
+ {
24966
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
24967
+ nonce: Buffer.from(row.nonce, "base64"),
24968
+ authTag: Buffer.from(row.authTag, "base64")
24969
+ },
24970
+ // Sealed under the ROW's epoch and format version. Rotation may have
24971
+ // moved the epoch past the one this token names, and a format bump may
24972
+ // have moved the constant past the generation this row was sealed
24973
+ // under — the AAD follows the row in both cases, never the token.
24974
+ bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
24975
+ );
24976
+ } catch {
24977
+ raw = null;
24978
+ }
24979
+ if (raw === null) {
24980
+ this.#audit(pointerId, opts, "unavailable");
24981
+ return UNAVAILABLE;
24982
+ }
24983
+ this.#audit(pointerId, opts, "revealed");
24984
+ return raw;
24985
+ }
24986
+ /**
24987
+ * Owner-surface reveal by row id: the dashboard shows a row the owner can
24988
+ * already see and asks for its value. There is no wire token here to verify —
24989
+ * the tag exists to stop FORGED tokens arriving in untrusted text, and a row
24990
+ * id selected server-side from the owner's own store is not that — so this
24991
+ * loads the row directly, opens its ciphertext under the row's epoch, and
24992
+ * audits exactly like a human-target de-reference. Never callable with
24993
+ * target 'model': the wire-token path with its grant gate is the only road
24994
+ * raw travels toward the model.
24995
+ */
24996
+ async revealEntry(pointerId, opts) {
24997
+ const row = this.#repo.byPointerId(pointerId);
24998
+ if (!row) {
24999
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
25000
+ return UNAVAILABLE;
25001
+ }
25002
+ const raw = await this.#openRow(row);
25003
+ if (raw === null) {
25004
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
25005
+ return UNAVAILABLE;
25006
+ }
25007
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
25008
+ return raw;
25009
+ }
25010
+ /** Badge and listing data. No raw value, no fingerprint, and no audit row. */
25011
+ async describePointer(token) {
25012
+ const row = await this.#rowFor(token);
25013
+ if (!row) return null;
25014
+ return {
25015
+ category: row.category,
25016
+ ...row.provider === void 0 ? {} : { provider: row.provider },
25017
+ maskedMatch: row.maskedMatch,
25018
+ occurrences: row.occurrenceCount,
25019
+ firstSeen: new Date(row.firstSeen).toISOString(),
25020
+ lastSeen: new Date(row.lastSeen).toISOString()
25021
+ };
25022
+ }
25023
+ /**
25024
+ * The raw-free row identity a reveal grant matches on. Deliberately not fed to
25025
+ * view surfaces: the keyed fingerprint is a correlation key and must not reach
25026
+ * a presentation layer.
25027
+ */
25028
+ async resolvePointerIdentity(token) {
25029
+ const row = await this.#rowFor(token);
25030
+ if (!row) return null;
25031
+ return {
25032
+ ruleId: row.ruleId,
25033
+ valueFingerprint: row.valueFingerprint,
25034
+ fingerprintKeyVersion: row.fingerprintKeyVersion
25035
+ };
25036
+ }
25037
+ /**
25038
+ * Mint the next vault key epoch and re-encrypt every entry under it. Pointers
25039
+ * already emitted keep verifying: their tag is checked against the historical
25040
+ * epoch they name, which the key provider retains.
25041
+ *
25042
+ * Safe to interrupt — each row carries the epoch its ciphertext is sealed
25043
+ * under, so a half-finished pass leaves every row openable.
25044
+ *
25045
+ * The rotation lock covers only the keyring mint inside `rotate()`; the
25046
+ * re-seal pass below runs unlocked. Two concurrent rotations therefore
25047
+ * serialize on the keyring but interleave over the rows, so a slower pass can
25048
+ * re-seal a row back to an epoch a faster one already moved past, and
25049
+ * `reEncrypted` can double-count. No value is lost either way — every epoch is
25050
+ * retained and every row stays openable — but "after rotation every row sits
25051
+ * at the newest epoch" does not hold under concurrency. Holding the lock
25052
+ * across the whole pass requires an async-aware lock, since a callback that
25053
+ * awaits would release the lock at its first suspension.
25054
+ */
25055
+ async rotateVaultKey() {
25056
+ const next = await this.#keys.rotate();
25057
+ const nextEnc = deriveSubkeys(next.material).enc;
25058
+ let reEncrypted = 0;
25059
+ for (const row of this.#repo.listAll()) {
25060
+ if (row.keyVersion === next.version) continue;
25061
+ const pointerId = base32Decode(row.pointerId);
25062
+ let raw;
25063
+ try {
25064
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25065
+ raw = open(
25066
+ deriveSubkeys(epoch.material).enc,
25067
+ {
25068
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25069
+ nonce: Buffer.from(row.nonce, "base64"),
25070
+ authTag: Buffer.from(row.authTag, "base64")
25071
+ },
25072
+ bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
25073
+ );
25074
+ } catch {
25075
+ raw = null;
25076
+ }
25077
+ if (raw === null) continue;
25078
+ const sealed = seal(
25079
+ nextEnc,
25080
+ raw,
25081
+ bindingInput(next.version, pointerId, row.category, row.formatVersion),
25082
+ randomBytes3(NONCE_BYTES)
25083
+ );
25084
+ this.#repo.replaceCiphertext(row.pointerId, {
25085
+ keyVersion: next.version,
25086
+ ciphertext: sealed.ciphertext.toString("base64"),
25087
+ nonce: sealed.nonce.toString("base64"),
25088
+ authTag: sealed.authTag.toString("base64")
25089
+ });
25090
+ reEncrypted += 1;
25091
+ }
25092
+ return { version: next.version, reEncrypted };
25093
+ }
25094
+ /**
25095
+ * Re-key every entry's value fingerprint after the exception key rotates,
25096
+ * PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
25097
+ * because the raw values are gone — the vault still holds the values, so
25098
+ * determinism, dedup, and every outstanding pointer survive the rotation.
25099
+ *
25100
+ * Every fingerprint-key rotation must run this: a row left at the old epoch
25101
+ * still resolves, but the same value detected again fingerprints under the
25102
+ * NEW key, misses the dedup lookup, and mints a second row and a second
25103
+ * pointer — one value, two tokens in circulation.
25104
+ *
25105
+ * Per-row best-effort: a row that cannot open, or whose refreshed
25106
+ * fingerprint collides with a row already refreshed, is skipped rather than
25107
+ * aborting the pass — one damaged entry must not strand the re-key of every
25108
+ * other. A skipped row keeps resolving under its old fingerprint epoch.
25109
+ */
25110
+ async refreshFingerprints(next) {
25111
+ let refreshed = 0;
25112
+ for (const row of this.#repo.listAll()) {
25113
+ try {
25114
+ const raw = await this.#openRow(row);
25115
+ if (raw === null) continue;
25116
+ this.#repo.refreshFingerprint(row.pointerId, {
25117
+ valueFingerprint: fingerprintValue(next, raw),
25118
+ fingerprintKeyVersion: next.version
25119
+ });
25120
+ refreshed += 1;
25121
+ } catch {
25122
+ continue;
25123
+ }
25124
+ }
25125
+ return refreshed;
25126
+ }
25127
+ /**
25128
+ * Destroy every entry, making all outstanding pointers permanently
25129
+ * unresolvable.
25130
+ *
25131
+ * The count comes from `purgeAll` rather than a separate `countEntries` —
25132
+ * `purgeAll` counts inside the same transaction that deletes, so the audit row
25133
+ * reports what was actually destroyed. Counting beforehand would let a
25134
+ * concurrent write land between the two statements and put a number in the
25135
+ * durable record that never matched reality.
25136
+ */
25137
+ purgeVault() {
25138
+ const destroyed = this.#repo.purgeAll();
25139
+ this.#repo.recordDeref({
25140
+ id: randomUUID10(),
25141
+ pointerId: VAULT_PURGE_POINTER_ID,
25142
+ at: this.#now(),
25143
+ target: "human",
25144
+ reason: "purge",
25145
+ outcome: "unavailable",
25146
+ pointerCount: Math.max(destroyed, 1)
25147
+ });
25148
+ return destroyed;
25149
+ }
25150
+ // Sign under the epoch the token names — which for a re-detected value is the
25151
+ // epoch its row currently sits at rather than whatever is current.
25152
+ //
25153
+ // The row's format version is NOT a tag input. It binds the row's ciphertext
25154
+ // (it is part of the AEAD AAD, so an old row stays openable) but never the
25155
+ // wire tag, which verification checks against POINTER_FORMAT_VERSION without
25156
+ // knowing any row. Signing a token here under a row's own generation is what
25157
+ // would make the vault emit tokens it then refuses.
25158
+ async #emitToken(keyVersion, pointerIdB32, category) {
25159
+ const pointerId = base32Decode(pointerIdB32);
25160
+ const epoch = await this.#keys.materialFor(keyVersion);
25161
+ const signKey = deriveSubkeys(epoch.material).sign;
25162
+ return formatPointer(
25163
+ category,
25164
+ keyVersion,
25165
+ pointerId,
25166
+ signPointer(signKey, keyVersion, pointerId, category)
25167
+ );
25168
+ }
25169
+ async #openRow(row) {
25170
+ try {
25171
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25172
+ return open(
25173
+ deriveSubkeys(epoch.material).enc,
25174
+ {
25175
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25176
+ nonce: Buffer.from(row.nonce, "base64"),
25177
+ authTag: Buffer.from(row.authTag, "base64")
25178
+ },
25179
+ bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
25180
+ );
25181
+ } catch {
25182
+ return null;
25183
+ }
25184
+ }
25185
+ // Shared lookup for the read-only surfaces. It verifies the tag exactly as
25186
+ // detokenize does: a descriptor is not raw, but a token nobody can vouch for
25187
+ // should not resolve to anything at all — otherwise a fabricated pointer, or a
25188
+ // lookalike planted in a file, would still yield a category and a masked
25189
+ // preview. Verifying needs the historical epoch's key, which is why these
25190
+ // surfaces are async.
25191
+ async #rowFor(token) {
25192
+ const parsed = parsePointer(token);
25193
+ if (!parsed) return null;
25194
+ try {
25195
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
25196
+ const signKey = deriveSubkeys(epoch.material).sign;
25197
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
25198
+ return null;
25199
+ }
25200
+ } catch {
25201
+ return null;
25202
+ }
25203
+ const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
25204
+ if (row?.category !== parsed.category) return null;
25205
+ return row;
25206
+ }
25207
+ #audit(pointerId, opts, outcome) {
25208
+ this.#repo.recordDeref({
25209
+ id: randomUUID10(),
25210
+ pointerId,
25211
+ at: this.#now(),
25212
+ target: opts.target,
25213
+ reason: opts.reason,
25214
+ outcome,
25215
+ ...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
25216
+ // Only the batched reasons carry a count above one; a model crossing is
25217
+ // always its own row.
25218
+ pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
25219
+ });
25220
+ }
25221
+ };
25222
+
25223
+ // ../../packages/persistence/src/warn-era-cap.ts
25224
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25225
+ import { join as join6 } from "path";
25226
+ var MARKER = "warn-era-capped";
25227
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25228
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25229
+ const marker = join6(dataDir2, MARKER);
25230
+ if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25231
+ const capped = db.policies.capCategoryActions();
25232
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
25233
+ `, { mode: DATA_FILE_MODE });
25234
+ return { capped };
25235
+ }
25236
+
25237
+ // ../../packages/plugin-sdk/src/provider-env.ts
25238
+ var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
25239
+ var booleanish = external_exports.string().optional().transform((v) => {
25240
+ if (v === void 0) return void 0;
25241
+ const t = v.trim().toLowerCase();
25242
+ if (t === "" || t === "false" || t === "0") return false;
25243
+ return true;
25244
+ }).catch(void 0);
25245
+ var optionalBaseUrl = external_exports.preprocess((v) => {
25246
+ if (typeof v === "string" && v.trim() === "") return void 0;
25247
+ return v;
25248
+ }, external_exports.string().optional()).catch(void 0);
25249
+ var providerEnvShape = {
25250
+ CLAUDE_CODE_USE_BEDROCK: booleanish,
25251
+ CLAUDE_CODE_USE_VERTEX: booleanish,
25252
+ ANTHROPIC_BASE_URL: optionalBaseUrl
25253
+ };
25254
+ var ProviderEnvSchema = external_exports.object(providerEnvShape);
25255
+
25256
+ // ../../packages/plugin-sdk/src/provider.ts
25257
+ function hostOf(url2) {
25258
+ try {
25259
+ const host = new URL(url2).host;
25260
+ if (host !== "") return host;
23813
25261
  } catch {
23814
25262
  }
23815
25263
  try {
@@ -23848,8 +25296,8 @@ function providerFromModelId(modelId) {
23848
25296
  function loadConfig(base = defaultDataDir()) {
23849
25297
  try {
23850
25298
  ensureLayoutDirSync(base);
23851
- const settingsFile = join6(settingsDir(base), "settings.json");
23852
- if (existsSync3(settingsFile)) tightenFile(settingsFile);
25299
+ const settingsFile = join7(settingsDir(base), "settings.json");
25300
+ if (existsSync4(settingsFile)) tightenFile(settingsFile);
23853
25301
  } catch {
23854
25302
  }
23855
25303
  migrateLegacyLayout(base);
@@ -23872,9 +25320,9 @@ function resolveProviderSafe() {
23872
25320
  }
23873
25321
 
23874
25322
  // ../../packages/plugin-sdk/src/config-inventory.ts
23875
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
25323
+ import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
23876
25324
  import { homedir as homedir2 } from "os";
23877
- import { basename as basename2, join as join8 } from "path";
25325
+ import { basename as basename2, join as join9 } from "path";
23878
25326
 
23879
25327
  // ../../packages/detections/src/egress/registry.ts
23880
25328
  var EXTRACTOR_VERSION = "1";
@@ -24662,12 +26110,12 @@ function redact(text, findings) {
24662
26110
  const regions = [];
24663
26111
  for (const f of sorted) {
24664
26112
  const rank = SEVERITY_RANK2[f.severity];
24665
- const open = regions[regions.length - 1];
24666
- if (open && f.span.start < open.end) {
24667
- open.end = Math.max(open.end, f.span.end);
24668
- if (rank > open.rank) {
24669
- open.rank = rank;
24670
- open.category = f.category;
26113
+ const open2 = regions[regions.length - 1];
26114
+ if (open2 && f.span.start < open2.end) {
26115
+ open2.end = Math.max(open2.end, f.span.end);
26116
+ if (rank > open2.rank) {
26117
+ open2.rank = rank;
26118
+ open2.category = f.category;
24671
26119
  }
24672
26120
  } else {
24673
26121
  regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
@@ -24696,6 +26144,24 @@ function maskMatch(raw) {
24696
26144
  return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
24697
26145
  }
24698
26146
 
26147
+ // ../../packages/detections/src/pointer-shield.ts
26148
+ function shieldPointers(text) {
26149
+ const spans = [];
26150
+ let out = null;
26151
+ for (const match of text.matchAll(pointerTokenScanner())) {
26152
+ spans.push({ start: match.index, end: match.index + match[0].length });
26153
+ out ??= text;
26154
+ out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
26155
+ }
26156
+ return { text: out ?? text, spans };
26157
+ }
26158
+ function dropShieldedFindings(findings, spans) {
26159
+ if (spans.length === 0) return findings;
26160
+ return findings.filter(
26161
+ (finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
26162
+ );
26163
+ }
26164
+
24699
26165
  // ../../packages/detections/src/posture/config-posture.ts
24700
26166
  var RULE_VERSION = "1";
24701
26167
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
@@ -26910,7 +28376,8 @@ function scanText(text, ruleVersions) {
26910
28376
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
26911
28377
  try {
26912
28378
  const rules = getLoadedRules();
26913
- const matches = scan(text, rules);
28379
+ const shielded = shieldPointers(text);
28380
+ const matches = dropShieldedFindings(scan(shielded.text, rules), shielded.spans);
26914
28381
  if (matches.length === 0) return { masked: text, findings: [] };
26915
28382
  const byId = new Map(rules.map((r) => [r.id, r]));
26916
28383
  const findings = matches.map((m) => {
@@ -26933,8 +28400,8 @@ function scanText(text, ruleVersions) {
26933
28400
  }
26934
28401
 
26935
28402
  // ../../packages/plugin-sdk/src/repo.ts
26936
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
26937
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
28403
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28404
+ import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
26938
28405
  function resolveRepoIdentity(cwd) {
26939
28406
  try {
26940
28407
  const root = findGitRoot(cwd);
@@ -26967,36 +28434,36 @@ function resolveRepoNwo(cwd) {
26967
28434
  function findGitRoot(start) {
26968
28435
  let dir = start;
26969
28436
  for (; ; ) {
26970
- if (existsSync4(join7(dir, ".git"))) return dir;
28437
+ if (existsSync5(join8(dir, ".git"))) return dir;
26971
28438
  const parent = dirname(dir);
26972
28439
  if (parent === dir) return void 0;
26973
28440
  dir = parent;
26974
28441
  }
26975
28442
  }
26976
28443
  function resolveGitContext(root) {
26977
- const dotGit = join7(root, ".git");
28444
+ const dotGit = join8(root, ".git");
26978
28445
  try {
26979
- if (statSync(dotGit).isDirectory()) {
26980
- return { configPath: join7(dotGit, "config"), headRoot: root };
28446
+ if (statSync2(dotGit).isDirectory()) {
28447
+ return { configPath: join8(dotGit, "config"), headRoot: root };
26981
28448
  }
26982
28449
  } catch {
26983
28450
  return void 0;
26984
28451
  }
26985
28452
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
26986
28453
  if (!target) return void 0;
26987
- const gitdir = isAbsolute(target) ? target : join7(root, target);
26988
- if (existsSync4(join7(gitdir, "config"))) {
26989
- return { configPath: join7(gitdir, "config"), headRoot: root };
28454
+ const gitdir = isAbsolute(target) ? target : join8(root, target);
28455
+ if (existsSync5(join8(gitdir, "config"))) {
28456
+ return { configPath: join8(gitdir, "config"), headRoot: root };
26990
28457
  }
26991
- const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
28458
+ const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
26992
28459
  if (!commonRaw) return void 0;
26993
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
28460
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
26994
28461
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
26995
- return { configPath: join7(commonGitDir, "config"), headRoot };
28462
+ return { configPath: join8(commonGitDir, "config"), headRoot };
26996
28463
  }
26997
28464
  function safeRead(path) {
26998
28465
  try {
26999
- return readFileSync3(path, "utf8");
28466
+ return readFileSync4(path, "utf8");
27000
28467
  } catch {
27001
28468
  return void 0;
27002
28469
  }
@@ -27047,13 +28514,13 @@ function nwoFromUrl(url2) {
27047
28514
  }
27048
28515
 
27049
28516
  // ../../packages/plugin-sdk/src/events.ts
27050
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
28517
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
27051
28518
  function contentHashOf(text) {
27052
28519
  return createHash4("sha256").update(text).digest("hex");
27053
28520
  }
27054
28521
  function buildIngestEvent(input) {
27055
28522
  return {
27056
- id: randomUUID9(),
28523
+ id: randomUUID11(),
27057
28524
  sourceTool: input.sourceTool,
27058
28525
  kind: input.kind,
27059
28526
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -27064,7 +28531,7 @@ function buildIngestEvent(input) {
27064
28531
  // SDK boot in the fail-open hook path). Preserve any id the caller already set.
27065
28532
  metadata: {
27066
28533
  ...input.metadata,
27067
- correlationId: input.metadata?.correlationId ?? randomUUID9()
28534
+ correlationId: input.metadata?.correlationId ?? randomUUID11()
27068
28535
  }
27069
28536
  };
27070
28537
  }
@@ -27099,8 +28566,8 @@ function resolveInventoryContext(input) {
27099
28566
  }
27100
28567
 
27101
28568
  // ../../packages/plugin-sdk/src/nudge.ts
27102
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27103
- import { join as join9 } from "path";
28569
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28570
+ import { join as join10 } from "path";
27104
28571
 
27105
28572
  // ../../packages/plugin-sdk/src/paths.ts
27106
28573
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -27108,8 +28575,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
27108
28575
 
27109
28576
  // ../../packages/plugin-sdk/src/project-files.ts
27110
28577
  var import_ignore = __toESM(require_ignore(), 1);
27111
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27112
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
28578
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28579
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
27113
28580
 
27114
28581
  // ../../packages/plugin-sdk/src/raw-egress.ts
27115
28582
  var RawEgressError = class extends Error {
@@ -27205,7 +28672,7 @@ async function filterUnsafeRules(rules, gateway, opts) {
27205
28672
  }
27206
28673
 
27207
28674
  // ../../packages/plugin-sdk/src/runtime.ts
27208
- import { randomUUID as randomUUID10 } from "crypto";
28675
+ import { randomUUID as randomUUID12 } from "crypto";
27209
28676
  var ENFORCEMENT_CEILING_ENABLED = false;
27210
28677
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
27211
28678
  function entryIsActive(entry, now) {
@@ -27308,7 +28775,12 @@ function createPluginRuntime(gateway, settings, opts) {
27308
28775
  if (worst === "block") return { action: "block", text: null, findings };
27309
28776
  if (worst === "redact") {
27310
28777
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
27311
- return { action: "redact", text: redact(text, redactFindings), findings };
28778
+ return {
28779
+ action: "redact",
28780
+ text: redact(text, redactFindings),
28781
+ findings,
28782
+ enforcedFindings: redactFindings
28783
+ };
27312
28784
  }
27313
28785
  return { action: worst, text, findings };
27314
28786
  }
@@ -27349,9 +28821,17 @@ function createPluginRuntime(gateway, settings, opts) {
27349
28821
  else groups.set(pair, [finding]);
27350
28822
  }
27351
28823
  const now = Date.now();
28824
+ const preAuthorized = new Set(ctx.preAuthorizedGrantIds ?? []);
27352
28825
  for (const [pair, group] of groups) {
27353
28826
  const entry = entries.get(pair);
27354
- if (!entry || !entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
28827
+ if (!entry) continue;
28828
+ if (preAuthorized.has(entry.id)) {
28829
+ if (!conditionsMatch(entry.conditions, ctx)) continue;
28830
+ for (const finding of group) excepted.add(finding);
28831
+ exceptionIds.push(entry.id);
28832
+ continue;
28833
+ }
28834
+ if (!entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
27355
28835
  continue;
27356
28836
  }
27357
28837
  let consumed = false;
@@ -27383,7 +28863,7 @@ function createPluginRuntime(gateway, settings, opts) {
27383
28863
  const pair = `${finding.ruleId}:${fp}`;
27384
28864
  if (seen.has(pair)) continue;
27385
28865
  seen.add(pair);
27386
- const reference = randomUUID10().replaceAll("-", "").slice(0, 6);
28866
+ const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
27387
28867
  const maskedValue = maskMatch(finding.rawMatch);
27388
28868
  try {
27389
28869
  await gateway.recordBlockedDetection({
@@ -27407,7 +28887,8 @@ function createPluginRuntime(gateway, settings, opts) {
27407
28887
  async function evaluate(text, context, ctx) {
27408
28888
  try {
27409
28889
  await ensureInitialized();
27410
- const findings = scan(text, rules, context);
28890
+ const shielded = shieldPointers(text);
28891
+ const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
27411
28892
  const fpCache = /* @__PURE__ */ new Map();
27412
28893
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
27413
28894
  const decision = decide(findings, text, excepted);
@@ -27430,7 +28911,11 @@ function createPluginRuntime(gateway, settings, opts) {
27430
28911
  const { decision, excepted, exceptionIds } = await evaluate(
27431
28912
  input.text,
27432
28913
  filePath ? { filePath } : void 0,
27433
- { sourceTool: input.sourceTool, metadata: input.metadata }
28914
+ {
28915
+ sourceTool: input.sourceTool,
28916
+ metadata: input.metadata,
28917
+ preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
28918
+ }
27434
28919
  );
27435
28920
  if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
27436
28921
  try {
@@ -27460,7 +28945,7 @@ function createPluginRuntime(gateway, settings, opts) {
27460
28945
  valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
27461
28946
  }) : void 0;
27462
28947
  return {
27463
- id: randomUUID10(),
28948
+ id: randomUUID12(),
27464
28949
  eventId: event.id,
27465
28950
  ruleId: match.ruleId,
27466
28951
  category: match.category,
@@ -27486,7 +28971,7 @@ function createPluginRuntime(gateway, settings, opts) {
27486
28971
  const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
27487
28972
  return contentHashOf(JSON.stringify(sorted));
27488
28973
  } catch {
27489
- return `unresolved-${randomUUID10()}`;
28974
+ return `unresolved-${randomUUID12()}`;
27490
28975
  }
27491
28976
  }
27492
28977
  async function close() {
@@ -27499,11 +28984,306 @@ function createPluginRuntime(gateway, settings, opts) {
27499
28984
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27500
28985
 
27501
28986
  // ../../packages/plugin-sdk/src/throttle.ts
27502
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27503
- import { join as join11 } from "path";
28987
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28988
+ import { join as join12 } from "path";
28989
+
28990
+ // ../../packages/plugin-sdk/src/tokenize.ts
28991
+ function redactedPlaceholder(category) {
28992
+ return `[REDACTED:${category.toUpperCase()}]`;
28993
+ }
28994
+ var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
28995
+ var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
28996
+ function groupSpans(text, findings) {
28997
+ 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);
28998
+ const groups = [];
28999
+ for (const finding of sorted) {
29000
+ const last = groups[groups.length - 1];
29001
+ if (last && finding.span.start < last.end) {
29002
+ last.end = Math.max(last.end, finding.span.end);
29003
+ if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
29004
+ last.category = finding.category;
29005
+ last.severity = finding.severity;
29006
+ }
29007
+ delete last.finding;
29008
+ continue;
29009
+ }
29010
+ groups.push({
29011
+ start: finding.span.start,
29012
+ end: finding.span.end,
29013
+ finding,
29014
+ category: finding.category,
29015
+ severity: finding.severity
29016
+ });
29017
+ }
29018
+ return groups;
29019
+ }
29020
+ var NULL_RESOLVER = () => Promise.resolve(null);
29021
+ var SecretVaultGlue = class {
29022
+ #vault;
29023
+ revealGrantResolver;
29024
+ // Set only when THIS glue opened the store, so a glue over an injected vault
29025
+ // never closes a handle it does not own.
29026
+ #release;
29027
+ constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
29028
+ this.#vault = vault;
29029
+ this.revealGrantResolver = revealGrantResolver;
29030
+ this.#release = release2;
29031
+ }
29032
+ close() {
29033
+ const release2 = this.#release;
29034
+ this.#release = void 0;
29035
+ try {
29036
+ release2?.();
29037
+ } catch {
29038
+ }
29039
+ }
29040
+ async tokenizeValue(raw, meta3) {
29041
+ try {
29042
+ const result = await this.#vault.tokenize(raw, meta3);
29043
+ return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
29044
+ } catch {
29045
+ return redactedPlaceholder(meta3.category);
29046
+ }
29047
+ }
29048
+ async tokenizeText(text, opts) {
29049
+ try {
29050
+ const findings = opts?.findings ?? this.#selfScan(text);
29051
+ if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
29052
+ if (findings.length === 0) return { text, pointers: [], degraded: [] };
29053
+ const groups = groupSpans(text, findings);
29054
+ const pointers = [];
29055
+ const degraded = [];
29056
+ let out = text;
29057
+ for (const group of [...groups].reverse()) {
29058
+ const original = text.slice(group.start, group.end);
29059
+ const finding = group.finding;
29060
+ let replacement;
29061
+ if (finding === void 0) {
29062
+ replacement = redactedPlaceholder(group.category);
29063
+ degraded.unshift({ category: group.category });
29064
+ } else if (original !== finding.rawMatch) {
29065
+ replacement = redactedPlaceholder(group.category);
29066
+ degraded.unshift({ category: group.category });
29067
+ } else {
29068
+ replacement = await this.tokenizeValue(finding.rawMatch, {
29069
+ ruleId: finding.ruleId,
29070
+ category: finding.category,
29071
+ maskedMatch: maskMatch(finding.rawMatch)
29072
+ });
29073
+ if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
29074
+ else degraded.unshift({ category: finding.category });
29075
+ }
29076
+ out = out.slice(0, group.start) + replacement + out.slice(group.end);
29077
+ }
29078
+ if (opts?.sighting && pointers.length > 0) {
29079
+ for (const pointer of pointers) {
29080
+ try {
29081
+ const id = pointer.split(".")[1];
29082
+ if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
29083
+ } catch {
29084
+ }
29085
+ }
29086
+ }
29087
+ return { text: out, pointers, degraded };
29088
+ } catch {
29089
+ return { text: "[REDACTED]", pointers: [], degraded: [] };
29090
+ }
29091
+ }
29092
+ async detokenizeText(text, opts) {
29093
+ try {
29094
+ const matches = [...text.matchAll(pointerTokenScanner())];
29095
+ if (matches.length === 0) return { text, revealed: 0 };
29096
+ const occurrences = /* @__PURE__ */ new Map();
29097
+ for (const match of matches) {
29098
+ occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
29099
+ }
29100
+ const resolved = /* @__PURE__ */ new Map();
29101
+ for (const [pointer, count] of occurrences) {
29102
+ try {
29103
+ const value = await this.#vault.detokenize(pointer, {
29104
+ target: "human",
29105
+ reason: opts.reason,
29106
+ pointerCount: count
29107
+ });
29108
+ resolved.set(pointer, typeof value === "string" ? value : null);
29109
+ } catch {
29110
+ resolved.set(pointer, null);
29111
+ }
29112
+ }
29113
+ let out = text;
29114
+ let revealed = 0;
29115
+ for (const match of [...matches].reverse()) {
29116
+ const value = resolved.get(match[0]);
29117
+ const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
29118
+ if (value !== null && value !== void 0) revealed += 1;
29119
+ out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
29120
+ }
29121
+ return { text: out, revealed };
29122
+ } catch {
29123
+ return { text, revealed: 0 };
29124
+ }
29125
+ }
29126
+ // Scan with the bundled packs, as the mask path does. Pointers already in the
29127
+ // text are blanked first so a pointer is never re-tokenized. Returns null
29128
+ // when the registry or the scan itself failed — the caller must then treat
29129
+ // the whole text as unclassifiable.
29130
+ #selfScan(text) {
29131
+ try {
29132
+ registerBundledPacks();
29133
+ const shielded = shieldPointers(text);
29134
+ return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
29135
+ } catch {
29136
+ return null;
29137
+ }
29138
+ }
29139
+ async describePointerSafe(token) {
29140
+ try {
29141
+ return await this.#vault.describePointer(token);
29142
+ } catch {
29143
+ return null;
29144
+ }
29145
+ }
29146
+ async probeModelPointers(text, opts) {
29147
+ const granted = /* @__PURE__ */ new Map();
29148
+ const ungranted = [];
29149
+ try {
29150
+ for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
29151
+ try {
29152
+ const grantId = await opts.resolveGrant(pointer);
29153
+ if (grantId === null) ungranted.push(pointer);
29154
+ else granted.set(pointer, grantId);
29155
+ } catch {
29156
+ ungranted.push(pointer);
29157
+ }
29158
+ }
29159
+ return { granted, ungranted };
29160
+ } catch {
29161
+ return { granted: /* @__PURE__ */ new Map(), ungranted };
29162
+ }
29163
+ }
29164
+ async substituteModelPointers(text, opts) {
29165
+ try {
29166
+ const matches = [...text.matchAll(pointerTokenScanner())];
29167
+ if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
29168
+ const resolved = /* @__PURE__ */ new Map();
29169
+ for (const pointer of new Set(matches.map((m) => m[0]))) {
29170
+ try {
29171
+ const grantId = await opts.resolveGrant(pointer);
29172
+ if (grantId === null) {
29173
+ await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
29174
+ resolved.set(pointer, null);
29175
+ continue;
29176
+ }
29177
+ const value = await this.#vault.detokenize(pointer, {
29178
+ target: "model",
29179
+ reason: "model-input",
29180
+ grantId
29181
+ });
29182
+ resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
29183
+ } catch {
29184
+ resolved.set(pointer, null);
29185
+ }
29186
+ }
29187
+ const spentGrants = /* @__PURE__ */ new Set();
29188
+ for (const entry of resolved.values()) {
29189
+ if (entry === null || spentGrants.has(entry.grantId)) continue;
29190
+ spentGrants.add(entry.grantId);
29191
+ try {
29192
+ await this.#vault.consumeGrant?.(entry.grantId);
29193
+ } catch {
29194
+ }
29195
+ }
29196
+ let out = text;
29197
+ const revealed = /* @__PURE__ */ new Set();
29198
+ const unresolved = /* @__PURE__ */ new Set();
29199
+ for (const match of [...matches].reverse()) {
29200
+ const entry = resolved.get(match[0]);
29201
+ if (entry === null || entry === void 0) {
29202
+ unresolved.add(match[0]);
29203
+ continue;
29204
+ }
29205
+ revealed.add(match[0]);
29206
+ out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
29207
+ }
29208
+ return {
29209
+ text: out,
29210
+ revealed: [...revealed],
29211
+ unresolved: [...unresolved],
29212
+ grantIds: [...spentGrants]
29213
+ };
29214
+ } catch {
29215
+ return { text, revealed: [], unresolved: [], grantIds: [] };
29216
+ }
29217
+ }
29218
+ };
29219
+ function createVaultGlue(options) {
29220
+ if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
29221
+ const base = options?.base ?? defaultDataDir();
29222
+ try {
29223
+ const dir = dataDir(base);
29224
+ const db = openLocalDatabase(dir);
29225
+ const settings = readWorkspaceSettings(base);
29226
+ const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
29227
+ const vault = new SecretVault({
29228
+ repo: db.secretVault,
29229
+ keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
29230
+ fingerprintKey: loadOrCreateFingerprintKey(dir),
29231
+ // Read live so a revocation applies to the very next call, not the next
29232
+ // process.
29233
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
29234
+ // This is the one construction site that reveals to the model, so it is
29235
+ // the one that supplies the last gate. The decision is re-taken from the
29236
+ // ROW's identity at the moment of crossing, which closes the window
29237
+ // between resolving a grant and spending it: a grant revoked in between
29238
+ // refuses here.
29239
+ //
29240
+ // The re-decision is on the identity alone, never on the grant id
29241
+ // matching the one the resolver returned. ExceptionPolicyProvider
29242
+ // promises no id stability across calls — a provider deciding from
29243
+ // external policy may well mint a fresh id each time — so comparing ids
29244
+ // would silently refuse every crossing for such a provider while looking
29245
+ // like a security check. `allow` for this row is the whole question.
29246
+ verifyGrant: async (_grantId, identity) => {
29247
+ const decision = await provider.decideReveal(identity);
29248
+ return decision.allow;
29249
+ }
29250
+ });
29251
+ const vaultWithSightings = {
29252
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
29253
+ detokenize: (token, opts) => vault.detokenize(token, opts),
29254
+ describePointer: (token) => vault.describePointer(token),
29255
+ resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
29256
+ recordSighting: (pointerId, sighting) => {
29257
+ db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
29258
+ },
29259
+ consumeGrant: (grantId) => db.exceptions.consume(grantId)
29260
+ };
29261
+ const revealGrantResolver = async (pointer) => {
29262
+ try {
29263
+ const identity = await vault.resolvePointerIdentity(pointer);
29264
+ if (identity === null) return null;
29265
+ const decision = await provider.decideReveal(identity);
29266
+ return decision.allow ? decision.grantId : null;
29267
+ } catch {
29268
+ return null;
29269
+ }
29270
+ };
29271
+ return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
29272
+ db.close();
29273
+ });
29274
+ } catch {
29275
+ return new SecretVaultGlue(UNOPENABLE_VAULT);
29276
+ }
29277
+ }
29278
+ var UNOPENABLE_VAULT = {
29279
+ tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29280
+ detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29281
+ describePointer: () => Promise.resolve(null),
29282
+ resolvePointerIdentity: () => Promise.resolve(null)
29283
+ };
27504
29284
 
27505
29285
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
27506
- import { randomUUID as randomUUID11 } from "crypto";
29286
+ import { randomUUID as randomUUID13 } from "crypto";
27507
29287
 
27508
29288
  // ../../packages/plugin-runtime/src/recorder.ts
27509
29289
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -27542,12 +29322,12 @@ var StandaloneDataGateway = class {
27542
29322
  // reconciler drops the whole pass and recovers it idempotently on the next read.
27543
29323
  recordLlmCalls(inputs) {
27544
29324
  if (inputs.length === 0) return Promise.resolve();
27545
- return new Promise((resolve, reject) => {
29325
+ return new Promise((resolve2, reject) => {
27546
29326
  try {
27547
29327
  this.db.auditEvents.runInTransaction(() => {
27548
29328
  for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
27549
29329
  });
27550
- resolve();
29330
+ resolve2();
27551
29331
  } catch (err) {
27552
29332
  reject(err instanceof Error ? err : new Error(String(err)));
27553
29333
  }
@@ -27559,12 +29339,12 @@ var StandaloneDataGateway = class {
27559
29339
  // drops the whole pass and recovers it idempotently next time.
27560
29340
  recordToolCalls(inputs) {
27561
29341
  if (inputs.length === 0) return Promise.resolve();
27562
- return new Promise((resolve, reject) => {
29342
+ return new Promise((resolve2, reject) => {
27563
29343
  try {
27564
29344
  this.db.auditEvents.runInTransaction(() => {
27565
29345
  for (const input of inputs) this.writeToolCall(input);
27566
29346
  });
27567
- resolve();
29347
+ resolve2();
27568
29348
  } catch (err) {
27569
29349
  reject(err instanceof Error ? err : new Error(String(err)));
27570
29350
  }
@@ -27665,7 +29445,7 @@ var StandaloneDataGateway = class {
27665
29445
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
27666
29446
  const installed = this.installedScanRules();
27667
29447
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
27668
- id: randomUUID11(),
29448
+ id: randomUUID13(),
27669
29449
  scope: "global",
27670
29450
  target: { ruleId },
27671
29451
  action,
@@ -27818,15 +29598,15 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
27818
29598
  }
27819
29599
 
27820
29600
  // ../../packages/plugin-runtime/src/handle-session-start.ts
27821
- import { randomUUID as randomUUID12 } from "crypto";
29601
+ import { randomUUID as randomUUID14 } from "crypto";
27822
29602
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
27823
29603
 
27824
29604
  // src/history/transcripts.ts
27825
- import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
29605
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
27826
29606
  import { homedir as homedir3 } from "os";
27827
- import { join as join12 } from "path";
29607
+ import { join as join13 } from "path";
27828
29608
  function transcriptsDir(home) {
27829
- return join12(home ?? homedir3(), ".claude", "projects");
29609
+ return join13(home ?? homedir3(), ".claude", "projects");
27830
29610
  }
27831
29611
  function isRecord(value) {
27832
29612
  return typeof value === "object" && value !== null;
@@ -28074,7 +29854,7 @@ function* iterateFileContents(dir, excludeSessionId) {
28074
29854
  return;
28075
29855
  }
28076
29856
  for (const project of projects) {
28077
- const projectDir = join12(dir, project);
29857
+ const projectDir = join13(dir, project);
28078
29858
  let files;
28079
29859
  try {
28080
29860
  files = readdirSync4(projectDir).filter((name) => name.endsWith(".jsonl"));
@@ -28084,10 +29864,10 @@ function* iterateFileContents(dir, excludeSessionId) {
28084
29864
  for (const file2 of files) {
28085
29865
  if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
28086
29866
  continue;
28087
- const filePath = join12(projectDir, file2);
29867
+ const filePath = join13(projectDir, file2);
28088
29868
  let content;
28089
29869
  try {
28090
- content = readFileSync7(filePath, "utf8");
29870
+ content = readFileSync8(filePath, "utf8");
28091
29871
  } catch {
28092
29872
  continue;
28093
29873
  }
@@ -28151,7 +29931,15 @@ function buildTriageHit(text, f, otherFindings = [], filePath = "") {
28151
29931
  async function scanHistory(config2, opts = {}, onHit) {
28152
29932
  const windowDays = opts.windowDays ?? 30;
28153
29933
  if (config2.settings.historicalAccess !== "full") {
28154
- return { consented: false, scanned: 0, skipped: 0, findings: 0, bySeverity: {}, windowDays };
29934
+ return {
29935
+ consented: false,
29936
+ scanned: 0,
29937
+ skipped: 0,
29938
+ findings: 0,
29939
+ bySeverity: {},
29940
+ windowDays,
29941
+ visitedFiles: []
29942
+ };
28155
29943
  }
28156
29944
  const gateway = resolveDataGateway(config2);
28157
29945
  const runtime = createPluginRuntime(gateway, config2.settings, { dataDir: config2.dataDir });
@@ -28159,9 +29947,11 @@ async function scanHistory(config2, opts = {}, onHit) {
28159
29947
  let scanned = 0;
28160
29948
  let skipped = 0;
28161
29949
  let findings = 0;
29950
+ const visited = /* @__PURE__ */ new Set();
28162
29951
  try {
28163
29952
  const seen = await gateway.knownContentHashes();
28164
29953
  for (const message of iterateHistory(opts)) {
29954
+ visited.add(message.filePath);
28165
29955
  const hash2 = contentHashOf(message.text);
28166
29956
  if (seen.has(hash2)) {
28167
29957
  skipped++;
@@ -28197,7 +29987,85 @@ async function scanHistory(config2, opts = {}, onHit) {
28197
29987
  } finally {
28198
29988
  await runtime.close();
28199
29989
  }
28200
- return { consented: true, scanned, skipped, findings, bySeverity, windowDays };
29990
+ return {
29991
+ consented: true,
29992
+ scanned,
29993
+ skipped,
29994
+ findings,
29995
+ bySeverity,
29996
+ windowDays,
29997
+ visitedFiles: [...visited]
29998
+ };
29999
+ }
30000
+
30001
+ // src/history/tail-scrub.ts
30002
+ import { readFileSync as readFileSync10, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
30003
+
30004
+ // src/remediation/redact.ts
30005
+ import { readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
30006
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
30007
+ function platformRedactionScope(home) {
30008
+ return { artifactRoots: [transcriptsDir(home)] };
30009
+ }
30010
+ function realPathOrNull(path) {
30011
+ try {
30012
+ return realpathSync3(path);
30013
+ } catch {
30014
+ return null;
30015
+ }
30016
+ }
30017
+ function isWithinRoot(realTarget, root) {
30018
+ const realRoot = realPathOrNull(root);
30019
+ if (realRoot === null) return false;
30020
+ const rel = relative2(realRoot, realTarget);
30021
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
30022
+ }
30023
+ function resolveRedactableArtifact(filePath, scope) {
30024
+ const realTarget = realPathOrNull(resolve(filePath));
30025
+ if (realTarget === null) return null;
30026
+ return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
30027
+ }
30028
+
30029
+ // src/history/tail-scrub.ts
30030
+ var DEFAULT_MAX_SCRUB_BYTES = 32 * 1024 * 1024;
30031
+ async function scrubTranscriptTail(filePath, deps) {
30032
+ try {
30033
+ const realPath = resolveRedactableArtifact(filePath, deps.scope);
30034
+ if (realPath === null) return null;
30035
+ const statBefore = statSync5(realPath);
30036
+ if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
30037
+ const content = readFileSync10(realPath, "utf8");
30038
+ const lines = content.split("\n");
30039
+ let rewritten = 0;
30040
+ for (const [i, line] of lines.entries()) {
30041
+ if (line === "") continue;
30042
+ const result = await deps.tokenizeText(line);
30043
+ if (result.text === line) continue;
30044
+ if (result.pointers.length === 0 && result.degraded.length === 0) return null;
30045
+ lines[i] = result.text;
30046
+ rewritten += 1;
30047
+ }
30048
+ if (rewritten === 0) return { rewritten: 0 };
30049
+ const tmpPath = `${realPath}.aka-scrub.tmp`;
30050
+ try {
30051
+ writeFileSync7(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
30052
+ const statNow = statSync5(realPath);
30053
+ if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
30054
+ rmSync5(tmpPath, { force: true, recursive: true });
30055
+ return null;
30056
+ }
30057
+ renameSync6(tmpPath, realPath);
30058
+ } catch {
30059
+ try {
30060
+ rmSync5(tmpPath, { force: true, recursive: true });
30061
+ } catch {
30062
+ }
30063
+ return null;
30064
+ }
30065
+ return { rewritten };
30066
+ } catch {
30067
+ return null;
30068
+ }
28201
30069
  }
28202
30070
 
28203
30071
  // src/history/tail.ts
@@ -28205,13 +30073,13 @@ import { createHash as createHash5 } from "crypto";
28205
30073
  import {
28206
30074
  closeSync,
28207
30075
  fstatSync,
28208
- mkdirSync as mkdirSync4,
30076
+ mkdirSync as mkdirSync5,
28209
30077
  openSync,
28210
- readFileSync as readFileSync8,
30078
+ readFileSync as readFileSync11,
28211
30079
  readSync,
28212
- writeFileSync as writeFileSync5
30080
+ writeFileSync as writeFileSync8
28213
30081
  } from "fs";
28214
- import { join as join13 } from "path";
30082
+ import { join as join14 } from "path";
28215
30083
 
28216
30084
  // src/history/usage.ts
28217
30085
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
@@ -28516,6 +30384,20 @@ async function runBackfill(deps) {
28516
30384
  await deps.reconcileHistory(cfg);
28517
30385
  } catch {
28518
30386
  }
30387
+ let scrubbedFiles = 0;
30388
+ if (isVaultConsentValid(cfg.settings.vaultConsent) && summary.visitedFiles.length > 0) {
30389
+ try {
30390
+ const scrubFile = deps.scrubFile ?? buildTranscriptScrubber();
30391
+ for (const file2 of summary.visitedFiles) {
30392
+ try {
30393
+ const result = await scrubFile(file2);
30394
+ if (result !== null && result.rewritten > 0) scrubbedFiles += 1;
30395
+ } catch {
30396
+ }
30397
+ }
30398
+ } catch {
30399
+ }
30400
+ }
28519
30401
  if (triage) {
28520
30402
  const status = summary.scanned === 0 && summary.skipped === 0 ? "complete:no-history" : "complete";
28521
30403
  io.stdout(triageSentinel(count, status));
@@ -28523,7 +30405,16 @@ async function runBackfill(deps) {
28523
30405
  const heading = "\u2713 Historical scan complete";
28524
30406
  const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
28525
30407
  const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
28526
- io.stdout(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
30408
+ const lines = [heading, "", indent(scope), "", indent(result)];
30409
+ if (scrubbedFiles > 0) {
30410
+ lines.push(
30411
+ "",
30412
+ indent(
30413
+ `Rewrote secrets in ${String(scrubbedFiles)} transcript file${scrubbedFiles === 1 ? "" : "s"} to recoverable vault pointers (aka vault show).`
30414
+ )
30415
+ );
30416
+ }
30417
+ io.stdout(`${fenced(lines.join("\n"))}
28527
30418
  `);
28528
30419
  }
28529
30420
  } catch (err) {
@@ -28538,6 +30429,14 @@ async function runBackfill(deps) {
28538
30429
  }
28539
30430
  }
28540
30431
  }
30432
+ function buildTranscriptScrubber() {
30433
+ const glue = createVaultGlue();
30434
+ const scope = platformRedactionScope();
30435
+ return (filePath) => scrubTranscriptTail(filePath, {
30436
+ tokenizeText: (text) => glue.tokenizeText(text, { sighting: { location: filePath, kind: "transcript" } }),
30437
+ scope
30438
+ });
30439
+ }
28541
30440
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
28542
30441
  const triage = process.argv.includes("--triage");
28543
30442
  const startedAt = Date.now();
@@ -28560,9 +30459,9 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
28560
30459
  }
28561
30460
  });
28562
30461
  if (process.stdout.writableLength > 0) {
28563
- await new Promise((resolve) => {
30462
+ await new Promise((resolve2) => {
28564
30463
  process.stdout.write("", () => {
28565
- resolve();
30464
+ resolve2();
28566
30465
  });
28567
30466
  });
28568
30467
  }