@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.
@@ -492,11 +492,11 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync3 } from "fs";
496
- import { join as join6 } from "path";
495
+ import { existsSync as existsSync4 } from "fs";
496
+ import { join as join7 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/database.ts
499
- import { randomUUID as randomUUID8 } from "crypto";
499
+ import { randomUUID as randomUUID9 } from "crypto";
500
500
  import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
501
501
  import { join, sep } from "path";
502
502
  import { DatabaseSync } from "node:sqlite";
@@ -562,6 +562,22 @@ var SQLITE_MIGRATIONS = [
562
562
  {
563
563
  tag: "0014_drop_legacy_events_findings",
564
564
  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"
565
+ },
566
+ {
567
+ tag: "0015_busy_vengeance",
568
+ 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`);"
569
+ },
570
+ {
571
+ tag: "0016_breezy_zodiak",
572
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
573
+ },
574
+ {
575
+ tag: "0017_rainy_kat_farrell",
576
+ 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`);"
577
+ },
578
+ {
579
+ tag: "0018_serious_tana_nile",
580
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
565
581
  }
566
582
  ];
567
583
 
@@ -16218,6 +16234,7 @@ var ExceptionConditions = external_exports.object({
16218
16234
  sourceTool: external_exports.string().optional(),
16219
16235
  provider: external_exports.string().optional()
16220
16236
  }).strict();
16237
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16221
16238
  var DetectionException = external_exports.object({
16222
16239
  id: external_exports.guid(),
16223
16240
  ruleId: external_exports.string(),
@@ -16234,6 +16251,7 @@ var DetectionException = external_exports.object({
16234
16251
  keyVersion: external_exports.number().int().positive(),
16235
16252
  // maskMatch() preview of the approved value — never the raw value.
16236
16253
  maskedValue: external_exports.string(),
16254
+ capability: ExceptionCapability.default("suppress"),
16237
16255
  scope: ExceptionScope,
16238
16256
  expiresAt: external_exports.iso.datetime().nullable(),
16239
16257
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16257,6 +16275,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16257
16275
  ruleId: true,
16258
16276
  valueFingerprint: true,
16259
16277
  keyVersion: true,
16278
+ capability: true,
16260
16279
  expiresAt: true,
16261
16280
  maxUses: true,
16262
16281
  useCount: true,
@@ -17361,8 +17380,128 @@ var PatchInstalledPackRequest = external_exports.object({
17361
17380
  message: "At least one field must be provided"
17362
17381
  }).meta({ id: "PatchInstalledPackRequest" });
17363
17382
 
17383
+ // ../../packages/schema/src/zod/vault.ts
17384
+ var POINTER_FORMAT_VERSION = 2;
17385
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17386
+ var POINTER_TOKEN_PATTERN = new RegExp(
17387
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17388
+ );
17389
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17390
+ function pointerTokenScanner() {
17391
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
17392
+ }
17393
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17394
+ var ParsedPointer = external_exports.object({
17395
+ category: DetectionCategory,
17396
+ keyVersion: external_exports.number().int().positive(),
17397
+ pointerId: external_exports.string(),
17398
+ tag: external_exports.string()
17399
+ });
17400
+ var VaultEntry = external_exports.object({
17401
+ pointerId: external_exports.string(),
17402
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17403
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17404
+ // independently of the vault encryption key below.
17405
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17406
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17407
+ // The vault-key epoch this row's ciphertext was sealed under.
17408
+ keyVersion: external_exports.number().int().positive(),
17409
+ // Fixed at first mint and never updated: the same value detected later under a
17410
+ // different rule's category keeps the category it was minted with, so one
17411
+ // value always produces exactly one wire token.
17412
+ category: DetectionCategory,
17413
+ ruleId: external_exports.string(),
17414
+ // Partial-reveal preview for badges and listings. Never the raw value.
17415
+ maskedMatch: external_exports.string(),
17416
+ provider: external_exports.string().optional(),
17417
+ ciphertext: external_exports.string(),
17418
+ nonce: external_exports.string(),
17419
+ authTag: external_exports.string(),
17420
+ // How many times this value has been detected on this machine — the reuse
17421
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17422
+ occurrenceCount: external_exports.number().int().nonnegative(),
17423
+ firstSeen: external_exports.string(),
17424
+ lastSeen: external_exports.string()
17425
+ });
17426
+ var PointerDescriptor = external_exports.object({
17427
+ category: DetectionCategory,
17428
+ provider: external_exports.string().optional(),
17429
+ maskedMatch: external_exports.string(),
17430
+ occurrences: external_exports.number().int().nonnegative(),
17431
+ firstSeen: external_exports.string(),
17432
+ lastSeen: external_exports.string()
17433
+ });
17434
+ var PointerIdentity = external_exports.object({
17435
+ ruleId: external_exports.string(),
17436
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17437
+ fingerprintKeyVersion: external_exports.number().int().positive()
17438
+ });
17439
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17440
+ var VaultDerefReason = external_exports.enum([
17441
+ "display",
17442
+ "explicit-reveal",
17443
+ "view-render",
17444
+ "model-input",
17445
+ "remediation",
17446
+ "purge"
17447
+ ]);
17448
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17449
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
17450
+ function isBatchedDerefReason(reason) {
17451
+ return BATCHED_DEREF_REASONS.includes(reason);
17452
+ }
17453
+ var VaultDeref = external_exports.object({
17454
+ id: external_exports.guid(),
17455
+ pointerId: external_exports.string(),
17456
+ at: external_exports.string(),
17457
+ target: DetokenizeTarget,
17458
+ reason: VaultDerefReason,
17459
+ outcome: VaultDerefOutcome,
17460
+ // Present only on a model-target crossing that a reveal grant authorized.
17461
+ grantId: external_exports.string().optional(),
17462
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17463
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17464
+ pointerCount: external_exports.number().int().positive().default(1)
17465
+ });
17466
+ var VaultSightingKind = external_exports.enum([
17467
+ "prompt",
17468
+ "tool-input",
17469
+ "tool-output",
17470
+ "file",
17471
+ "transcript"
17472
+ ]);
17473
+ var VaultSighting = external_exports.object({
17474
+ location: external_exports.string(),
17475
+ kind: VaultSightingKind,
17476
+ firstSeen: external_exports.string(),
17477
+ lastSeen: external_exports.string()
17478
+ });
17479
+ var VaultInventoryEntry = external_exports.object({
17480
+ pointerId: external_exports.string(),
17481
+ category: DetectionCategory,
17482
+ provider: external_exports.string().optional(),
17483
+ maskedMatch: external_exports.string(),
17484
+ occurrences: external_exports.number().int().nonnegative(),
17485
+ firstSeen: external_exports.string(),
17486
+ lastSeen: external_exports.string(),
17487
+ // The active reveal-to-model grant covering this value, when one exists —
17488
+ // the inventory badges it, the row links to revocation.
17489
+ revealGrantId: external_exports.string().nullable(),
17490
+ sightings: external_exports.array(VaultSighting)
17491
+ });
17492
+ var VaultKeyCustody = external_exports.string();
17493
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17494
+ var VAULT_CONSENT_VERSION = 1;
17495
+ var VaultConsent = external_exports.object({
17496
+ acknowledgedAt: external_exports.iso.datetime(),
17497
+ version: external_exports.number().int().positive()
17498
+ });
17499
+ function isVaultConsentValid(consent) {
17500
+ return consent?.version === VAULT_CONSENT_VERSION;
17501
+ }
17502
+
17364
17503
  // ../../packages/schema/src/zod/local.ts
17365
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17504
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17366
17505
  var RunMode = external_exports.enum(["standalone"]);
17367
17506
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17368
17507
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17384,6 +17523,16 @@ var WorkspaceSettings = external_exports.object({
17384
17523
  // In-place egress extraction on the scan paths; disable to stop all Data
17385
17524
  // Shares writes.
17386
17525
  dataSharesInPlace: external_exports.boolean().default(true),
17526
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17527
+ // vault, instead of destroying them. Absent by default: this is a custody
17528
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17529
+ // Revoking stops future vaulting; it does not erase what is already stored —
17530
+ // purging the vault is the eraser.
17531
+ vaultConsent: VaultConsent.optional(),
17532
+ // Where the vault master key lives.
17533
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17534
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17535
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17387
17536
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17388
17537
  onboardedAt: external_exports.iso.datetime().optional(),
17389
17538
  // Records that the user consented to sending findings to the model API for
@@ -19798,6 +19947,9 @@ var AmbiguousExceptionIdError = class extends Error {
19798
19947
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19799
19948
  AND (expires_at IS NULL OR expires_at > :now)
19800
19949
  AND (max_uses IS NULL OR use_count < max_uses)`;
19950
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19951
+ AND conditions IS NULL
19952
+ AND ${ACTIVE_PREDICATE}`;
19801
19953
  var SqliteExceptionsRepository = class {
19802
19954
  constructor(db) {
19803
19955
  this.db = db;
@@ -19889,11 +20041,11 @@ var SqliteExceptionsRepository = class {
19889
20041
  this.db.prepare(
19890
20042
  `INSERT INTO exceptions (
19891
20043
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19892
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19893
- conditions, created_by, created_via, created_at, updated_at
20044
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20045
+ justification, conditions, created_by, created_via, created_at, updated_at
19894
20046
  ) VALUES (
19895
20047
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19896
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20048
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19897
20049
  :conditions, :createdBy, :createdVia, :now, :now
19898
20050
  )`
19899
20051
  ).run({
@@ -19903,6 +20055,7 @@ var SqliteExceptionsRepository = class {
19903
20055
  valueFingerprint: input.valueFingerprint,
19904
20056
  keyVersion: input.keyVersion,
19905
20057
  maskedValue: input.maskedValue,
20058
+ capability: input.capability ?? "suppress",
19906
20059
  scope: input.scope,
19907
20060
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19908
20061
  maxUses: input.maxUses,
@@ -19996,6 +20149,7 @@ var SqliteExceptionsRepository = class {
19996
20149
  ruleId: row.rule_id,
19997
20150
  valueFingerprint: row.value_fingerprint,
19998
20151
  keyVersion: row.key_version,
20152
+ capability: row.capability,
19999
20153
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20000
20154
  maxUses: row.max_uses,
20001
20155
  useCount: row.use_count,
@@ -20050,6 +20204,35 @@ var SqliteExceptionsRepository = class {
20050
20204
  }))
20051
20205
  );
20052
20206
  }
20207
+ /**
20208
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20209
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20210
+ * suppression uses — plus the capability: a suppression grant must never
20211
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20212
+ * revealed value re-enters the detection scan immediately afterward and the
20213
+ * suppression match there claims the use — one crossing, one use.
20214
+ *
20215
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20216
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20217
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20218
+ */
20219
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20220
+ try {
20221
+ const row = getRow(
20222
+ this.db.prepare(
20223
+ `SELECT id FROM exceptions
20224
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20225
+ AND key_version = :keyVersion
20226
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20227
+ LIMIT 1`
20228
+ ),
20229
+ { ruleId, valueFingerprint, keyVersion, now }
20230
+ );
20231
+ return Promise.resolve(row ?? null);
20232
+ } catch (err) {
20233
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20234
+ }
20235
+ }
20053
20236
  /**
20054
20237
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20055
20238
  * exhausted) whose last transition is older than the retention window.
@@ -20077,6 +20260,7 @@ function parseExceptionRow(row) {
20077
20260
  valueFingerprint: row.value_fingerprint,
20078
20261
  keyVersion: row.key_version,
20079
20262
  maskedValue: row.masked_value,
20263
+ capability: row.capability,
20080
20264
  scope: row.scope,
20081
20265
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20082
20266
  maxUses: row.max_uses,
@@ -22242,6 +22426,287 @@ var SqliteScanLedgerRepository = class {
22242
22426
  }
22243
22427
  };
22244
22428
 
22429
+ // ../../packages/persistence/src/repositories/secret-vault.ts
22430
+ import { randomUUID as randomUUID7 } from "crypto";
22431
+ var SELECT_COLUMNS = `
22432
+ pointer_id AS pointerId,
22433
+ value_fingerprint AS valueFingerprint,
22434
+ fingerprint_key_version AS fingerprintKeyVersion,
22435
+ key_version AS keyVersion,
22436
+ format_version AS formatVersion,
22437
+ category,
22438
+ rule_id AS ruleId,
22439
+ masked_match AS maskedMatch,
22440
+ provider,
22441
+ ciphertext,
22442
+ nonce,
22443
+ auth_tag AS authTag,
22444
+ occurrence_count AS occurrenceCount,
22445
+ first_seen AS firstSeen,
22446
+ last_seen AS lastSeen`;
22447
+ function toRow(raw) {
22448
+ const { provider, ...rest } = raw;
22449
+ return provider === null ? rest : { ...rest, provider };
22450
+ }
22451
+ var SqliteSecretVaultRepository = class {
22452
+ constructor(db) {
22453
+ this.db = db;
22454
+ this.insertStmt = db.prepare(
22455
+ `INSERT INTO secret_vault (
22456
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
22457
+ format_version, category, rule_id, masked_match, provider,
22458
+ ciphertext, nonce, auth_tag,
22459
+ occurrence_count, first_seen, last_seen
22460
+ ) VALUES (
22461
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
22462
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
22463
+ :ciphertext, :nonce, :authTag,
22464
+ 1, :now, :now
22465
+ )`
22466
+ );
22467
+ this.bumpStmt = db.prepare(
22468
+ `UPDATE secret_vault
22469
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
22470
+ WHERE value_fingerprint = :valueFingerprint`
22471
+ );
22472
+ this.byPointerStmt = db.prepare(
22473
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
22474
+ );
22475
+ this.byFingerprintStmt = db.prepare(
22476
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
22477
+ );
22478
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
22479
+ this.replaceCiphertextStmt = db.prepare(
22480
+ `UPDATE secret_vault
22481
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
22482
+ WHERE pointer_id = :pointerId`
22483
+ );
22484
+ this.refreshFingerprintStmt = db.prepare(
22485
+ `UPDATE secret_vault
22486
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
22487
+ WHERE pointer_id = :pointerId`
22488
+ );
22489
+ this.derefStmt = db.prepare(
22490
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
22491
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
22492
+ );
22493
+ }
22494
+ db;
22495
+ insertStmt;
22496
+ bumpStmt;
22497
+ byPointerStmt;
22498
+ byFingerprintStmt;
22499
+ listStmt;
22500
+ replaceCiphertextStmt;
22501
+ refreshFingerprintStmt;
22502
+ derefStmt;
22503
+ /**
22504
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
22505
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
22506
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
22507
+ * pointer, category and ciphertext, so the same secret always resolves to one
22508
+ * wire token. `minted` is true only when this call created the row.
22509
+ *
22510
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
22511
+ * writers cannot both decide they are minting.
22512
+ */
22513
+ upsert(input, now) {
22514
+ let minted = false;
22515
+ withTransaction(
22516
+ this.db,
22517
+ () => {
22518
+ const existing = getRow(this.byFingerprintStmt, {
22519
+ valueFingerprint: input.valueFingerprint
22520
+ });
22521
+ if (existing === void 0) {
22522
+ this.insertStmt.run(
22523
+ bindParams({
22524
+ pointerId: input.pointerId,
22525
+ valueFingerprint: input.valueFingerprint,
22526
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
22527
+ keyVersion: input.keyVersion,
22528
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
22529
+ category: input.category,
22530
+ ruleId: input.ruleId,
22531
+ maskedMatch: input.maskedMatch,
22532
+ provider: input.provider,
22533
+ ciphertext: input.ciphertext,
22534
+ nonce: input.nonce,
22535
+ authTag: input.authTag,
22536
+ now
22537
+ })
22538
+ );
22539
+ minted = true;
22540
+ return;
22541
+ }
22542
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
22543
+ },
22544
+ "IMMEDIATE"
22545
+ );
22546
+ const row = getRow(this.byFingerprintStmt, {
22547
+ valueFingerprint: input.valueFingerprint
22548
+ });
22549
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
22550
+ return { row: toRow(row), minted };
22551
+ }
22552
+ byPointerId(pointerId) {
22553
+ const raw = getRow(this.byPointerStmt, { pointerId });
22554
+ return raw === void 0 ? null : toRow(raw);
22555
+ }
22556
+ byValueFingerprint(fingerprint) {
22557
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
22558
+ return raw === void 0 ? null : toRow(raw);
22559
+ }
22560
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
22561
+ recordDeref(entry) {
22562
+ this.derefStmt.run(
22563
+ bindParams({
22564
+ id: entry.id,
22565
+ pointerId: entry.pointerId,
22566
+ at: entry.at,
22567
+ target: entry.target,
22568
+ reason: entry.reason,
22569
+ outcome: entry.outcome,
22570
+ grantId: entry.grantId,
22571
+ pointerCount: entry.pointerCount ?? 1
22572
+ })
22573
+ );
22574
+ }
22575
+ listAll() {
22576
+ return allRows(this.listStmt).map(toRow);
22577
+ }
22578
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
22579
+ replaceCiphertext(pointerId, next) {
22580
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
22581
+ }
22582
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
22583
+ refreshFingerprint(pointerId, next) {
22584
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
22585
+ }
22586
+ /**
22587
+ * Destroy every vaulted value and report how many were destroyed. The deref
22588
+ * audit is left alone on purpose — see the table note above.
22589
+ */
22590
+ purgeAll() {
22591
+ let destroyed = 0;
22592
+ withTransaction(
22593
+ this.db,
22594
+ () => {
22595
+ destroyed = this.countEntries();
22596
+ this.db.exec("DELETE FROM secret_vault");
22597
+ },
22598
+ "IMMEDIATE"
22599
+ );
22600
+ return destroyed;
22601
+ }
22602
+ /**
22603
+ * Record (or re-stamp) one place a pointer has been written. One row per
22604
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
22605
+ * on hook paths — a failure must never affect the rewrite that triggered it,
22606
+ * so callers wrap this, not the other way around.
22607
+ */
22608
+ recordSighting(entry, now) {
22609
+ this.db.prepare(
22610
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
22611
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
22612
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22613
+ ).run({
22614
+ id: randomUUID7(),
22615
+ pointerId: entry.pointerId,
22616
+ location: entry.location,
22617
+ kind: entry.kind,
22618
+ now
22619
+ });
22620
+ }
22621
+ listSightings(pointerId) {
22622
+ const rows = allRows(
22623
+ this.db.prepare(
22624
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22625
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
22626
+ ),
22627
+ { pointerId }
22628
+ );
22629
+ return rows.map((r) => ({
22630
+ location: r.location,
22631
+ kind: r.kind,
22632
+ firstSeen: new Date(r.first_seen).toISOString(),
22633
+ lastSeen: new Date(r.last_seen).toISOString()
22634
+ }));
22635
+ }
22636
+ /**
22637
+ * The dashboard inventory: every vaulted value's descriptor data joined with
22638
+ * its sightings and the active reveal-to-model grant when one exists.
22639
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
22640
+ * columns are selected.
22641
+ */
22642
+ listInventory(now = Date.now()) {
22643
+ const rows = allRows(
22644
+ this.db.prepare(
22645
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22646
+ v.occurrence_count, v.first_seen, v.last_seen,
22647
+ (SELECT e.id FROM exceptions e
22648
+ WHERE e.rule_id = v.rule_id
22649
+ AND e.value_fingerprint = v.value_fingerprint
22650
+ AND e.key_version = v.fingerprint_key_version
22651
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22652
+ LIMIT 1) AS grant_id
22653
+ FROM secret_vault v
22654
+ ORDER BY v.last_seen DESC`
22655
+ ),
22656
+ { now }
22657
+ );
22658
+ return rows.map((r) => ({
22659
+ pointerId: r.pointer_id,
22660
+ category: r.category,
22661
+ ...r.provider === null ? {} : { provider: r.provider },
22662
+ maskedMatch: r.masked_match,
22663
+ occurrences: r.occurrence_count,
22664
+ firstSeen: new Date(r.first_seen).toISOString(),
22665
+ lastSeen: new Date(r.last_seen).toISOString(),
22666
+ revealGrantId: r.grant_id,
22667
+ sightings: this.listSightings(r.pointer_id)
22668
+ }));
22669
+ }
22670
+ /**
22671
+ * The de-reference trail, newest first. By default the batched, high-volume
22672
+ * reasons (display, view-render) are hidden and counted instead — the rows
22673
+ * that matter as a signal are the model crossings, and burying them under
22674
+ * render noise would defeat the audit's purpose.
22675
+ */
22676
+ listDerefs(opts) {
22677
+ const limit = opts?.limit ?? 200;
22678
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
22679
+ const rows = allRows(
22680
+ this.db.prepare(
22681
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22682
+ FROM secret_vault_deref ${where}
22683
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
22684
+ ),
22685
+ { limit }
22686
+ );
22687
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
22688
+ this.db,
22689
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22690
+ );
22691
+ return {
22692
+ rows: rows.map((r) => ({
22693
+ id: r.id,
22694
+ pointerId: r.pointer_id,
22695
+ at: new Date(r.at).toISOString(),
22696
+ target: r.target,
22697
+ reason: r.reason,
22698
+ outcome: r.outcome,
22699
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
22700
+ pointerCount: r.pointer_count
22701
+ })),
22702
+ hiddenBatched
22703
+ };
22704
+ }
22705
+ countEntries() {
22706
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22707
+ }
22708
+ };
22709
+
22245
22710
  // ../../packages/persistence/src/repositories/security.ts
22246
22711
  var DAY_MS4 = 864e5;
22247
22712
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22587,7 +23052,7 @@ var SqliteSecurityRepository = class {
22587
23052
  };
22588
23053
 
22589
23054
  // ../../packages/persistence/src/repositories/shares.ts
22590
- import { randomUUID as randomUUID7 } from "crypto";
23055
+ import { randomUUID as randomUUID8 } from "crypto";
22591
23056
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22592
23057
  var IN_CHUNK = 500;
22593
23058
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22843,7 +23308,7 @@ var SqliteSharesRepository = class {
22843
23308
  (id, destination_id, host, decision, created_at, updated_at)
22844
23309
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22845
23310
  ).run({
22846
- id: randomUUID7(),
23311
+ id: randomUUID8(),
22847
23312
  destinationId,
22848
23313
  host: dest.host,
22849
23314
  decision,
@@ -22992,7 +23457,7 @@ var SqliteSharesRepository = class {
22992
23457
  let destinationId = destIds.get(hit.host);
22993
23458
  if (destinationId === void 0) {
22994
23459
  destStmt.run({
22995
- id: randomUUID7(),
23460
+ id: randomUUID8(),
22996
23461
  kind: hit.kind,
22997
23462
  name: hit.name,
22998
23463
  host: hit.host,
@@ -23008,7 +23473,7 @@ var SqliteSharesRepository = class {
23008
23473
  let endpointId = endpointIds.get(endpointKey);
23009
23474
  if (endpointId === void 0) {
23010
23475
  endpointStmt.run({
23011
- id: randomUUID7(),
23476
+ id: randomUUID8(),
23012
23477
  destinationId,
23013
23478
  method: hit.method,
23014
23479
  transport: hit.transport,
@@ -23021,7 +23486,7 @@ var SqliteSharesRepository = class {
23021
23486
  endpointIds.set(endpointKey, endpointId);
23022
23487
  }
23023
23488
  siteStmt.run({
23024
- id: randomUUID7(),
23489
+ id: randomUUID8(),
23025
23490
  endpointId,
23026
23491
  project: input.project,
23027
23492
  projectKey: input.projectKey,
@@ -23389,11 +23854,22 @@ function purgeSampleData(db) {
23389
23854
  function linkHost(input, hostId) {
23390
23855
  return hostId ? { ...input, hostId } : input;
23391
23856
  }
23857
+ function closeQuietly(db) {
23858
+ try {
23859
+ db.close();
23860
+ } catch {
23861
+ }
23862
+ }
23392
23863
  function openWithPragmas(file2) {
23393
23864
  const db = new DatabaseSync(file2);
23394
- db.exec("PRAGMA journal_mode = WAL");
23395
- db.exec("PRAGMA busy_timeout = 2000");
23396
- db.exec("PRAGMA foreign_keys = ON");
23865
+ try {
23866
+ db.exec("PRAGMA journal_mode = WAL");
23867
+ db.exec("PRAGMA busy_timeout = 2000");
23868
+ db.exec("PRAGMA foreign_keys = ON");
23869
+ } catch (err) {
23870
+ closeQuietly(db);
23871
+ throw err;
23872
+ }
23397
23873
  return db;
23398
23874
  }
23399
23875
  function backupLegacyStore(file2) {
@@ -23405,43 +23881,82 @@ function backupLegacyStore(file2) {
23405
23881
  }
23406
23882
  return backup;
23407
23883
  }
23884
+ function openAndInitialize(file2) {
23885
+ let db = openWithPragmas(file2);
23886
+ try {
23887
+ if (isForeignSqliteLineage(db)) {
23888
+ db.close();
23889
+ const backup = backupLegacyStore(file2);
23890
+ db = openWithPragmas(file2);
23891
+ akaWarn(
23892
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23893
+ );
23894
+ }
23895
+ applyMigrations(db, file2);
23896
+ tightenPerms(file2);
23897
+ const policies = new SqlitePoliciesRepository(db);
23898
+ const installedPacks = new SqliteInstalledPacksRepository(db);
23899
+ const repositories = {
23900
+ events: new SqliteEventsRepository(db),
23901
+ findings: new SqliteFindingsRepository(db),
23902
+ policies,
23903
+ installedPacks,
23904
+ scanLedger: new SqliteScanLedgerRepository(db),
23905
+ secretVault: new SqliteSecretVaultRepository(db),
23906
+ exceptions: new SqliteExceptionsRepository(db),
23907
+ resolutions: new SqliteResolutionsRepository(db),
23908
+ ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
23909
+ security: new SqliteSecurityRepository(db),
23910
+ detections: new SqliteDetectionsRepository(db),
23911
+ shares: new SqliteSharesRepository(db),
23912
+ policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
23913
+ inventory: new SqliteInventoryRepository(db),
23914
+ inventoryAssets: new SqliteInventoryAssetsRepository(db),
23915
+ projectFiles: new SqliteProjectFilesRepository(db),
23916
+ activity: new SqliteActivityRepository(db),
23917
+ sourceProject: new SqliteSourceProjectRepository(db),
23918
+ auditEvents: new SqliteAuditEventsRepository(db),
23919
+ classifiedData: new SqliteClassifiedDataRepository(db),
23920
+ inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
23921
+ inspectionFindings: new SqliteInspectionFindingsRepository(db),
23922
+ configInventory: new SqliteConfigInventoryRepository(db)
23923
+ };
23924
+ policies.seedDefaults();
23925
+ return { db, ...repositories };
23926
+ } catch (err) {
23927
+ closeQuietly(db);
23928
+ throw err;
23929
+ }
23930
+ }
23408
23931
  function openLocalDatabase(dir) {
23409
23932
  ensureDataDirSync(dir);
23410
23933
  const file2 = join(dir, DB_FILENAME);
23411
- let db = openWithPragmas(file2);
23412
- if (isForeignSqliteLineage(db)) {
23413
- db.close();
23414
- const backup = backupLegacyStore(file2);
23415
- db = openWithPragmas(file2);
23416
- akaWarn(
23417
- `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23418
- );
23419
- }
23420
- applyMigrations(db, file2);
23421
- tightenPerms(file2);
23422
- const events = new SqliteEventsRepository(db);
23423
- const findings = new SqliteFindingsRepository(db);
23424
- const policies = new SqlitePoliciesRepository(db);
23425
- const installedPacks = new SqliteInstalledPacksRepository(db);
23426
- const scanLedger = new SqliteScanLedgerRepository(db);
23427
- const exceptions = new SqliteExceptionsRepository(db);
23428
- const resolutions = new SqliteResolutionsRepository(db);
23429
- const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
23430
- const security = new SqliteSecurityRepository(db);
23431
- const detections = new SqliteDetectionsRepository(db);
23432
- const shares = new SqliteSharesRepository(db);
23433
- const policyCatalog = new SqlitePolicyCatalogRepository(installedPacks);
23434
- const inventory = new SqliteInventoryRepository(db);
23435
- const inventoryAssets = new SqliteInventoryAssetsRepository(db);
23436
- const projectFiles = new SqliteProjectFilesRepository(db);
23437
- const activity = new SqliteActivityRepository(db);
23438
- const sourceProject = new SqliteSourceProjectRepository(db);
23439
- const auditEvents = new SqliteAuditEventsRepository(db);
23440
- const classifiedData = new SqliteClassifiedDataRepository(db);
23441
- const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
23442
- const inspectionFindings = new SqliteInspectionFindingsRepository(db);
23443
- const configInventory = new SqliteConfigInventoryRepository(db);
23444
- policies.seedDefaults();
23934
+ const {
23935
+ db,
23936
+ events,
23937
+ findings,
23938
+ policies,
23939
+ installedPacks,
23940
+ scanLedger,
23941
+ secretVault,
23942
+ exceptions,
23943
+ resolutions,
23944
+ ruleProbeCache,
23945
+ security,
23946
+ detections,
23947
+ shares,
23948
+ policyCatalog,
23949
+ inventory,
23950
+ inventoryAssets,
23951
+ projectFiles,
23952
+ activity,
23953
+ sourceProject,
23954
+ auditEvents,
23955
+ classifiedData,
23956
+ inspectionDefinitions,
23957
+ inspectionFindings,
23958
+ configInventory
23959
+ } = openAndInitialize(file2);
23445
23960
  function recordCapture(event, detected) {
23446
23961
  failOpenTransaction(db, () => {
23447
23962
  const sessionId = event.metadata?.sessionId;
@@ -23532,7 +24047,7 @@ function openLocalDatabase(dir) {
23532
24047
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23533
24048
  if (!definitionId) continue;
23534
24049
  inspectionFindings.insertFinding({
23535
- id: randomUUID8(),
24050
+ id: randomUUID9(),
23536
24051
  auditEventId: record2.scanEvent.id,
23537
24052
  inspectionDefinitionId: definitionId,
23538
24053
  span: finding.span,
@@ -23609,6 +24124,7 @@ function openLocalDatabase(dir) {
23609
24124
  policies,
23610
24125
  installedPacks,
23611
24126
  scanLedger,
24127
+ secretVault,
23612
24128
  exceptions,
23613
24129
  resolutions,
23614
24130
  ruleProbeCache,
@@ -23641,6 +24157,26 @@ function openLocalDatabase(dir) {
23641
24157
  };
23642
24158
  }
23643
24159
 
24160
+ // ../../packages/persistence/src/exception-policy.ts
24161
+ var UserGrantPolicyProvider = class {
24162
+ #exceptions;
24163
+ constructor(exceptions) {
24164
+ this.#exceptions = exceptions;
24165
+ }
24166
+ async decideReveal(identity) {
24167
+ try {
24168
+ const grant = await this.#exceptions.activeRevealGrant(
24169
+ identity.ruleId,
24170
+ identity.valueFingerprint,
24171
+ identity.fingerprintKeyVersion
24172
+ );
24173
+ return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
24174
+ } catch {
24175
+ return { allow: false };
24176
+ }
24177
+ }
24178
+ };
24179
+
23644
24180
  // ../../packages/persistence/src/finding-key.ts
23645
24181
  import { createHash as createHash3 } from "crypto";
23646
24182
  function normalizeFilePath(filePath) {
@@ -23653,8 +24189,9 @@ function computeFindingKey(input) {
23653
24189
 
23654
24190
  // ../../packages/persistence/src/fingerprint.ts
23655
24191
  import { createHmac, randomBytes } from "crypto";
23656
- import { readFileSync } from "fs";
24192
+ import { existsSync as existsSync2, readFileSync } from "fs";
23657
24193
  import { join as join2 } from "path";
24194
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
23658
24195
  var KEY_FILENAME = "exception.key";
23659
24196
  var KEY_MATERIAL_BYTES = 32;
23660
24197
  function keyFilePath(dataDir2) {
@@ -23678,6 +24215,50 @@ function parseKeyFile(raw) {
23678
24215
  }
23679
24216
  return { version: version2, material: bytes };
23680
24217
  }
24218
+ var KEY_VERSION_COLUMNS = {
24219
+ exceptions: "key_version",
24220
+ blocked_detections: "key_version",
24221
+ secret_vault: "fingerprint_key_version"
24222
+ };
24223
+ var SQLITE_ERROR = 1;
24224
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
24225
+ var FloorUnreadableError = class extends Error {
24226
+ code = "floor-unreadable";
24227
+ constructor(cause) {
24228
+ super(
24229
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
24230
+ { cause }
24231
+ );
24232
+ this.name = "FloorUnreadableError";
24233
+ }
24234
+ };
24235
+ function storedKeyVersionFloor(dataDir2) {
24236
+ const file2 = join2(dataDir2, DB_FILENAME);
24237
+ if (!existsSync2(file2)) return 0;
24238
+ let db;
24239
+ try {
24240
+ db = new DatabaseSync2(file2, { readOnly: true });
24241
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
24242
+ let floor = 0;
24243
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
24244
+ try {
24245
+ const row = getRow(
24246
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
24247
+ );
24248
+ floor = Math.max(floor, row?.v ?? 0);
24249
+ } catch (err) {
24250
+ if (err.errcode !== SQLITE_ERROR) {
24251
+ throw new FloorUnreadableError(err);
24252
+ }
24253
+ }
24254
+ }
24255
+ return floor;
24256
+ } catch (err) {
24257
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
24258
+ } finally {
24259
+ db?.close();
24260
+ }
24261
+ }
23681
24262
  function writeKeyFile(dataDir2, key) {
23682
24263
  ensureDataDirSync(dataDir2);
23683
24264
  const file2 = keyFilePath(dataDir2);
@@ -23702,7 +24283,10 @@ function loadOrCreateFingerprintKey(dataDir2) {
23702
24283
  tightenFile(keyFilePath(dataDir2));
23703
24284
  return existing;
23704
24285
  }
23705
- return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
24286
+ return writeKeyFile(dataDir2, {
24287
+ version: storedKeyVersionFloor(dataDir2) + 1,
24288
+ material: randomBytes(KEY_MATERIAL_BYTES)
24289
+ });
23706
24290
  }
23707
24291
  function fingerprintValue(key, raw) {
23708
24292
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
@@ -23725,6 +24309,9 @@ function dataDir(base = defaultDataDir()) {
23725
24309
  function dbPath(base = defaultDataDir()) {
23726
24310
  return join3(dataDir(base), "aka.db");
23727
24311
  }
24312
+ function keysDir(base = defaultDataDir()) {
24313
+ return join3(base, "keys");
24314
+ }
23728
24315
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23729
24316
  ensureDataDirSync(dir);
23730
24317
  }
@@ -23766,44 +24353,905 @@ function readJson(file2) {
23766
24353
  return parseJsonObject(text) ?? null;
23767
24354
  }
23768
24355
 
23769
- // ../../packages/persistence/src/warn-era-cap.ts
23770
- import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
23771
- import { join as join5 } from "path";
23772
- var MARKER = "warn-era-capped";
23773
- function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23774
- if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23775
- const marker = join5(dataDir2, MARKER);
23776
- if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
23777
- const capped = db.policies.capCategoryActions();
23778
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
23779
- `, { mode: DATA_FILE_MODE });
23780
- return { capped };
24356
+ // ../../packages/persistence/src/vault/crypto.ts
24357
+ import {
24358
+ createCipheriv,
24359
+ createDecipheriv,
24360
+ createHmac as createHmac2,
24361
+ hkdfSync,
24362
+ timingSafeEqual
24363
+ } from "crypto";
24364
+ var POINTER_ID_BYTES = 16;
24365
+ var NONCE_BYTES = 12;
24366
+ var TAG_BYTES = 10;
24367
+ var SUBKEY_BYTES = 32;
24368
+ var HKDF_INFO_ENC = "aka:vault:enc:v1";
24369
+ var HKDF_INFO_SIGN = "aka:vault:sign:v1";
24370
+ var HKDF_SALT = "aka:vault:v1";
24371
+ var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
24372
+ function base32Encode(bytes) {
24373
+ let out = "";
24374
+ let buffer = 0;
24375
+ let bits = 0;
24376
+ for (const byte of bytes) {
24377
+ buffer = buffer << 8 | byte;
24378
+ bits += 8;
24379
+ while (bits >= 5) {
24380
+ out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
24381
+ bits -= 5;
24382
+ }
24383
+ }
24384
+ if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
24385
+ return out;
23781
24386
  }
23782
-
23783
- // ../../packages/plugin-sdk/src/provider-env.ts
23784
- var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
23785
- var booleanish = external_exports.string().optional().transform((v) => {
23786
- if (v === void 0) return void 0;
23787
- const t = v.trim().toLowerCase();
23788
- if (t === "" || t === "false" || t === "0") return false;
23789
- return true;
23790
- }).catch(void 0);
23791
- var optionalBaseUrl = external_exports.preprocess((v) => {
23792
- if (typeof v === "string" && v.trim() === "") return void 0;
23793
- return v;
23794
- }, external_exports.string().optional()).catch(void 0);
23795
- var providerEnvShape = {
23796
- CLAUDE_CODE_USE_BEDROCK: booleanish,
23797
- CLAUDE_CODE_USE_VERTEX: booleanish,
23798
- ANTHROPIC_BASE_URL: optionalBaseUrl
23799
- };
23800
- var ProviderEnvSchema = external_exports.object(providerEnvShape);
23801
-
23802
- // ../../packages/plugin-sdk/src/provider.ts
23803
- function hostOf(url2) {
24387
+ function base32Decode(text) {
24388
+ const out = [];
24389
+ let buffer = 0;
24390
+ let bits = 0;
24391
+ for (const char of text) {
24392
+ const value = B32_ALPHABET.indexOf(char);
24393
+ if (value < 0) throw new Error("base32: character outside the alphabet");
24394
+ buffer = buffer << 5 | value;
24395
+ bits += 5;
24396
+ if (bits >= 8) {
24397
+ out.push(buffer >>> bits - 8 & 255);
24398
+ bits -= 8;
24399
+ }
24400
+ }
24401
+ return Buffer.from(out);
24402
+ }
24403
+ function encodeKeyVersion(version2) {
24404
+ if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
24405
+ throw new Error("vault: key version out of range");
24406
+ }
24407
+ const bytes = [];
24408
+ let remaining = version2;
24409
+ while (remaining > 0) {
24410
+ bytes.unshift(remaining & 255);
24411
+ remaining = Math.floor(remaining / 256);
24412
+ }
24413
+ return base32Encode(Uint8Array.from(bytes));
24414
+ }
24415
+ function decodeKeyVersion(encoded) {
24416
+ const bytes = base32Decode(encoded);
24417
+ if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
24418
+ let version2 = 0;
24419
+ for (const byte of bytes) version2 = version2 * 256 + byte;
24420
+ if (version2 < 1) throw new Error("vault: bad key version");
24421
+ return version2;
24422
+ }
24423
+ function deriveSubkeys(master) {
24424
+ const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
24425
+ return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
24426
+ }
24427
+ function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
24428
+ if (pointerId.length !== POINTER_ID_BYTES) {
24429
+ throw new Error("vault: pointer id must be 16 bytes");
24430
+ }
24431
+ const head = Buffer.alloc(6);
24432
+ head.writeUInt16BE(formatVersion, 0);
24433
+ head.writeUInt32BE(keyVersion, 2);
24434
+ return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
24435
+ }
24436
+ function seal(encKey, plaintext, aad, nonce) {
24437
+ const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
24438
+ cipher.setAAD(aad);
24439
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24440
+ return { ciphertext, nonce, authTag: cipher.getAuthTag() };
24441
+ }
24442
+ function open(encKey, sealed, aad) {
23804
24443
  try {
23805
- const host = new URL(url2).host;
23806
- if (host !== "") return host;
24444
+ const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
24445
+ decipher.setAAD(aad);
24446
+ decipher.setAuthTag(sealed.authTag);
24447
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
24448
+ } catch {
24449
+ return null;
24450
+ }
24451
+ }
24452
+ function signPointer(signKey, keyVersion, pointerId, category) {
24453
+ return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
24454
+ }
24455
+ function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
24456
+ if (tag.length !== TAG_BYTES) return false;
24457
+ const expected = signPointer(signKey, keyVersion, pointerId, category);
24458
+ return timingSafeEqual(expected, Buffer.from(tag));
24459
+ }
24460
+ function formatPointer(category, keyVersion, pointerId, tag) {
24461
+ return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
24462
+ }
24463
+
24464
+ // ../../packages/persistence/src/vault/key-provider.ts
24465
+ import { execFileSync } from "child_process";
24466
+ import { randomBytes as randomBytes2 } from "crypto";
24467
+ import {
24468
+ chmodSync as chmodSync2,
24469
+ mkdirSync as mkdirSync2,
24470
+ readFileSync as readFileSync3,
24471
+ renameSync as renameSync4,
24472
+ rmSync as rmSync3,
24473
+ statSync,
24474
+ writeFileSync as writeFileSync2
24475
+ } from "fs";
24476
+ import { join as join5 } from "path";
24477
+ var VaultKeyEpochMissingError = class extends Error {
24478
+ version;
24479
+ constructor(version2) {
24480
+ super(`vault: key epoch ${String(version2)} is not present in the keyring`);
24481
+ this.name = "VaultKeyEpochMissingError";
24482
+ this.version = version2;
24483
+ }
24484
+ };
24485
+ var VAULT_KEY_FILENAME = "vault.key";
24486
+ var KEY_MATERIAL_BYTES2 = 32;
24487
+ var KEYCHAIN_SERVICE = "aka-vault";
24488
+ var KEYCHAIN_ACCOUNT = "keyring";
24489
+ function parseKeyring(raw) {
24490
+ const parsed = JSON.parse(raw);
24491
+ if (typeof parsed !== "object" || parsed === null) {
24492
+ throw new Error("vault key file is corrupt: not a JSON object");
24493
+ }
24494
+ const { current, keys } = parsed;
24495
+ if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
24496
+ throw new Error("vault key file is corrupt: bad current version");
24497
+ }
24498
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
24499
+ throw new Error("vault key file is corrupt: bad keys map");
24500
+ }
24501
+ const map2 = /* @__PURE__ */ new Map();
24502
+ for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
24503
+ const version2 = Number(rawVersion);
24504
+ if (!Number.isInteger(version2) || version2 < 1) {
24505
+ throw new Error("vault key file is corrupt: bad key version");
24506
+ }
24507
+ if (typeof rawMaterial !== "string") {
24508
+ throw new Error("vault key file is corrupt: bad key material");
24509
+ }
24510
+ const bytes = Buffer.from(rawMaterial, "base64");
24511
+ if (bytes.length !== KEY_MATERIAL_BYTES2) {
24512
+ throw new Error("vault key file is corrupt: bad key material length");
24513
+ }
24514
+ map2.set(version2, bytes);
24515
+ }
24516
+ if (!map2.has(current)) {
24517
+ throw new Error("vault key file is corrupt: current version has no material");
24518
+ }
24519
+ return { current, keys: map2 };
24520
+ }
24521
+ function serializeKeyring(keyring) {
24522
+ const keys = {};
24523
+ for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
24524
+ const material = keyring.keys.get(version2);
24525
+ if (material) keys[String(version2)] = material.toString("base64");
24526
+ }
24527
+ return JSON.stringify({ current: keyring.current, keys });
24528
+ }
24529
+ function mintKeyring() {
24530
+ return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
24531
+ }
24532
+ function withNextEpoch(keyring) {
24533
+ const next = Math.max(...keyring.keys.keys()) + 1;
24534
+ const keys = new Map(keyring.keys);
24535
+ keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
24536
+ return { current: next, keys };
24537
+ }
24538
+ function currentOf(keyring) {
24539
+ const material = keyring.keys.get(keyring.current);
24540
+ if (!material) throw new VaultKeyEpochMissingError(keyring.current);
24541
+ return { material, version: keyring.current };
24542
+ }
24543
+ function epochOf(keyring, version2) {
24544
+ const material = keyring.keys.get(version2);
24545
+ if (!material) throw new VaultKeyEpochMissingError(version2);
24546
+ return { material, version: version2 };
24547
+ }
24548
+ function asAsync(work) {
24549
+ try {
24550
+ return Promise.resolve(work());
24551
+ } catch (err) {
24552
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
24553
+ }
24554
+ }
24555
+ function asError(err) {
24556
+ return err instanceof Error ? err : new Error(String(err));
24557
+ }
24558
+ var ROTATION_LOCK_STALE_MS = 6e4;
24559
+ var LOCK_OWNER_FILE = "owner";
24560
+ var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
24561
+ function claimRotationLock(lock, owner) {
24562
+ try {
24563
+ mkdirSync2(lock);
24564
+ } catch (err) {
24565
+ if (err.code === "EEXIST") return false;
24566
+ throw asError(err);
24567
+ }
24568
+ try {
24569
+ writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
24570
+ `, { mode: DATA_FILE_MODE });
24571
+ return true;
24572
+ } catch (err) {
24573
+ rmSync3(lock, { recursive: true, force: true });
24574
+ throw asError(err);
24575
+ }
24576
+ }
24577
+ function acquireRotationLock(keysDir2) {
24578
+ const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
24579
+ const owner = randomBytes2(16).toString("hex");
24580
+ if (claimRotationLock(lock, owner)) return { lock, owner };
24581
+ let held;
24582
+ try {
24583
+ held = statSync(lock);
24584
+ } catch {
24585
+ throw new Error(ROTATION_IN_PROGRESS);
24586
+ }
24587
+ if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
24588
+ const aside = `${lock}.stale.${owner}`;
24589
+ try {
24590
+ const now = statSync(lock);
24591
+ if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
24592
+ throw new Error(ROTATION_IN_PROGRESS);
24593
+ }
24594
+ renameSync4(lock, aside);
24595
+ } catch (err) {
24596
+ if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
24597
+ throw new Error(ROTATION_IN_PROGRESS, { cause: err });
24598
+ }
24599
+ rmSync3(aside, { recursive: true, force: true });
24600
+ if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
24601
+ return { lock, owner };
24602
+ }
24603
+ function releaseRotationLock(lease) {
24604
+ try {
24605
+ if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
24606
+ } catch {
24607
+ return;
24608
+ }
24609
+ rmSync3(lease.lock, { recursive: true, force: true });
24610
+ }
24611
+ function withRotationLock(keysDir2, work) {
24612
+ ensureDataDirSync(keysDir2);
24613
+ const lease = acquireRotationLock(keysDir2);
24614
+ try {
24615
+ return work();
24616
+ } finally {
24617
+ releaseRotationLock(lease);
24618
+ }
24619
+ }
24620
+ var FileKeyProvider = class {
24621
+ #keysDir;
24622
+ constructor(keysDir2) {
24623
+ this.#keysDir = keysDir2;
24624
+ }
24625
+ get filePath() {
24626
+ return join5(this.#keysDir, VAULT_KEY_FILENAME);
24627
+ }
24628
+ loadOrCreate() {
24629
+ return asAsync(() => {
24630
+ const existing = this.#read();
24631
+ if (!existing) return currentOf(this.#createExclusive());
24632
+ tightenFileMode(this.filePath);
24633
+ return currentOf(existing);
24634
+ });
24635
+ }
24636
+ rotate() {
24637
+ return asAsync(
24638
+ () => withRotationLock(this.#keysDir, () => {
24639
+ const existing = this.#read();
24640
+ if (!existing) return currentOf(this.#createExclusive());
24641
+ return currentOf(this.#write(withNextEpoch(existing)));
24642
+ })
24643
+ );
24644
+ }
24645
+ materialFor(version2) {
24646
+ return asAsync(() => {
24647
+ const existing = this.#read();
24648
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24649
+ return epochOf(existing, version2);
24650
+ });
24651
+ }
24652
+ /** The keyring, or null when the file is ABSENT. A corrupt file throws. */
24653
+ #read() {
24654
+ let raw;
24655
+ try {
24656
+ raw = readFileSync3(this.filePath, "utf8");
24657
+ } catch (err) {
24658
+ if (err.code === "ENOENT") return null;
24659
+ throw err instanceof Error ? err : new Error(String(err));
24660
+ }
24661
+ return parseKeyring(raw);
24662
+ }
24663
+ /**
24664
+ * First mint: the keyring is created at its FINAL path with a
24665
+ * creation-exclusive write, so two processes racing a fresh machine cannot
24666
+ * each mint a different epoch 1 — with tmp + rename the loser's replace
24667
+ * would orphan everything the winner had already sealed. On EEXIST the
24668
+ * loser re-reads and adopts the winner's keyring; it minted nothing.
24669
+ * Atomic replace is unnecessary here: nothing can be mid-read of a file
24670
+ * that did not exist, and a torn exclusive write parses as corrupt on the
24671
+ * next read and fails secure rather than being re-minted over.
24672
+ */
24673
+ #createExclusive() {
24674
+ ensureDataDirSync(this.#keysDir);
24675
+ const keyring = mintKeyring();
24676
+ try {
24677
+ writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
24678
+ `, {
24679
+ flag: "wx",
24680
+ mode: DATA_FILE_MODE
24681
+ });
24682
+ } catch (err) {
24683
+ if (err.code !== "EEXIST") throw asError(err);
24684
+ const winner = this.#read();
24685
+ if (!winner) {
24686
+ throw new Error("vault: key file vanished during first mint", { cause: err });
24687
+ }
24688
+ return winner;
24689
+ }
24690
+ tightenFileMode(this.filePath);
24691
+ return keyring;
24692
+ }
24693
+ /**
24694
+ * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
24695
+ * Used only for rotation, under the rotation lock — first creation goes
24696
+ * through the creation-exclusive path instead.
24697
+ */
24698
+ #write(keyring) {
24699
+ ensureDataDirSync(this.#keysDir);
24700
+ const file2 = this.filePath;
24701
+ const tmp = `${file2}.tmp`;
24702
+ writeFileSync2(tmp, `${serializeKeyring(keyring)}
24703
+ `, { mode: DATA_FILE_MODE });
24704
+ renameSync4(tmp, file2);
24705
+ tightenFileMode(file2);
24706
+ return keyring;
24707
+ }
24708
+ };
24709
+ function tightenFileMode(file2) {
24710
+ try {
24711
+ chmodSync2(file2, DATA_FILE_MODE);
24712
+ } catch {
24713
+ }
24714
+ }
24715
+ var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
24716
+ encoding: "utf8",
24717
+ stdio: ["ignore", "pipe", "ignore"]
24718
+ });
24719
+ var SECURITY_ITEM_NOT_FOUND = 44;
24720
+ var KeychainKeyProvider = class {
24721
+ #keysDir;
24722
+ #exec;
24723
+ constructor(keysDir2, exec = runSecurity) {
24724
+ if (exec === runSecurity && process.platform !== "darwin") {
24725
+ throw new Error(
24726
+ `keychain custody is not available on this platform (${process.platform}); use file custody`
24727
+ );
24728
+ }
24729
+ this.#keysDir = keysDir2;
24730
+ this.#exec = exec;
24731
+ }
24732
+ /** Where a fallback file provider for the same vault would keep its keyring. */
24733
+ get keysDir() {
24734
+ return this.#keysDir;
24735
+ }
24736
+ loadOrCreate() {
24737
+ return asAsync(() => {
24738
+ const existing = this.#read();
24739
+ if (existing) return currentOf(existing);
24740
+ return currentOf(this.#create(mintKeyring()));
24741
+ });
24742
+ }
24743
+ rotate() {
24744
+ return asAsync(
24745
+ () => withRotationLock(this.#keysDir, () => {
24746
+ const existing = this.#read();
24747
+ if (!existing) return currentOf(this.#create(mintKeyring()));
24748
+ return currentOf(this.#replace(withNextEpoch(existing)));
24749
+ })
24750
+ );
24751
+ }
24752
+ materialFor(version2) {
24753
+ return asAsync(() => {
24754
+ const existing = this.#read();
24755
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24756
+ return epochOf(existing, version2);
24757
+ });
24758
+ }
24759
+ /** The keyring, or null when no item exists yet. A corrupt item throws. */
24760
+ #read() {
24761
+ let raw;
24762
+ try {
24763
+ raw = this.#exec([
24764
+ "find-generic-password",
24765
+ "-s",
24766
+ KEYCHAIN_SERVICE,
24767
+ "-a",
24768
+ KEYCHAIN_ACCOUNT,
24769
+ "-w"
24770
+ ]);
24771
+ } catch (err) {
24772
+ if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
24773
+ throw new Error(
24774
+ `vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
24775
+ { cause: err }
24776
+ );
24777
+ }
24778
+ const body = raw.trim();
24779
+ if (body.length === 0) return null;
24780
+ return parseKeyring(body);
24781
+ }
24782
+ /**
24783
+ * First mint: a plain `add-generic-password` (no `-U`) fails when an item
24784
+ * already exists, so a concurrent first mint cannot overwrite the winner's
24785
+ * keyring — the loser re-reads and adopts it instead.
24786
+ */
24787
+ #create(keyring) {
24788
+ const args = [
24789
+ "add-generic-password",
24790
+ "-s",
24791
+ KEYCHAIN_SERVICE,
24792
+ "-a",
24793
+ KEYCHAIN_ACCOUNT,
24794
+ "-w",
24795
+ serializeKeyring(keyring)
24796
+ ];
24797
+ try {
24798
+ this.#exec(args);
24799
+ } catch (err) {
24800
+ const winner = this.#read();
24801
+ if (winner) return winner;
24802
+ throw asError(err);
24803
+ }
24804
+ return keyring;
24805
+ }
24806
+ // `-U` updates the item in place, deliberately replacing the stored map with
24807
+ // one that contains it — used only for rotation, under the rotation lock.
24808
+ #replace(keyring) {
24809
+ this.#exec([
24810
+ "add-generic-password",
24811
+ "-U",
24812
+ "-s",
24813
+ KEYCHAIN_SERVICE,
24814
+ "-a",
24815
+ KEYCHAIN_ACCOUNT,
24816
+ "-w",
24817
+ serializeKeyring(keyring)
24818
+ ]);
24819
+ return keyring;
24820
+ }
24821
+ };
24822
+ function createKeyProvider(custody, keysDir2) {
24823
+ if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
24824
+ return new FileKeyProvider(keysDir2);
24825
+ }
24826
+
24827
+ // ../../packages/persistence/src/vault/vault.ts
24828
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
24829
+ var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
24830
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
24831
+ var VAULT_PURGE_POINTER_ID = "*";
24832
+ function parsePointer(token) {
24833
+ if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
24834
+ const body = token.slice("[[aka:".length, -"]]".length);
24835
+ const colon = body.indexOf(":");
24836
+ if (colon < 0) return null;
24837
+ const category = body.slice(0, colon);
24838
+ const [kv, id, tag] = body.slice(colon + 1).split(".");
24839
+ if (kv === void 0 || id === void 0 || tag === void 0) return null;
24840
+ try {
24841
+ const keyVersion = decodeKeyVersion(kv);
24842
+ const pointerId = base32Decode(id);
24843
+ const tagBytes = base32Decode(tag);
24844
+ if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
24845
+ return null;
24846
+ }
24847
+ return { category, keyVersion, pointerId, tag: tagBytes };
24848
+ } catch {
24849
+ return null;
24850
+ }
24851
+ }
24852
+ var SecretVault = class {
24853
+ #repo;
24854
+ #keys;
24855
+ #fingerprintKey;
24856
+ #isConsented;
24857
+ #verifyGrant;
24858
+ #now;
24859
+ constructor(deps) {
24860
+ this.#repo = deps.repo;
24861
+ this.#keys = deps.keys;
24862
+ this.#fingerprintKey = deps.fingerprintKey;
24863
+ this.#isConsented = deps.isConsented;
24864
+ this.#verifyGrant = deps.verifyGrant;
24865
+ this.#now = deps.now ?? (() => Date.now());
24866
+ }
24867
+ /**
24868
+ * Store a value and return the pointer that stands for it. The same value
24869
+ * always yields the same pointer on this machine — one row, one pointer id,
24870
+ * one category — which is what makes dedup and reuse counting work.
24871
+ */
24872
+ async tokenize(raw, meta3) {
24873
+ if (!this.#isConsented()) return CONSENT_ABSENT;
24874
+ const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
24875
+ const existing = this.#repo.byValueFingerprint(valueFingerprint);
24876
+ const now = this.#now();
24877
+ if (existing) {
24878
+ this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
24879
+ return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
24880
+ }
24881
+ const { material, version: version2 } = await this.#keys.loadOrCreate();
24882
+ const subkeys = deriveSubkeys(material);
24883
+ const pointerId = randomBytes3(POINTER_ID_BYTES);
24884
+ const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
24885
+ const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
24886
+ const { row } = this.#repo.upsert(
24887
+ {
24888
+ pointerId: base32Encode(pointerId),
24889
+ valueFingerprint,
24890
+ fingerprintKeyVersion: this.#fingerprintKey.version,
24891
+ keyVersion: version2,
24892
+ // Recorded so the row stays OPENABLE if the wire-format constant ever
24893
+ // moves: it is part of this row's AEAD AAD. It is not a tag input —
24894
+ // tags are pinned to the constant on both sides.
24895
+ formatVersion: POINTER_FORMAT_VERSION,
24896
+ category: meta3.category,
24897
+ ruleId: meta3.ruleId,
24898
+ maskedMatch: meta3.maskedMatch,
24899
+ provider: meta3.provider,
24900
+ ciphertext: sealed.ciphertext.toString("base64"),
24901
+ nonce: sealed.nonce.toString("base64"),
24902
+ authTag: sealed.authTag.toString("base64")
24903
+ },
24904
+ now
24905
+ );
24906
+ return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
24907
+ }
24908
+ /**
24909
+ * Resolve a pointer back to its value, for a human or (with a grant) for the
24910
+ * model. Every call that gets as far as an identified row writes an audit row.
24911
+ */
24912
+ async detokenize(token, opts) {
24913
+ const parsed = parsePointer(token);
24914
+ if (!parsed) return UNAVAILABLE;
24915
+ let signKey;
24916
+ try {
24917
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
24918
+ signKey = deriveSubkeys(epoch.material).sign;
24919
+ } catch {
24920
+ return UNAVAILABLE;
24921
+ }
24922
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
24923
+ return UNAVAILABLE;
24924
+ }
24925
+ const pointerId = base32Encode(parsed.pointerId);
24926
+ const row = this.#repo.byPointerId(pointerId);
24927
+ if (!row) {
24928
+ this.#audit(pointerId, opts, "unavailable");
24929
+ return UNAVAILABLE;
24930
+ }
24931
+ if (row.category !== parsed.category) return UNAVAILABLE;
24932
+ if (opts.target === "model") {
24933
+ const grantId = opts.grantId;
24934
+ const verify = this.#verifyGrant;
24935
+ if (verify === void 0 || grantId === void 0 || grantId === "") {
24936
+ this.#audit(pointerId, opts, "refused");
24937
+ return UNAVAILABLE;
24938
+ }
24939
+ let covered;
24940
+ try {
24941
+ covered = await verify(grantId, {
24942
+ ruleId: row.ruleId,
24943
+ valueFingerprint: row.valueFingerprint,
24944
+ fingerprintKeyVersion: row.fingerprintKeyVersion
24945
+ });
24946
+ } catch {
24947
+ covered = false;
24948
+ }
24949
+ if (!covered) {
24950
+ this.#audit(pointerId, opts, "refused");
24951
+ return UNAVAILABLE;
24952
+ }
24953
+ }
24954
+ let raw;
24955
+ try {
24956
+ const epoch = await this.#keys.materialFor(row.keyVersion);
24957
+ raw = open(
24958
+ deriveSubkeys(epoch.material).enc,
24959
+ {
24960
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
24961
+ nonce: Buffer.from(row.nonce, "base64"),
24962
+ authTag: Buffer.from(row.authTag, "base64")
24963
+ },
24964
+ // Sealed under the ROW's epoch and format version. Rotation may have
24965
+ // moved the epoch past the one this token names, and a format bump may
24966
+ // have moved the constant past the generation this row was sealed
24967
+ // under — the AAD follows the row in both cases, never the token.
24968
+ bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
24969
+ );
24970
+ } catch {
24971
+ raw = null;
24972
+ }
24973
+ if (raw === null) {
24974
+ this.#audit(pointerId, opts, "unavailable");
24975
+ return UNAVAILABLE;
24976
+ }
24977
+ this.#audit(pointerId, opts, "revealed");
24978
+ return raw;
24979
+ }
24980
+ /**
24981
+ * Owner-surface reveal by row id: the dashboard shows a row the owner can
24982
+ * already see and asks for its value. There is no wire token here to verify —
24983
+ * the tag exists to stop FORGED tokens arriving in untrusted text, and a row
24984
+ * id selected server-side from the owner's own store is not that — so this
24985
+ * loads the row directly, opens its ciphertext under the row's epoch, and
24986
+ * audits exactly like a human-target de-reference. Never callable with
24987
+ * target 'model': the wire-token path with its grant gate is the only road
24988
+ * raw travels toward the model.
24989
+ */
24990
+ async revealEntry(pointerId, opts) {
24991
+ const row = this.#repo.byPointerId(pointerId);
24992
+ if (!row) {
24993
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
24994
+ return UNAVAILABLE;
24995
+ }
24996
+ const raw = await this.#openRow(row);
24997
+ if (raw === null) {
24998
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
24999
+ return UNAVAILABLE;
25000
+ }
25001
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
25002
+ return raw;
25003
+ }
25004
+ /** Badge and listing data. No raw value, no fingerprint, and no audit row. */
25005
+ async describePointer(token) {
25006
+ const row = await this.#rowFor(token);
25007
+ if (!row) return null;
25008
+ return {
25009
+ category: row.category,
25010
+ ...row.provider === void 0 ? {} : { provider: row.provider },
25011
+ maskedMatch: row.maskedMatch,
25012
+ occurrences: row.occurrenceCount,
25013
+ firstSeen: new Date(row.firstSeen).toISOString(),
25014
+ lastSeen: new Date(row.lastSeen).toISOString()
25015
+ };
25016
+ }
25017
+ /**
25018
+ * The raw-free row identity a reveal grant matches on. Deliberately not fed to
25019
+ * view surfaces: the keyed fingerprint is a correlation key and must not reach
25020
+ * a presentation layer.
25021
+ */
25022
+ async resolvePointerIdentity(token) {
25023
+ const row = await this.#rowFor(token);
25024
+ if (!row) return null;
25025
+ return {
25026
+ ruleId: row.ruleId,
25027
+ valueFingerprint: row.valueFingerprint,
25028
+ fingerprintKeyVersion: row.fingerprintKeyVersion
25029
+ };
25030
+ }
25031
+ /**
25032
+ * Mint the next vault key epoch and re-encrypt every entry under it. Pointers
25033
+ * already emitted keep verifying: their tag is checked against the historical
25034
+ * epoch they name, which the key provider retains.
25035
+ *
25036
+ * Safe to interrupt — each row carries the epoch its ciphertext is sealed
25037
+ * under, so a half-finished pass leaves every row openable.
25038
+ *
25039
+ * The rotation lock covers only the keyring mint inside `rotate()`; the
25040
+ * re-seal pass below runs unlocked. Two concurrent rotations therefore
25041
+ * serialize on the keyring but interleave over the rows, so a slower pass can
25042
+ * re-seal a row back to an epoch a faster one already moved past, and
25043
+ * `reEncrypted` can double-count. No value is lost either way — every epoch is
25044
+ * retained and every row stays openable — but "after rotation every row sits
25045
+ * at the newest epoch" does not hold under concurrency. Holding the lock
25046
+ * across the whole pass requires an async-aware lock, since a callback that
25047
+ * awaits would release the lock at its first suspension.
25048
+ */
25049
+ async rotateVaultKey() {
25050
+ const next = await this.#keys.rotate();
25051
+ const nextEnc = deriveSubkeys(next.material).enc;
25052
+ let reEncrypted = 0;
25053
+ for (const row of this.#repo.listAll()) {
25054
+ if (row.keyVersion === next.version) continue;
25055
+ const pointerId = base32Decode(row.pointerId);
25056
+ let raw;
25057
+ try {
25058
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25059
+ raw = open(
25060
+ deriveSubkeys(epoch.material).enc,
25061
+ {
25062
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25063
+ nonce: Buffer.from(row.nonce, "base64"),
25064
+ authTag: Buffer.from(row.authTag, "base64")
25065
+ },
25066
+ bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
25067
+ );
25068
+ } catch {
25069
+ raw = null;
25070
+ }
25071
+ if (raw === null) continue;
25072
+ const sealed = seal(
25073
+ nextEnc,
25074
+ raw,
25075
+ bindingInput(next.version, pointerId, row.category, row.formatVersion),
25076
+ randomBytes3(NONCE_BYTES)
25077
+ );
25078
+ this.#repo.replaceCiphertext(row.pointerId, {
25079
+ keyVersion: next.version,
25080
+ ciphertext: sealed.ciphertext.toString("base64"),
25081
+ nonce: sealed.nonce.toString("base64"),
25082
+ authTag: sealed.authTag.toString("base64")
25083
+ });
25084
+ reEncrypted += 1;
25085
+ }
25086
+ return { version: next.version, reEncrypted };
25087
+ }
25088
+ /**
25089
+ * Re-key every entry's value fingerprint after the exception key rotates,
25090
+ * PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
25091
+ * because the raw values are gone — the vault still holds the values, so
25092
+ * determinism, dedup, and every outstanding pointer survive the rotation.
25093
+ *
25094
+ * Every fingerprint-key rotation must run this: a row left at the old epoch
25095
+ * still resolves, but the same value detected again fingerprints under the
25096
+ * NEW key, misses the dedup lookup, and mints a second row and a second
25097
+ * pointer — one value, two tokens in circulation.
25098
+ *
25099
+ * Per-row best-effort: a row that cannot open, or whose refreshed
25100
+ * fingerprint collides with a row already refreshed, is skipped rather than
25101
+ * aborting the pass — one damaged entry must not strand the re-key of every
25102
+ * other. A skipped row keeps resolving under its old fingerprint epoch.
25103
+ */
25104
+ async refreshFingerprints(next) {
25105
+ let refreshed = 0;
25106
+ for (const row of this.#repo.listAll()) {
25107
+ try {
25108
+ const raw = await this.#openRow(row);
25109
+ if (raw === null) continue;
25110
+ this.#repo.refreshFingerprint(row.pointerId, {
25111
+ valueFingerprint: fingerprintValue(next, raw),
25112
+ fingerprintKeyVersion: next.version
25113
+ });
25114
+ refreshed += 1;
25115
+ } catch {
25116
+ continue;
25117
+ }
25118
+ }
25119
+ return refreshed;
25120
+ }
25121
+ /**
25122
+ * Destroy every entry, making all outstanding pointers permanently
25123
+ * unresolvable.
25124
+ *
25125
+ * The count comes from `purgeAll` rather than a separate `countEntries` —
25126
+ * `purgeAll` counts inside the same transaction that deletes, so the audit row
25127
+ * reports what was actually destroyed. Counting beforehand would let a
25128
+ * concurrent write land between the two statements and put a number in the
25129
+ * durable record that never matched reality.
25130
+ */
25131
+ purgeVault() {
25132
+ const destroyed = this.#repo.purgeAll();
25133
+ this.#repo.recordDeref({
25134
+ id: randomUUID10(),
25135
+ pointerId: VAULT_PURGE_POINTER_ID,
25136
+ at: this.#now(),
25137
+ target: "human",
25138
+ reason: "purge",
25139
+ outcome: "unavailable",
25140
+ pointerCount: Math.max(destroyed, 1)
25141
+ });
25142
+ return destroyed;
25143
+ }
25144
+ // Sign under the epoch the token names — which for a re-detected value is the
25145
+ // epoch its row currently sits at rather than whatever is current.
25146
+ //
25147
+ // The row's format version is NOT a tag input. It binds the row's ciphertext
25148
+ // (it is part of the AEAD AAD, so an old row stays openable) but never the
25149
+ // wire tag, which verification checks against POINTER_FORMAT_VERSION without
25150
+ // knowing any row. Signing a token here under a row's own generation is what
25151
+ // would make the vault emit tokens it then refuses.
25152
+ async #emitToken(keyVersion, pointerIdB32, category) {
25153
+ const pointerId = base32Decode(pointerIdB32);
25154
+ const epoch = await this.#keys.materialFor(keyVersion);
25155
+ const signKey = deriveSubkeys(epoch.material).sign;
25156
+ return formatPointer(
25157
+ category,
25158
+ keyVersion,
25159
+ pointerId,
25160
+ signPointer(signKey, keyVersion, pointerId, category)
25161
+ );
25162
+ }
25163
+ async #openRow(row) {
25164
+ try {
25165
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25166
+ return open(
25167
+ deriveSubkeys(epoch.material).enc,
25168
+ {
25169
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25170
+ nonce: Buffer.from(row.nonce, "base64"),
25171
+ authTag: Buffer.from(row.authTag, "base64")
25172
+ },
25173
+ bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
25174
+ );
25175
+ } catch {
25176
+ return null;
25177
+ }
25178
+ }
25179
+ // Shared lookup for the read-only surfaces. It verifies the tag exactly as
25180
+ // detokenize does: a descriptor is not raw, but a token nobody can vouch for
25181
+ // should not resolve to anything at all — otherwise a fabricated pointer, or a
25182
+ // lookalike planted in a file, would still yield a category and a masked
25183
+ // preview. Verifying needs the historical epoch's key, which is why these
25184
+ // surfaces are async.
25185
+ async #rowFor(token) {
25186
+ const parsed = parsePointer(token);
25187
+ if (!parsed) return null;
25188
+ try {
25189
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
25190
+ const signKey = deriveSubkeys(epoch.material).sign;
25191
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
25192
+ return null;
25193
+ }
25194
+ } catch {
25195
+ return null;
25196
+ }
25197
+ const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
25198
+ if (row?.category !== parsed.category) return null;
25199
+ return row;
25200
+ }
25201
+ #audit(pointerId, opts, outcome) {
25202
+ this.#repo.recordDeref({
25203
+ id: randomUUID10(),
25204
+ pointerId,
25205
+ at: this.#now(),
25206
+ target: opts.target,
25207
+ reason: opts.reason,
25208
+ outcome,
25209
+ ...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
25210
+ // Only the batched reasons carry a count above one; a model crossing is
25211
+ // always its own row.
25212
+ pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
25213
+ });
25214
+ }
25215
+ };
25216
+
25217
+ // ../../packages/persistence/src/warn-era-cap.ts
25218
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25219
+ import { join as join6 } from "path";
25220
+ var MARKER = "warn-era-capped";
25221
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25222
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25223
+ const marker = join6(dataDir2, MARKER);
25224
+ if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25225
+ const capped = db.policies.capCategoryActions();
25226
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
25227
+ `, { mode: DATA_FILE_MODE });
25228
+ return { capped };
25229
+ }
25230
+
25231
+ // ../../packages/plugin-sdk/src/provider-env.ts
25232
+ var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
25233
+ var booleanish = external_exports.string().optional().transform((v) => {
25234
+ if (v === void 0) return void 0;
25235
+ const t = v.trim().toLowerCase();
25236
+ if (t === "" || t === "false" || t === "0") return false;
25237
+ return true;
25238
+ }).catch(void 0);
25239
+ var optionalBaseUrl = external_exports.preprocess((v) => {
25240
+ if (typeof v === "string" && v.trim() === "") return void 0;
25241
+ return v;
25242
+ }, external_exports.string().optional()).catch(void 0);
25243
+ var providerEnvShape = {
25244
+ CLAUDE_CODE_USE_BEDROCK: booleanish,
25245
+ CLAUDE_CODE_USE_VERTEX: booleanish,
25246
+ ANTHROPIC_BASE_URL: optionalBaseUrl
25247
+ };
25248
+ var ProviderEnvSchema = external_exports.object(providerEnvShape);
25249
+
25250
+ // ../../packages/plugin-sdk/src/provider.ts
25251
+ function hostOf(url2) {
25252
+ try {
25253
+ const host = new URL(url2).host;
25254
+ if (host !== "") return host;
23807
25255
  } catch {
23808
25256
  }
23809
25257
  try {
@@ -23832,8 +25280,8 @@ function resolveProvider() {
23832
25280
  function loadConfig(base = defaultDataDir()) {
23833
25281
  try {
23834
25282
  ensureLayoutDirSync(base);
23835
- const settingsFile = join6(settingsDir(base), "settings.json");
23836
- if (existsSync3(settingsFile)) tightenFile(settingsFile);
25283
+ const settingsFile = join7(settingsDir(base), "settings.json");
25284
+ if (existsSync4(settingsFile)) tightenFile(settingsFile);
23837
25285
  } catch {
23838
25286
  }
23839
25287
  migrateLegacyLayout(base);
@@ -23856,9 +25304,9 @@ function resolveProviderSafe() {
23856
25304
  }
23857
25305
 
23858
25306
  // ../../packages/plugin-sdk/src/config-inventory.ts
23859
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
25307
+ import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
23860
25308
  import { homedir as homedir2 } from "os";
23861
- import { basename as basename2, join as join8 } from "path";
25309
+ import { basename as basename2, join as join9 } from "path";
23862
25310
 
23863
25311
  // ../../packages/detections/src/egress/registry.ts
23864
25312
  var EXTRACTOR_VERSION = "1";
@@ -24646,12 +26094,12 @@ function redact(text, findings) {
24646
26094
  const regions = [];
24647
26095
  for (const f of sorted) {
24648
26096
  const rank = SEVERITY_RANK2[f.severity];
24649
- const open = regions[regions.length - 1];
24650
- if (open && f.span.start < open.end) {
24651
- open.end = Math.max(open.end, f.span.end);
24652
- if (rank > open.rank) {
24653
- open.rank = rank;
24654
- open.category = f.category;
26097
+ const open2 = regions[regions.length - 1];
26098
+ if (open2 && f.span.start < open2.end) {
26099
+ open2.end = Math.max(open2.end, f.span.end);
26100
+ if (rank > open2.rank) {
26101
+ open2.rank = rank;
26102
+ open2.category = f.category;
24655
26103
  }
24656
26104
  } else {
24657
26105
  regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
@@ -24680,6 +26128,24 @@ function maskMatch(raw) {
24680
26128
  return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
24681
26129
  }
24682
26130
 
26131
+ // ../../packages/detections/src/pointer-shield.ts
26132
+ function shieldPointers(text) {
26133
+ const spans = [];
26134
+ let out = null;
26135
+ for (const match of text.matchAll(pointerTokenScanner())) {
26136
+ spans.push({ start: match.index, end: match.index + match[0].length });
26137
+ out ??= text;
26138
+ out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
26139
+ }
26140
+ return { text: out ?? text, spans };
26141
+ }
26142
+ function dropShieldedFindings(findings, spans) {
26143
+ if (spans.length === 0) return findings;
26144
+ return findings.filter(
26145
+ (finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
26146
+ );
26147
+ }
26148
+
24683
26149
  // ../../packages/detections/src/posture/config-posture.ts
24684
26150
  var RULE_VERSION = "1";
24685
26151
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
@@ -26879,8 +28345,8 @@ function uniqueRuleIds(findings) {
26879
28345
  }
26880
28346
 
26881
28347
  // ../../packages/plugin-sdk/src/repo.ts
26882
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
26883
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
28348
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28349
+ import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
26884
28350
  function resolveRepo(cwd) {
26885
28351
  try {
26886
28352
  const root = findGitRoot(cwd);
@@ -26895,36 +28361,36 @@ function resolveRepo(cwd) {
26895
28361
  function findGitRoot(start) {
26896
28362
  let dir = start;
26897
28363
  for (; ; ) {
26898
- if (existsSync4(join7(dir, ".git"))) return dir;
28364
+ if (existsSync5(join8(dir, ".git"))) return dir;
26899
28365
  const parent = dirname(dir);
26900
28366
  if (parent === dir) return void 0;
26901
28367
  dir = parent;
26902
28368
  }
26903
28369
  }
26904
28370
  function resolveGitContext(root) {
26905
- const dotGit = join7(root, ".git");
28371
+ const dotGit = join8(root, ".git");
26906
28372
  try {
26907
- if (statSync(dotGit).isDirectory()) {
26908
- return { configPath: join7(dotGit, "config"), headRoot: root };
28373
+ if (statSync2(dotGit).isDirectory()) {
28374
+ return { configPath: join8(dotGit, "config"), headRoot: root };
26909
28375
  }
26910
28376
  } catch {
26911
28377
  return void 0;
26912
28378
  }
26913
28379
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
26914
28380
  if (!target) return void 0;
26915
- const gitdir = isAbsolute(target) ? target : join7(root, target);
26916
- if (existsSync4(join7(gitdir, "config"))) {
26917
- return { configPath: join7(gitdir, "config"), headRoot: root };
28381
+ const gitdir = isAbsolute(target) ? target : join8(root, target);
28382
+ if (existsSync5(join8(gitdir, "config"))) {
28383
+ return { configPath: join8(gitdir, "config"), headRoot: root };
26918
28384
  }
26919
- const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
28385
+ const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
26920
28386
  if (!commonRaw) return void 0;
26921
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
28387
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
26922
28388
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
26923
- return { configPath: join7(commonGitDir, "config"), headRoot };
28389
+ return { configPath: join8(commonGitDir, "config"), headRoot };
26924
28390
  }
26925
28391
  function safeRead(path) {
26926
28392
  try {
26927
- return readFileSync3(path, "utf8");
28393
+ return readFileSync4(path, "utf8");
26928
28394
  } catch {
26929
28395
  return void 0;
26930
28396
  }
@@ -26962,13 +28428,13 @@ function slugFromUrl(url2) {
26962
28428
  }
26963
28429
 
26964
28430
  // ../../packages/plugin-sdk/src/events.ts
26965
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
28431
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
26966
28432
  function contentHashOf(text) {
26967
28433
  return createHash4("sha256").update(text).digest("hex");
26968
28434
  }
26969
28435
  function buildIngestEvent(input) {
26970
28436
  return {
26971
- id: randomUUID9(),
28437
+ id: randomUUID11(),
26972
28438
  sourceTool: input.sourceTool,
26973
28439
  kind: input.kind,
26974
28440
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -26979,7 +28445,7 @@ function buildIngestEvent(input) {
26979
28445
  // SDK boot in the fail-open hook path). Preserve any id the caller already set.
26980
28446
  metadata: {
26981
28447
  ...input.metadata,
26982
- correlationId: input.metadata?.correlationId ?? randomUUID9()
28448
+ correlationId: input.metadata?.correlationId ?? randomUUID11()
26983
28449
  }
26984
28450
  };
26985
28451
  }
@@ -26988,22 +28454,22 @@ function buildIngestEvent(input) {
26988
28454
  import { arch, hostname as hostname3, platform, release } from "os";
26989
28455
 
26990
28456
  // ../../packages/plugin-sdk/src/nudge.ts
26991
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
26992
- import { join as join9 } from "path";
28457
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28458
+ import { join as join10 } from "path";
26993
28459
  var NUDGE_MARKER = "nudge-last-session";
26994
28460
  function claimOnboardingNudge(dataDir2, sessionId) {
26995
28461
  return claimOncePerSession(dataDir2, NUDGE_MARKER, sessionId);
26996
28462
  }
26997
28463
  function claimOncePerSession(dataDir2, marker, sessionId) {
26998
28464
  if (!sessionId) return true;
26999
- const path = join9(dataDir2, marker);
28465
+ const path = join10(dataDir2, marker);
27000
28466
  try {
27001
- if (readFileSync5(path, "utf8") === sessionId) return false;
28467
+ if (readFileSync6(path, "utf8") === sessionId) return false;
27002
28468
  } catch {
27003
28469
  }
27004
28470
  try {
27005
- mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27006
- writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
28471
+ mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
28472
+ writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
27007
28473
  } catch {
27008
28474
  }
27009
28475
  return true;
@@ -27015,8 +28481,8 @@ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
27015
28481
 
27016
28482
  // ../../packages/plugin-sdk/src/project-files.ts
27017
28483
  var import_ignore = __toESM(require_ignore(), 1);
27018
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27019
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
28484
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28485
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
27020
28486
 
27021
28487
  // ../../packages/plugin-sdk/src/rule-quarantine.ts
27022
28488
  var PASS_BUDGET_MS = 2e3;
@@ -27072,7 +28538,7 @@ async function filterUnsafeRules(rules, gateway, opts) {
27072
28538
  }
27073
28539
 
27074
28540
  // ../../packages/plugin-sdk/src/runtime.ts
27075
- import { randomUUID as randomUUID10 } from "crypto";
28541
+ import { randomUUID as randomUUID12 } from "crypto";
27076
28542
  var ENFORCEMENT_CEILING_ENABLED = false;
27077
28543
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
27078
28544
  function entryIsActive(entry, now) {
@@ -27175,7 +28641,12 @@ function createPluginRuntime(gateway, settings, opts) {
27175
28641
  if (worst === "block") return { action: "block", text: null, findings };
27176
28642
  if (worst === "redact") {
27177
28643
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
27178
- return { action: "redact", text: redact(text, redactFindings), findings };
28644
+ return {
28645
+ action: "redact",
28646
+ text: redact(text, redactFindings),
28647
+ findings,
28648
+ enforcedFindings: redactFindings
28649
+ };
27179
28650
  }
27180
28651
  return { action: worst, text, findings };
27181
28652
  }
@@ -27216,9 +28687,17 @@ function createPluginRuntime(gateway, settings, opts) {
27216
28687
  else groups.set(pair, [finding]);
27217
28688
  }
27218
28689
  const now = Date.now();
28690
+ const preAuthorized = new Set(ctx.preAuthorizedGrantIds ?? []);
27219
28691
  for (const [pair, group] of groups) {
27220
28692
  const entry = entries.get(pair);
27221
- if (!entry || !entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
28693
+ if (!entry) continue;
28694
+ if (preAuthorized.has(entry.id)) {
28695
+ if (!conditionsMatch(entry.conditions, ctx)) continue;
28696
+ for (const finding of group) excepted.add(finding);
28697
+ exceptionIds.push(entry.id);
28698
+ continue;
28699
+ }
28700
+ if (!entryIsActive(entry, now) || !conditionsMatch(entry.conditions, ctx)) {
27222
28701
  continue;
27223
28702
  }
27224
28703
  let consumed = false;
@@ -27250,7 +28729,7 @@ function createPluginRuntime(gateway, settings, opts) {
27250
28729
  const pair = `${finding.ruleId}:${fp}`;
27251
28730
  if (seen.has(pair)) continue;
27252
28731
  seen.add(pair);
27253
- const reference = randomUUID10().replaceAll("-", "").slice(0, 6);
28732
+ const reference = randomUUID12().replaceAll("-", "").slice(0, 6);
27254
28733
  const maskedValue = maskMatch(finding.rawMatch);
27255
28734
  try {
27256
28735
  await gateway.recordBlockedDetection({
@@ -27274,7 +28753,8 @@ function createPluginRuntime(gateway, settings, opts) {
27274
28753
  async function evaluate(text, context, ctx) {
27275
28754
  try {
27276
28755
  await ensureInitialized();
27277
- const findings = scan(text, rules, context);
28756
+ const shielded = shieldPointers(text);
28757
+ const findings = dropShieldedFindings(scan(shielded.text, rules, context), shielded.spans);
27278
28758
  const fpCache = /* @__PURE__ */ new Map();
27279
28759
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
27280
28760
  const decision = decide(findings, text, excepted);
@@ -27297,7 +28777,11 @@ function createPluginRuntime(gateway, settings, opts) {
27297
28777
  const { decision, excepted, exceptionIds } = await evaluate(
27298
28778
  input.text,
27299
28779
  filePath ? { filePath } : void 0,
27300
- { sourceTool: input.sourceTool, metadata: input.metadata }
28780
+ {
28781
+ sourceTool: input.sourceTool,
28782
+ metadata: input.metadata,
28783
+ preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
28784
+ }
27301
28785
  );
27302
28786
  if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
27303
28787
  try {
@@ -27327,7 +28811,7 @@ function createPluginRuntime(gateway, settings, opts) {
27327
28811
  valueFingerprint: findingKeyFingerprintKey ? fingerprintOf(findingKeyFingerprintKey, match, findingKeyFpCache) : maskedMatch
27328
28812
  }) : void 0;
27329
28813
  return {
27330
- id: randomUUID10(),
28814
+ id: randomUUID12(),
27331
28815
  eventId: event.id,
27332
28816
  ruleId: match.ruleId,
27333
28817
  category: match.category,
@@ -27353,7 +28837,7 @@ function createPluginRuntime(gateway, settings, opts) {
27353
28837
  const sorted = [...rules].sort((a, b) => a.id.localeCompare(b.id));
27354
28838
  return contentHashOf(JSON.stringify(sorted));
27355
28839
  } catch {
27356
- return `unresolved-${randomUUID10()}`;
28840
+ return `unresolved-${randomUUID12()}`;
27357
28841
  }
27358
28842
  }
27359
28843
  async function close() {
@@ -27366,8 +28850,303 @@ function createPluginRuntime(gateway, settings, opts) {
27366
28850
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27367
28851
 
27368
28852
  // ../../packages/plugin-sdk/src/throttle.ts
27369
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27370
- import { join as join11 } from "path";
28853
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28854
+ import { join as join12 } from "path";
28855
+
28856
+ // ../../packages/plugin-sdk/src/tokenize.ts
28857
+ function redactedPlaceholder(category) {
28858
+ return `[REDACTED:${category.toUpperCase()}]`;
28859
+ }
28860
+ var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
28861
+ var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
28862
+ function groupSpans(text, findings) {
28863
+ 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);
28864
+ const groups = [];
28865
+ for (const finding of sorted) {
28866
+ const last = groups[groups.length - 1];
28867
+ if (last && finding.span.start < last.end) {
28868
+ last.end = Math.max(last.end, finding.span.end);
28869
+ if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
28870
+ last.category = finding.category;
28871
+ last.severity = finding.severity;
28872
+ }
28873
+ delete last.finding;
28874
+ continue;
28875
+ }
28876
+ groups.push({
28877
+ start: finding.span.start,
28878
+ end: finding.span.end,
28879
+ finding,
28880
+ category: finding.category,
28881
+ severity: finding.severity
28882
+ });
28883
+ }
28884
+ return groups;
28885
+ }
28886
+ var NULL_RESOLVER = () => Promise.resolve(null);
28887
+ var SecretVaultGlue = class {
28888
+ #vault;
28889
+ revealGrantResolver;
28890
+ // Set only when THIS glue opened the store, so a glue over an injected vault
28891
+ // never closes a handle it does not own.
28892
+ #release;
28893
+ constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
28894
+ this.#vault = vault;
28895
+ this.revealGrantResolver = revealGrantResolver;
28896
+ this.#release = release2;
28897
+ }
28898
+ close() {
28899
+ const release2 = this.#release;
28900
+ this.#release = void 0;
28901
+ try {
28902
+ release2?.();
28903
+ } catch {
28904
+ }
28905
+ }
28906
+ async tokenizeValue(raw, meta3) {
28907
+ try {
28908
+ const result = await this.#vault.tokenize(raw, meta3);
28909
+ return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
28910
+ } catch {
28911
+ return redactedPlaceholder(meta3.category);
28912
+ }
28913
+ }
28914
+ async tokenizeText(text, opts) {
28915
+ try {
28916
+ const findings = opts?.findings ?? this.#selfScan(text);
28917
+ if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
28918
+ if (findings.length === 0) return { text, pointers: [], degraded: [] };
28919
+ const groups = groupSpans(text, findings);
28920
+ const pointers = [];
28921
+ const degraded = [];
28922
+ let out = text;
28923
+ for (const group of [...groups].reverse()) {
28924
+ const original = text.slice(group.start, group.end);
28925
+ const finding = group.finding;
28926
+ let replacement;
28927
+ if (finding === void 0) {
28928
+ replacement = redactedPlaceholder(group.category);
28929
+ degraded.unshift({ category: group.category });
28930
+ } else if (original !== finding.rawMatch) {
28931
+ replacement = redactedPlaceholder(group.category);
28932
+ degraded.unshift({ category: group.category });
28933
+ } else {
28934
+ replacement = await this.tokenizeValue(finding.rawMatch, {
28935
+ ruleId: finding.ruleId,
28936
+ category: finding.category,
28937
+ maskedMatch: maskMatch(finding.rawMatch)
28938
+ });
28939
+ if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
28940
+ else degraded.unshift({ category: finding.category });
28941
+ }
28942
+ out = out.slice(0, group.start) + replacement + out.slice(group.end);
28943
+ }
28944
+ if (opts?.sighting && pointers.length > 0) {
28945
+ for (const pointer of pointers) {
28946
+ try {
28947
+ const id = pointer.split(".")[1];
28948
+ if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
28949
+ } catch {
28950
+ }
28951
+ }
28952
+ }
28953
+ return { text: out, pointers, degraded };
28954
+ } catch {
28955
+ return { text: "[REDACTED]", pointers: [], degraded: [] };
28956
+ }
28957
+ }
28958
+ async detokenizeText(text, opts) {
28959
+ try {
28960
+ const matches = [...text.matchAll(pointerTokenScanner())];
28961
+ if (matches.length === 0) return { text, revealed: 0 };
28962
+ const occurrences = /* @__PURE__ */ new Map();
28963
+ for (const match of matches) {
28964
+ occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
28965
+ }
28966
+ const resolved = /* @__PURE__ */ new Map();
28967
+ for (const [pointer, count] of occurrences) {
28968
+ try {
28969
+ const value = await this.#vault.detokenize(pointer, {
28970
+ target: "human",
28971
+ reason: opts.reason,
28972
+ pointerCount: count
28973
+ });
28974
+ resolved.set(pointer, typeof value === "string" ? value : null);
28975
+ } catch {
28976
+ resolved.set(pointer, null);
28977
+ }
28978
+ }
28979
+ let out = text;
28980
+ let revealed = 0;
28981
+ for (const match of [...matches].reverse()) {
28982
+ const value = resolved.get(match[0]);
28983
+ const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
28984
+ if (value !== null && value !== void 0) revealed += 1;
28985
+ out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
28986
+ }
28987
+ return { text: out, revealed };
28988
+ } catch {
28989
+ return { text, revealed: 0 };
28990
+ }
28991
+ }
28992
+ // Scan with the bundled packs, as the mask path does. Pointers already in the
28993
+ // text are blanked first so a pointer is never re-tokenized. Returns null
28994
+ // when the registry or the scan itself failed — the caller must then treat
28995
+ // the whole text as unclassifiable.
28996
+ #selfScan(text) {
28997
+ try {
28998
+ registerBundledPacks();
28999
+ const shielded = shieldPointers(text);
29000
+ return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
29001
+ } catch {
29002
+ return null;
29003
+ }
29004
+ }
29005
+ async describePointerSafe(token) {
29006
+ try {
29007
+ return await this.#vault.describePointer(token);
29008
+ } catch {
29009
+ return null;
29010
+ }
29011
+ }
29012
+ async probeModelPointers(text, opts) {
29013
+ const granted = /* @__PURE__ */ new Map();
29014
+ const ungranted = [];
29015
+ try {
29016
+ for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
29017
+ try {
29018
+ const grantId = await opts.resolveGrant(pointer);
29019
+ if (grantId === null) ungranted.push(pointer);
29020
+ else granted.set(pointer, grantId);
29021
+ } catch {
29022
+ ungranted.push(pointer);
29023
+ }
29024
+ }
29025
+ return { granted, ungranted };
29026
+ } catch {
29027
+ return { granted: /* @__PURE__ */ new Map(), ungranted };
29028
+ }
29029
+ }
29030
+ async substituteModelPointers(text, opts) {
29031
+ try {
29032
+ const matches = [...text.matchAll(pointerTokenScanner())];
29033
+ if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
29034
+ const resolved = /* @__PURE__ */ new Map();
29035
+ for (const pointer of new Set(matches.map((m) => m[0]))) {
29036
+ try {
29037
+ const grantId = await opts.resolveGrant(pointer);
29038
+ if (grantId === null) {
29039
+ await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
29040
+ resolved.set(pointer, null);
29041
+ continue;
29042
+ }
29043
+ const value = await this.#vault.detokenize(pointer, {
29044
+ target: "model",
29045
+ reason: "model-input",
29046
+ grantId
29047
+ });
29048
+ resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
29049
+ } catch {
29050
+ resolved.set(pointer, null);
29051
+ }
29052
+ }
29053
+ const spentGrants = /* @__PURE__ */ new Set();
29054
+ for (const entry of resolved.values()) {
29055
+ if (entry === null || spentGrants.has(entry.grantId)) continue;
29056
+ spentGrants.add(entry.grantId);
29057
+ try {
29058
+ await this.#vault.consumeGrant?.(entry.grantId);
29059
+ } catch {
29060
+ }
29061
+ }
29062
+ let out = text;
29063
+ const revealed = /* @__PURE__ */ new Set();
29064
+ const unresolved = /* @__PURE__ */ new Set();
29065
+ for (const match of [...matches].reverse()) {
29066
+ const entry = resolved.get(match[0]);
29067
+ if (entry === null || entry === void 0) {
29068
+ unresolved.add(match[0]);
29069
+ continue;
29070
+ }
29071
+ revealed.add(match[0]);
29072
+ out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
29073
+ }
29074
+ return {
29075
+ text: out,
29076
+ revealed: [...revealed],
29077
+ unresolved: [...unresolved],
29078
+ grantIds: [...spentGrants]
29079
+ };
29080
+ } catch {
29081
+ return { text, revealed: [], unresolved: [], grantIds: [] };
29082
+ }
29083
+ }
29084
+ };
29085
+ function createVaultGlue(options) {
29086
+ if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
29087
+ const base = options?.base ?? defaultDataDir();
29088
+ try {
29089
+ const dir = dataDir(base);
29090
+ const db = openLocalDatabase(dir);
29091
+ const settings = readWorkspaceSettings(base);
29092
+ const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
29093
+ const vault = new SecretVault({
29094
+ repo: db.secretVault,
29095
+ keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
29096
+ fingerprintKey: loadOrCreateFingerprintKey(dir),
29097
+ // Read live so a revocation applies to the very next call, not the next
29098
+ // process.
29099
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
29100
+ // This is the one construction site that reveals to the model, so it is
29101
+ // the one that supplies the last gate. The decision is re-taken from the
29102
+ // ROW's identity at the moment of crossing, which closes the window
29103
+ // between resolving a grant and spending it: a grant revoked in between
29104
+ // refuses here.
29105
+ //
29106
+ // The re-decision is on the identity alone, never on the grant id
29107
+ // matching the one the resolver returned. ExceptionPolicyProvider
29108
+ // promises no id stability across calls — a provider deciding from
29109
+ // external policy may well mint a fresh id each time — so comparing ids
29110
+ // would silently refuse every crossing for such a provider while looking
29111
+ // like a security check. `allow` for this row is the whole question.
29112
+ verifyGrant: async (_grantId, identity) => {
29113
+ const decision = await provider.decideReveal(identity);
29114
+ return decision.allow;
29115
+ }
29116
+ });
29117
+ const vaultWithSightings = {
29118
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
29119
+ detokenize: (token, opts) => vault.detokenize(token, opts),
29120
+ describePointer: (token) => vault.describePointer(token),
29121
+ resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
29122
+ recordSighting: (pointerId, sighting) => {
29123
+ db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
29124
+ },
29125
+ consumeGrant: (grantId) => db.exceptions.consume(grantId)
29126
+ };
29127
+ const revealGrantResolver = async (pointer) => {
29128
+ try {
29129
+ const identity = await vault.resolvePointerIdentity(pointer);
29130
+ if (identity === null) return null;
29131
+ const decision = await provider.decideReveal(identity);
29132
+ return decision.allow ? decision.grantId : null;
29133
+ } catch {
29134
+ return null;
29135
+ }
29136
+ };
29137
+ return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
29138
+ db.close();
29139
+ });
29140
+ } catch {
29141
+ return new SecretVaultGlue(UNOPENABLE_VAULT);
29142
+ }
29143
+ }
29144
+ var UNOPENABLE_VAULT = {
29145
+ tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29146
+ detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
29147
+ describePointer: () => Promise.resolve(null),
29148
+ resolvePointerIdentity: () => Promise.resolve(null)
29149
+ };
27371
29150
 
27372
29151
  // src/present.ts
27373
29152
  var fg = (hex3) => (text) => {
@@ -27416,9 +29195,62 @@ function exceptionPointer(references) {
27416
29195
  return ` To allow this exact value intentionally, run: aka exception approve ${ref.reference}.`;
27417
29196
  }
27418
29197
 
29198
+ // src/hooks/clipboard.ts
29199
+ import { spawnSync } from "child_process";
29200
+ var defaultSpawner = (cmd, args, input) => {
29201
+ try {
29202
+ const result = spawnSync(cmd, args, {
29203
+ input,
29204
+ stdio: ["pipe", "pipe", "pipe"],
29205
+ timeout: 3e3
29206
+ });
29207
+ return { ok: result.error === void 0 && result.status === 0 };
29208
+ } catch {
29209
+ return { ok: false };
29210
+ }
29211
+ };
29212
+ var LINUX_COMMANDS = [
29213
+ { cmd: "wl-copy", args: [] },
29214
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
29215
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
29216
+ ];
29217
+ function commandsFor(platform2) {
29218
+ if (platform2 === "darwin") return [{ cmd: "pbcopy", args: [] }];
29219
+ if (platform2 === "win32") return [{ cmd: "clip", args: [] }];
29220
+ if (platform2 === "linux") return LINUX_COMMANDS;
29221
+ return [];
29222
+ }
29223
+ function writeClipboard(text, opts) {
29224
+ try {
29225
+ const platform2 = opts?.platform ?? process.platform;
29226
+ const spawn = opts?.spawn ?? defaultSpawner;
29227
+ for (const command of commandsFor(platform2)) {
29228
+ if (spawn(command.cmd, [...command.args], text).ok) return true;
29229
+ }
29230
+ return false;
29231
+ } catch {
29232
+ return false;
29233
+ }
29234
+ }
29235
+
27419
29236
  // src/hooks/onboarding-nudge.ts
27420
29237
  var ONBOARDING_NUDGE = "AKA Security is installed but not calibrated \u2014 run /aka:setup to tune notifications to this machine (about a minute).";
27421
29238
 
29239
+ // src/hooks/resubmit-message.ts
29240
+ var REWRITE_OPEN = "----- safe prompt (copy everything between these lines) -----";
29241
+ var REWRITE_CLOSE = "----- end safe prompt -----";
29242
+ function resubmitMessage(opts) {
29243
+ const paste = opts.clipboardWrote ? "It is already on your clipboard \u2014 paste and resubmit." : "Copy it, then paste and resubmit.";
29244
+ return [
29245
+ `AKA blocked this prompt \u2014 flagged ${opts.ruleIds}. The flagged value never reached the model.`,
29246
+ `Here is your prompt with each detected secret replaced by a vault pointer. ${paste}`,
29247
+ REWRITE_OPEN,
29248
+ opts.rewrite,
29249
+ REWRITE_CLOSE,
29250
+ "The model works with the pointers; the real values stay in your local vault." + exceptionPointer(opts.blockedRef ? [opts.blockedRef] : void 0)
29251
+ ].join("\n");
29252
+ }
29253
+
27422
29254
  // src/hooks/shared.ts
27423
29255
  async function readStdin() {
27424
29256
  return new Promise((resolve) => {
@@ -27476,11 +29308,11 @@ function baseMetadata(input) {
27476
29308
  }
27477
29309
 
27478
29310
  // src/hooks/store-health.ts
27479
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
27480
- import { join as join12 } from "path";
29311
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
29312
+ import { join as join13 } from "path";
27481
29313
 
27482
29314
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
27483
- import { randomUUID as randomUUID11 } from "crypto";
29315
+ import { randomUUID as randomUUID13 } from "crypto";
27484
29316
 
27485
29317
  // ../../packages/plugin-runtime/src/recorder.ts
27486
29318
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -27642,7 +29474,7 @@ var StandaloneDataGateway = class {
27642
29474
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
27643
29475
  const installed = this.installedScanRules();
27644
29476
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
27645
- id: randomUUID11(),
29477
+ id: randomUUID13(),
27646
29478
  scope: "global",
27647
29479
  target: { ruleId },
27648
29480
  action,
@@ -27795,7 +29627,7 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
27795
29627
  }
27796
29628
 
27797
29629
  // ../../packages/plugin-runtime/src/handle-session-start.ts
27798
- import { randomUUID as randomUUID12 } from "crypto";
29630
+ import { randomUUID as randomUUID14 } from "crypto";
27799
29631
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
27800
29632
 
27801
29633
  // src/hooks/store-health.ts
@@ -27812,14 +29644,14 @@ function storeUnavailableMessage(dbPath2) {
27812
29644
  }
27813
29645
  function claimStoreUnavailableWarning(dataDir2, sessionId) {
27814
29646
  if (!sessionId) return true;
27815
- const path = join12(dataDir2, STORE_WARNING_MARKER);
29647
+ const path = join13(dataDir2, STORE_WARNING_MARKER);
27816
29648
  try {
27817
- if (readFileSync7(path, "utf8") === sessionId) return false;
29649
+ if (readFileSync8(path, "utf8") === sessionId) return false;
27818
29650
  } catch {
27819
29651
  }
27820
29652
  try {
27821
- mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27822
- writeFileSync5(path, sessionId, { mode: DATA_FILE_MODE });
29653
+ mkdirSync5(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
29654
+ writeFileSync6(path, sessionId, { mode: DATA_FILE_MODE });
27823
29655
  } catch {
27824
29656
  }
27825
29657
  return true;
@@ -27852,20 +29684,23 @@ async function main() {
27852
29684
  } finally {
27853
29685
  await runtime.close();
27854
29686
  }
27855
- if (result.action === "block") {
27856
- await emit({
27857
- decision: "block",
27858
- reason: blockMessage({
27859
- subject: "prompt",
27860
- ruleIds: uniqueRuleIds(result.findings),
27861
- blockedRef: result.blockedReferences?.[0]
27862
- })
27863
- });
29687
+ if (result.action === "block" || result.action === "redact") {
29688
+ const ruleIds = uniqueRuleIds(result.findings);
29689
+ const blockedRef = result.blockedReferences?.[0];
29690
+ let reason = blockMessage({ subject: "prompt", ruleIds, blockedRef });
29691
+ if (isVaultConsentValid(config2.settings.vaultConsent)) {
29692
+ const rewrite = await pointerizedRewrite(prompt, result.findings);
29693
+ if (rewrite !== null) {
29694
+ const clipboardWrote = writeClipboard(rewrite);
29695
+ reason = resubmitMessage({ ruleIds, rewrite, clipboardWrote, blockedRef });
29696
+ }
29697
+ }
29698
+ await emit({ decision: "block", reason });
27864
29699
  return;
27865
29700
  }
27866
- if (result.action === "redact" || result.action === "warn") {
29701
+ if (result.action === "warn") {
27867
29702
  await emit({
27868
- systemMessage: `AKA flagged sensitive content (${uniqueRuleIds(result.findings)}). Prompts cannot be redacted in place \u2014 sent unchanged.${exceptionPointer(result.blockedReferences)}`
29703
+ systemMessage: `AKA flagged sensitive content (${uniqueRuleIds(result.findings)}) \u2014 sent unchanged.${exceptionPointer(result.blockedReferences)}`
27869
29704
  });
27870
29705
  return;
27871
29706
  }
@@ -27873,6 +29708,21 @@ async function main() {
27873
29708
  await emit({ systemMessage: ONBOARDING_NUDGE });
27874
29709
  }
27875
29710
  }
29711
+ async function pointerizedRewrite(prompt, findings) {
29712
+ try {
29713
+ const tokenized = await createVaultGlue().tokenizeText(prompt, {
29714
+ findings,
29715
+ sighting: { location: "prompt", kind: "prompt" }
29716
+ });
29717
+ if (tokenized.pointers.length === 0) return null;
29718
+ for (const finding of findings) {
29719
+ if (finding.rawMatch !== "" && tokenized.text.includes(finding.rawMatch)) return null;
29720
+ }
29721
+ return tokenized.text;
29722
+ } catch {
29723
+ return null;
29724
+ }
29725
+ }
27876
29726
  try {
27877
29727
  await main();
27878
29728
  } catch {