@akasecurity/ai-tc-claude-code 0.9.3 → 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.
@@ -493,10 +493,10 @@ var require_ignore = __commonJS({
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
495
  import { existsSync as existsSync4 } from "fs";
496
- import { join as join6 } from "path";
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
 
@@ -16221,6 +16237,7 @@ var ExceptionConditions = external_exports.object({
16221
16237
  sourceTool: external_exports.string().optional(),
16222
16238
  provider: external_exports.string().optional()
16223
16239
  }).strict();
16240
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16224
16241
  var DetectionException = external_exports.object({
16225
16242
  id: external_exports.guid(),
16226
16243
  ruleId: external_exports.string(),
@@ -16237,6 +16254,7 @@ var DetectionException = external_exports.object({
16237
16254
  keyVersion: external_exports.number().int().positive(),
16238
16255
  // maskMatch() preview of the approved value — never the raw value.
16239
16256
  maskedValue: external_exports.string(),
16257
+ capability: ExceptionCapability.default("suppress"),
16240
16258
  scope: ExceptionScope,
16241
16259
  expiresAt: external_exports.iso.datetime().nullable(),
16242
16260
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16260,6 +16278,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16260
16278
  ruleId: true,
16261
16279
  valueFingerprint: true,
16262
16280
  keyVersion: true,
16281
+ capability: true,
16263
16282
  expiresAt: true,
16264
16283
  maxUses: true,
16265
16284
  useCount: true,
@@ -17364,8 +17383,128 @@ var PatchInstalledPackRequest = external_exports.object({
17364
17383
  message: "At least one field must be provided"
17365
17384
  }).meta({ id: "PatchInstalledPackRequest" });
17366
17385
 
17386
+ // ../../packages/schema/src/zod/vault.ts
17387
+ var POINTER_FORMAT_VERSION = 2;
17388
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17389
+ var POINTER_TOKEN_PATTERN = new RegExp(
17390
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17391
+ );
17392
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17393
+ function pointerTokenScanner() {
17394
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
17395
+ }
17396
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17397
+ var ParsedPointer = external_exports.object({
17398
+ category: DetectionCategory,
17399
+ keyVersion: external_exports.number().int().positive(),
17400
+ pointerId: external_exports.string(),
17401
+ tag: external_exports.string()
17402
+ });
17403
+ var VaultEntry = external_exports.object({
17404
+ pointerId: external_exports.string(),
17405
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17406
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17407
+ // independently of the vault encryption key below.
17408
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17409
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17410
+ // The vault-key epoch this row's ciphertext was sealed under.
17411
+ keyVersion: external_exports.number().int().positive(),
17412
+ // Fixed at first mint and never updated: the same value detected later under a
17413
+ // different rule's category keeps the category it was minted with, so one
17414
+ // value always produces exactly one wire token.
17415
+ category: DetectionCategory,
17416
+ ruleId: external_exports.string(),
17417
+ // Partial-reveal preview for badges and listings. Never the raw value.
17418
+ maskedMatch: external_exports.string(),
17419
+ provider: external_exports.string().optional(),
17420
+ ciphertext: external_exports.string(),
17421
+ nonce: external_exports.string(),
17422
+ authTag: external_exports.string(),
17423
+ // How many times this value has been detected on this machine — the reuse
17424
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17425
+ occurrenceCount: external_exports.number().int().nonnegative(),
17426
+ firstSeen: external_exports.string(),
17427
+ lastSeen: external_exports.string()
17428
+ });
17429
+ var PointerDescriptor = external_exports.object({
17430
+ category: DetectionCategory,
17431
+ provider: external_exports.string().optional(),
17432
+ maskedMatch: external_exports.string(),
17433
+ occurrences: external_exports.number().int().nonnegative(),
17434
+ firstSeen: external_exports.string(),
17435
+ lastSeen: external_exports.string()
17436
+ });
17437
+ var PointerIdentity = external_exports.object({
17438
+ ruleId: external_exports.string(),
17439
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17440
+ fingerprintKeyVersion: external_exports.number().int().positive()
17441
+ });
17442
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17443
+ var VaultDerefReason = external_exports.enum([
17444
+ "display",
17445
+ "explicit-reveal",
17446
+ "view-render",
17447
+ "model-input",
17448
+ "remediation",
17449
+ "purge"
17450
+ ]);
17451
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17452
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
17453
+ function isBatchedDerefReason(reason) {
17454
+ return BATCHED_DEREF_REASONS.includes(reason);
17455
+ }
17456
+ var VaultDeref = external_exports.object({
17457
+ id: external_exports.guid(),
17458
+ pointerId: external_exports.string(),
17459
+ at: external_exports.string(),
17460
+ target: DetokenizeTarget,
17461
+ reason: VaultDerefReason,
17462
+ outcome: VaultDerefOutcome,
17463
+ // Present only on a model-target crossing that a reveal grant authorized.
17464
+ grantId: external_exports.string().optional(),
17465
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17466
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17467
+ pointerCount: external_exports.number().int().positive().default(1)
17468
+ });
17469
+ var VaultSightingKind = external_exports.enum([
17470
+ "prompt",
17471
+ "tool-input",
17472
+ "tool-output",
17473
+ "file",
17474
+ "transcript"
17475
+ ]);
17476
+ var VaultSighting = external_exports.object({
17477
+ location: external_exports.string(),
17478
+ kind: VaultSightingKind,
17479
+ firstSeen: external_exports.string(),
17480
+ lastSeen: external_exports.string()
17481
+ });
17482
+ var VaultInventoryEntry = external_exports.object({
17483
+ pointerId: external_exports.string(),
17484
+ category: DetectionCategory,
17485
+ provider: external_exports.string().optional(),
17486
+ maskedMatch: external_exports.string(),
17487
+ occurrences: external_exports.number().int().nonnegative(),
17488
+ firstSeen: external_exports.string(),
17489
+ lastSeen: external_exports.string(),
17490
+ // The active reveal-to-model grant covering this value, when one exists —
17491
+ // the inventory badges it, the row links to revocation.
17492
+ revealGrantId: external_exports.string().nullable(),
17493
+ sightings: external_exports.array(VaultSighting)
17494
+ });
17495
+ var VaultKeyCustody = external_exports.string();
17496
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17497
+ var VAULT_CONSENT_VERSION = 1;
17498
+ var VaultConsent = external_exports.object({
17499
+ acknowledgedAt: external_exports.iso.datetime(),
17500
+ version: external_exports.number().int().positive()
17501
+ });
17502
+ function isVaultConsentValid(consent) {
17503
+ return consent?.version === VAULT_CONSENT_VERSION;
17504
+ }
17505
+
17367
17506
  // ../../packages/schema/src/zod/local.ts
17368
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17507
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17369
17508
  var RunMode = external_exports.enum(["standalone"]);
17370
17509
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17371
17510
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17387,6 +17526,16 @@ var WorkspaceSettings = external_exports.object({
17387
17526
  // In-place egress extraction on the scan paths; disable to stop all Data
17388
17527
  // Shares writes.
17389
17528
  dataSharesInPlace: external_exports.boolean().default(true),
17529
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17530
+ // vault, instead of destroying them. Absent by default: this is a custody
17531
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17532
+ // Revoking stops future vaulting; it does not erase what is already stored —
17533
+ // purging the vault is the eraser.
17534
+ vaultConsent: VaultConsent.optional(),
17535
+ // Where the vault master key lives.
17536
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17537
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17538
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17390
17539
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17391
17540
  onboardedAt: external_exports.iso.datetime().optional(),
17392
17541
  // Records that the user consented to sending findings to the model API for
@@ -18144,6 +18293,23 @@ function tightenFile(file2) {
18144
18293
  function tightenPerms(file2) {
18145
18294
  for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18146
18295
  }
18296
+ function writeOwnerOnlyFileSync(file2, data) {
18297
+ const tmp = `${file2}.${String(process.pid)}.tmp`;
18298
+ try {
18299
+ rmSync(tmp, { force: true });
18300
+ } catch {
18301
+ }
18302
+ try {
18303
+ writeFileSync(tmp, data, { mode: DATA_FILE_MODE, flag: "wx" });
18304
+ renameSync(tmp, file2);
18305
+ } finally {
18306
+ try {
18307
+ rmSync(tmp, { force: true });
18308
+ } catch {
18309
+ }
18310
+ }
18311
+ tightenFile(file2);
18312
+ }
18147
18313
 
18148
18314
  // ../../packages/persistence/src/migrations.ts
18149
18315
  function describeObject(object2) {
@@ -19784,6 +19950,9 @@ var AmbiguousExceptionIdError = class extends Error {
19784
19950
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19785
19951
  AND (expires_at IS NULL OR expires_at > :now)
19786
19952
  AND (max_uses IS NULL OR use_count < max_uses)`;
19953
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19954
+ AND conditions IS NULL
19955
+ AND ${ACTIVE_PREDICATE}`;
19787
19956
  var SqliteExceptionsRepository = class {
19788
19957
  constructor(db) {
19789
19958
  this.db = db;
@@ -19875,11 +20044,11 @@ var SqliteExceptionsRepository = class {
19875
20044
  this.db.prepare(
19876
20045
  `INSERT INTO exceptions (
19877
20046
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19878
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19879
- conditions, created_by, created_via, created_at, updated_at
20047
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20048
+ justification, conditions, created_by, created_via, created_at, updated_at
19880
20049
  ) VALUES (
19881
20050
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19882
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20051
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19883
20052
  :conditions, :createdBy, :createdVia, :now, :now
19884
20053
  )`
19885
20054
  ).run({
@@ -19889,6 +20058,7 @@ var SqliteExceptionsRepository = class {
19889
20058
  valueFingerprint: input.valueFingerprint,
19890
20059
  keyVersion: input.keyVersion,
19891
20060
  maskedValue: input.maskedValue,
20061
+ capability: input.capability ?? "suppress",
19892
20062
  scope: input.scope,
19893
20063
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19894
20064
  maxUses: input.maxUses,
@@ -19982,6 +20152,7 @@ var SqliteExceptionsRepository = class {
19982
20152
  ruleId: row.rule_id,
19983
20153
  valueFingerprint: row.value_fingerprint,
19984
20154
  keyVersion: row.key_version,
20155
+ capability: row.capability,
19985
20156
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19986
20157
  maxUses: row.max_uses,
19987
20158
  useCount: row.use_count,
@@ -20036,6 +20207,35 @@ var SqliteExceptionsRepository = class {
20036
20207
  }))
20037
20208
  );
20038
20209
  }
20210
+ /**
20211
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20212
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20213
+ * suppression uses — plus the capability: a suppression grant must never
20214
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20215
+ * revealed value re-enters the detection scan immediately afterward and the
20216
+ * suppression match there claims the use — one crossing, one use.
20217
+ *
20218
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20219
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20220
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20221
+ */
20222
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20223
+ try {
20224
+ const row = getRow(
20225
+ this.db.prepare(
20226
+ `SELECT id FROM exceptions
20227
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20228
+ AND key_version = :keyVersion
20229
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20230
+ LIMIT 1`
20231
+ ),
20232
+ { ruleId, valueFingerprint, keyVersion, now }
20233
+ );
20234
+ return Promise.resolve(row ?? null);
20235
+ } catch (err) {
20236
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20237
+ }
20238
+ }
20039
20239
  /**
20040
20240
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20041
20241
  * exhausted) whose last transition is older than the retention window.
@@ -20063,6 +20263,7 @@ function parseExceptionRow(row) {
20063
20263
  valueFingerprint: row.value_fingerprint,
20064
20264
  keyVersion: row.key_version,
20065
20265
  maskedValue: row.masked_value,
20266
+ capability: row.capability,
20066
20267
  scope: row.scope,
20067
20268
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20068
20269
  maxUses: row.max_uses,
@@ -22228,6 +22429,287 @@ var SqliteScanLedgerRepository = class {
22228
22429
  }
22229
22430
  };
22230
22431
 
22432
+ // ../../packages/persistence/src/repositories/secret-vault.ts
22433
+ import { randomUUID as randomUUID7 } from "crypto";
22434
+ var SELECT_COLUMNS = `
22435
+ pointer_id AS pointerId,
22436
+ value_fingerprint AS valueFingerprint,
22437
+ fingerprint_key_version AS fingerprintKeyVersion,
22438
+ key_version AS keyVersion,
22439
+ format_version AS formatVersion,
22440
+ category,
22441
+ rule_id AS ruleId,
22442
+ masked_match AS maskedMatch,
22443
+ provider,
22444
+ ciphertext,
22445
+ nonce,
22446
+ auth_tag AS authTag,
22447
+ occurrence_count AS occurrenceCount,
22448
+ first_seen AS firstSeen,
22449
+ last_seen AS lastSeen`;
22450
+ function toRow(raw) {
22451
+ const { provider, ...rest } = raw;
22452
+ return provider === null ? rest : { ...rest, provider };
22453
+ }
22454
+ var SqliteSecretVaultRepository = class {
22455
+ constructor(db) {
22456
+ this.db = db;
22457
+ this.insertStmt = db.prepare(
22458
+ `INSERT INTO secret_vault (
22459
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
22460
+ format_version, category, rule_id, masked_match, provider,
22461
+ ciphertext, nonce, auth_tag,
22462
+ occurrence_count, first_seen, last_seen
22463
+ ) VALUES (
22464
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
22465
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
22466
+ :ciphertext, :nonce, :authTag,
22467
+ 1, :now, :now
22468
+ )`
22469
+ );
22470
+ this.bumpStmt = db.prepare(
22471
+ `UPDATE secret_vault
22472
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
22473
+ WHERE value_fingerprint = :valueFingerprint`
22474
+ );
22475
+ this.byPointerStmt = db.prepare(
22476
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
22477
+ );
22478
+ this.byFingerprintStmt = db.prepare(
22479
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
22480
+ );
22481
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
22482
+ this.replaceCiphertextStmt = db.prepare(
22483
+ `UPDATE secret_vault
22484
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
22485
+ WHERE pointer_id = :pointerId`
22486
+ );
22487
+ this.refreshFingerprintStmt = db.prepare(
22488
+ `UPDATE secret_vault
22489
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
22490
+ WHERE pointer_id = :pointerId`
22491
+ );
22492
+ this.derefStmt = db.prepare(
22493
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
22494
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
22495
+ );
22496
+ }
22497
+ db;
22498
+ insertStmt;
22499
+ bumpStmt;
22500
+ byPointerStmt;
22501
+ byFingerprintStmt;
22502
+ listStmt;
22503
+ replaceCiphertextStmt;
22504
+ refreshFingerprintStmt;
22505
+ derefStmt;
22506
+ /**
22507
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
22508
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
22509
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
22510
+ * pointer, category and ciphertext, so the same secret always resolves to one
22511
+ * wire token. `minted` is true only when this call created the row.
22512
+ *
22513
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
22514
+ * writers cannot both decide they are minting.
22515
+ */
22516
+ upsert(input, now) {
22517
+ let minted = false;
22518
+ withTransaction(
22519
+ this.db,
22520
+ () => {
22521
+ const existing = getRow(this.byFingerprintStmt, {
22522
+ valueFingerprint: input.valueFingerprint
22523
+ });
22524
+ if (existing === void 0) {
22525
+ this.insertStmt.run(
22526
+ bindParams({
22527
+ pointerId: input.pointerId,
22528
+ valueFingerprint: input.valueFingerprint,
22529
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
22530
+ keyVersion: input.keyVersion,
22531
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
22532
+ category: input.category,
22533
+ ruleId: input.ruleId,
22534
+ maskedMatch: input.maskedMatch,
22535
+ provider: input.provider,
22536
+ ciphertext: input.ciphertext,
22537
+ nonce: input.nonce,
22538
+ authTag: input.authTag,
22539
+ now
22540
+ })
22541
+ );
22542
+ minted = true;
22543
+ return;
22544
+ }
22545
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
22546
+ },
22547
+ "IMMEDIATE"
22548
+ );
22549
+ const row = getRow(this.byFingerprintStmt, {
22550
+ valueFingerprint: input.valueFingerprint
22551
+ });
22552
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
22553
+ return { row: toRow(row), minted };
22554
+ }
22555
+ byPointerId(pointerId) {
22556
+ const raw = getRow(this.byPointerStmt, { pointerId });
22557
+ return raw === void 0 ? null : toRow(raw);
22558
+ }
22559
+ byValueFingerprint(fingerprint) {
22560
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
22561
+ return raw === void 0 ? null : toRow(raw);
22562
+ }
22563
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
22564
+ recordDeref(entry) {
22565
+ this.derefStmt.run(
22566
+ bindParams({
22567
+ id: entry.id,
22568
+ pointerId: entry.pointerId,
22569
+ at: entry.at,
22570
+ target: entry.target,
22571
+ reason: entry.reason,
22572
+ outcome: entry.outcome,
22573
+ grantId: entry.grantId,
22574
+ pointerCount: entry.pointerCount ?? 1
22575
+ })
22576
+ );
22577
+ }
22578
+ listAll() {
22579
+ return allRows(this.listStmt).map(toRow);
22580
+ }
22581
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
22582
+ replaceCiphertext(pointerId, next) {
22583
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
22584
+ }
22585
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
22586
+ refreshFingerprint(pointerId, next) {
22587
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
22588
+ }
22589
+ /**
22590
+ * Destroy every vaulted value and report how many were destroyed. The deref
22591
+ * audit is left alone on purpose — see the table note above.
22592
+ */
22593
+ purgeAll() {
22594
+ let destroyed = 0;
22595
+ withTransaction(
22596
+ this.db,
22597
+ () => {
22598
+ destroyed = this.countEntries();
22599
+ this.db.exec("DELETE FROM secret_vault");
22600
+ },
22601
+ "IMMEDIATE"
22602
+ );
22603
+ return destroyed;
22604
+ }
22605
+ /**
22606
+ * Record (or re-stamp) one place a pointer has been written. One row per
22607
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
22608
+ * on hook paths — a failure must never affect the rewrite that triggered it,
22609
+ * so callers wrap this, not the other way around.
22610
+ */
22611
+ recordSighting(entry, now) {
22612
+ this.db.prepare(
22613
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
22614
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
22615
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22616
+ ).run({
22617
+ id: randomUUID7(),
22618
+ pointerId: entry.pointerId,
22619
+ location: entry.location,
22620
+ kind: entry.kind,
22621
+ now
22622
+ });
22623
+ }
22624
+ listSightings(pointerId) {
22625
+ const rows = allRows(
22626
+ this.db.prepare(
22627
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22628
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
22629
+ ),
22630
+ { pointerId }
22631
+ );
22632
+ return rows.map((r) => ({
22633
+ location: r.location,
22634
+ kind: r.kind,
22635
+ firstSeen: new Date(r.first_seen).toISOString(),
22636
+ lastSeen: new Date(r.last_seen).toISOString()
22637
+ }));
22638
+ }
22639
+ /**
22640
+ * The dashboard inventory: every vaulted value's descriptor data joined with
22641
+ * its sightings and the active reveal-to-model grant when one exists.
22642
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
22643
+ * columns are selected.
22644
+ */
22645
+ listInventory(now = Date.now()) {
22646
+ const rows = allRows(
22647
+ this.db.prepare(
22648
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22649
+ v.occurrence_count, v.first_seen, v.last_seen,
22650
+ (SELECT e.id FROM exceptions e
22651
+ WHERE e.rule_id = v.rule_id
22652
+ AND e.value_fingerprint = v.value_fingerprint
22653
+ AND e.key_version = v.fingerprint_key_version
22654
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22655
+ LIMIT 1) AS grant_id
22656
+ FROM secret_vault v
22657
+ ORDER BY v.last_seen DESC`
22658
+ ),
22659
+ { now }
22660
+ );
22661
+ return rows.map((r) => ({
22662
+ pointerId: r.pointer_id,
22663
+ category: r.category,
22664
+ ...r.provider === null ? {} : { provider: r.provider },
22665
+ maskedMatch: r.masked_match,
22666
+ occurrences: r.occurrence_count,
22667
+ firstSeen: new Date(r.first_seen).toISOString(),
22668
+ lastSeen: new Date(r.last_seen).toISOString(),
22669
+ revealGrantId: r.grant_id,
22670
+ sightings: this.listSightings(r.pointer_id)
22671
+ }));
22672
+ }
22673
+ /**
22674
+ * The de-reference trail, newest first. By default the batched, high-volume
22675
+ * reasons (display, view-render) are hidden and counted instead — the rows
22676
+ * that matter as a signal are the model crossings, and burying them under
22677
+ * render noise would defeat the audit's purpose.
22678
+ */
22679
+ listDerefs(opts) {
22680
+ const limit = opts?.limit ?? 200;
22681
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
22682
+ const rows = allRows(
22683
+ this.db.prepare(
22684
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22685
+ FROM secret_vault_deref ${where}
22686
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
22687
+ ),
22688
+ { limit }
22689
+ );
22690
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
22691
+ this.db,
22692
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22693
+ );
22694
+ return {
22695
+ rows: rows.map((r) => ({
22696
+ id: r.id,
22697
+ pointerId: r.pointer_id,
22698
+ at: new Date(r.at).toISOString(),
22699
+ target: r.target,
22700
+ reason: r.reason,
22701
+ outcome: r.outcome,
22702
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
22703
+ pointerCount: r.pointer_count
22704
+ })),
22705
+ hiddenBatched
22706
+ };
22707
+ }
22708
+ countEntries() {
22709
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22710
+ }
22711
+ };
22712
+
22231
22713
  // ../../packages/persistence/src/repositories/security.ts
22232
22714
  var DAY_MS4 = 864e5;
22233
22715
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22573,7 +23055,7 @@ var SqliteSecurityRepository = class {
22573
23055
  };
22574
23056
 
22575
23057
  // ../../packages/persistence/src/repositories/shares.ts
22576
- import { randomUUID as randomUUID7 } from "crypto";
23058
+ import { randomUUID as randomUUID8 } from "crypto";
22577
23059
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22578
23060
  var IN_CHUNK = 500;
22579
23061
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22829,7 +23311,7 @@ var SqliteSharesRepository = class {
22829
23311
  (id, destination_id, host, decision, created_at, updated_at)
22830
23312
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22831
23313
  ).run({
22832
- id: randomUUID7(),
23314
+ id: randomUUID8(),
22833
23315
  destinationId,
22834
23316
  host: dest.host,
22835
23317
  decision,
@@ -22978,7 +23460,7 @@ var SqliteSharesRepository = class {
22978
23460
  let destinationId = destIds.get(hit.host);
22979
23461
  if (destinationId === void 0) {
22980
23462
  destStmt.run({
22981
- id: randomUUID7(),
23463
+ id: randomUUID8(),
22982
23464
  kind: hit.kind,
22983
23465
  name: hit.name,
22984
23466
  host: hit.host,
@@ -22994,7 +23476,7 @@ var SqliteSharesRepository = class {
22994
23476
  let endpointId = endpointIds.get(endpointKey);
22995
23477
  if (endpointId === void 0) {
22996
23478
  endpointStmt.run({
22997
- id: randomUUID7(),
23479
+ id: randomUUID8(),
22998
23480
  destinationId,
22999
23481
  method: hit.method,
23000
23482
  transport: hit.transport,
@@ -23007,7 +23489,7 @@ var SqliteSharesRepository = class {
23007
23489
  endpointIds.set(endpointKey, endpointId);
23008
23490
  }
23009
23491
  siteStmt.run({
23010
- id: randomUUID7(),
23492
+ id: randomUUID8(),
23011
23493
  endpointId,
23012
23494
  project: input.project,
23013
23495
  projectKey: input.projectKey,
@@ -23423,6 +23905,7 @@ function openAndInitialize(file2) {
23423
23905
  policies,
23424
23906
  installedPacks,
23425
23907
  scanLedger: new SqliteScanLedgerRepository(db),
23908
+ secretVault: new SqliteSecretVaultRepository(db),
23426
23909
  exceptions: new SqliteExceptionsRepository(db),
23427
23910
  resolutions: new SqliteResolutionsRepository(db),
23428
23911
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23458,6 +23941,7 @@ function openLocalDatabase(dir) {
23458
23941
  policies,
23459
23942
  installedPacks,
23460
23943
  scanLedger,
23944
+ secretVault,
23461
23945
  exceptions,
23462
23946
  resolutions,
23463
23947
  ruleProbeCache,
@@ -23566,7 +24050,7 @@ function openLocalDatabase(dir) {
23566
24050
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23567
24051
  if (!definitionId) continue;
23568
24052
  inspectionFindings.insertFinding({
23569
- id: randomUUID8(),
24053
+ id: randomUUID9(),
23570
24054
  auditEventId: record2.scanEvent.id,
23571
24055
  inspectionDefinitionId: definitionId,
23572
24056
  span: finding.span,
@@ -23643,6 +24127,7 @@ function openLocalDatabase(dir) {
23643
24127
  policies,
23644
24128
  installedPacks,
23645
24129
  scanLedger,
24130
+ secretVault,
23646
24131
  exceptions,
23647
24132
  resolutions,
23648
24133
  ruleProbeCache,
@@ -23675,6 +24160,26 @@ function openLocalDatabase(dir) {
23675
24160
  };
23676
24161
  }
23677
24162
 
24163
+ // ../../packages/persistence/src/exception-policy.ts
24164
+ var UserGrantPolicyProvider = class {
24165
+ #exceptions;
24166
+ constructor(exceptions) {
24167
+ this.#exceptions = exceptions;
24168
+ }
24169
+ async decideReveal(identity) {
24170
+ try {
24171
+ const grant = await this.#exceptions.activeRevealGrant(
24172
+ identity.ruleId,
24173
+ identity.valueFingerprint,
24174
+ identity.fingerprintKeyVersion
24175
+ );
24176
+ return grant === null ? { allow: false } : { allow: true, grantId: grant.id };
24177
+ } catch {
24178
+ return { allow: false };
24179
+ }
24180
+ }
24181
+ };
24182
+
23678
24183
  // ../../packages/persistence/src/finding-key.ts
23679
24184
  import { createHash as createHash3 } from "crypto";
23680
24185
 
@@ -23706,6 +24211,58 @@ function parseKeyFile(raw) {
23706
24211
  }
23707
24212
  return { version: version2, material: bytes };
23708
24213
  }
24214
+ var KEY_VERSION_COLUMNS = {
24215
+ exceptions: "key_version",
24216
+ blocked_detections: "key_version",
24217
+ secret_vault: "fingerprint_key_version"
24218
+ };
24219
+ var SQLITE_ERROR = 1;
24220
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
24221
+ var FloorUnreadableError = class extends Error {
24222
+ code = "floor-unreadable";
24223
+ constructor(cause) {
24224
+ super(
24225
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
24226
+ { cause }
24227
+ );
24228
+ this.name = "FloorUnreadableError";
24229
+ }
24230
+ };
24231
+ function storedKeyVersionFloor(dataDir2) {
24232
+ const file2 = join2(dataDir2, DB_FILENAME);
24233
+ if (!existsSync2(file2)) return 0;
24234
+ let db;
24235
+ try {
24236
+ db = new DatabaseSync2(file2, { readOnly: true });
24237
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
24238
+ let floor = 0;
24239
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
24240
+ try {
24241
+ const row = getRow(
24242
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
24243
+ );
24244
+ floor = Math.max(floor, row?.v ?? 0);
24245
+ } catch (err) {
24246
+ if (err.errcode !== SQLITE_ERROR) {
24247
+ throw new FloorUnreadableError(err);
24248
+ }
24249
+ }
24250
+ }
24251
+ return floor;
24252
+ } catch (err) {
24253
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
24254
+ } finally {
24255
+ db?.close();
24256
+ }
24257
+ }
24258
+ function writeKeyFile(dataDir2, key) {
24259
+ ensureDataDirSync(dataDir2);
24260
+ const file2 = keyFilePath(dataDir2);
24261
+ const body = JSON.stringify({ version: key.version, material: key.material.toString("base64") });
24262
+ writeOwnerOnlyFileSync(file2, `${body}
24263
+ `);
24264
+ return key;
24265
+ }
23709
24266
  function readFingerprintKey(dataDir2) {
23710
24267
  let raw;
23711
24268
  try {
@@ -23716,6 +24273,20 @@ function readFingerprintKey(dataDir2) {
23716
24273
  }
23717
24274
  return parseKeyFile(raw);
23718
24275
  }
24276
+ function loadOrCreateFingerprintKey(dataDir2) {
24277
+ const existing = readFingerprintKey(dataDir2);
24278
+ if (existing) {
24279
+ tightenFile(keyFilePath(dataDir2));
24280
+ return existing;
24281
+ }
24282
+ return writeKeyFile(dataDir2, {
24283
+ version: storedKeyVersionFloor(dataDir2) + 1,
24284
+ material: randomBytes(KEY_MATERIAL_BYTES)
24285
+ });
24286
+ }
24287
+ function fingerprintValue(key, raw) {
24288
+ return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
24289
+ }
23719
24290
 
23720
24291
  // ../../packages/persistence/src/local-layout.ts
23721
24292
  import { renameSync as renameSync3 } from "fs";
@@ -23734,6 +24305,9 @@ function dataDir(base = defaultDataDir()) {
23734
24305
  function dbPath(base = defaultDataDir()) {
23735
24306
  return join3(dataDir(base), "aka.db");
23736
24307
  }
24308
+ function keysDir(base = defaultDataDir()) {
24309
+ return join3(base, "keys");
24310
+ }
23737
24311
  function ensureLayoutDirSync(dir = defaultDataDir()) {
23738
24312
  ensureDataDirSync(dir);
23739
24313
  }
@@ -23775,73 +24349,934 @@ function readJson(file2) {
23775
24349
  return parseJsonObject(text) ?? null;
23776
24350
  }
23777
24351
 
23778
- // ../../packages/persistence/src/warn-era-cap.ts
23779
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
23780
- import { join as join5 } from "path";
23781
- var MARKER = "warn-era-capped";
23782
- function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23783
- if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23784
- const marker = join5(dataDir2, MARKER);
23785
- if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
23786
- const capped = db.policies.capCategoryActions();
23787
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
23788
- `, { mode: DATA_FILE_MODE });
23789
- return { capped };
24352
+ // ../../packages/persistence/src/vault/crypto.ts
24353
+ import {
24354
+ createCipheriv,
24355
+ createDecipheriv,
24356
+ createHmac as createHmac2,
24357
+ hkdfSync,
24358
+ timingSafeEqual
24359
+ } from "crypto";
24360
+ var POINTER_ID_BYTES = 16;
24361
+ var NONCE_BYTES = 12;
24362
+ var TAG_BYTES = 10;
24363
+ var SUBKEY_BYTES = 32;
24364
+ var HKDF_INFO_ENC = "aka:vault:enc:v1";
24365
+ var HKDF_INFO_SIGN = "aka:vault:sign:v1";
24366
+ var HKDF_SALT = "aka:vault:v1";
24367
+ var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
24368
+ function base32Encode(bytes) {
24369
+ let out = "";
24370
+ let buffer = 0;
24371
+ let bits = 0;
24372
+ for (const byte of bytes) {
24373
+ buffer = buffer << 8 | byte;
24374
+ bits += 8;
24375
+ while (bits >= 5) {
24376
+ out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
24377
+ bits -= 5;
24378
+ }
24379
+ }
24380
+ if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
24381
+ return out;
23790
24382
  }
23791
-
23792
- // ../../packages/plugin-sdk/src/provider-env.ts
23793
- var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
23794
- var booleanish = external_exports.string().optional().transform((v) => {
23795
- if (v === void 0) return void 0;
23796
- const t = v.trim().toLowerCase();
23797
- if (t === "" || t === "false" || t === "0") return false;
23798
- return true;
23799
- }).catch(void 0);
23800
- var optionalBaseUrl = external_exports.preprocess((v) => {
23801
- if (typeof v === "string" && v.trim() === "") return void 0;
23802
- return v;
23803
- }, external_exports.string().optional()).catch(void 0);
23804
- var providerEnvShape = {
23805
- CLAUDE_CODE_USE_BEDROCK: booleanish,
23806
- CLAUDE_CODE_USE_VERTEX: booleanish,
23807
- ANTHROPIC_BASE_URL: optionalBaseUrl
23808
- };
23809
- var ProviderEnvSchema = external_exports.object(providerEnvShape);
23810
-
23811
- // ../../packages/plugin-sdk/src/provider.ts
23812
- function hostOf(url2) {
23813
- try {
23814
- const host = new URL(url2).host;
23815
- if (host !== "") return host;
23816
- } catch {
23817
- }
24383
+ function base32Decode(text) {
24384
+ const out = [];
24385
+ let buffer = 0;
24386
+ let bits = 0;
24387
+ for (const char of text) {
24388
+ const value = B32_ALPHABET.indexOf(char);
24389
+ if (value < 0) throw new Error("base32: character outside the alphabet");
24390
+ buffer = buffer << 5 | value;
24391
+ bits += 5;
24392
+ if (bits >= 8) {
24393
+ out.push(buffer >>> bits - 8 & 255);
24394
+ bits -= 8;
24395
+ }
24396
+ }
24397
+ return Buffer.from(out);
24398
+ }
24399
+ function encodeKeyVersion(version2) {
24400
+ if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
24401
+ throw new Error("vault: key version out of range");
24402
+ }
24403
+ const bytes = [];
24404
+ let remaining = version2;
24405
+ while (remaining > 0) {
24406
+ bytes.unshift(remaining & 255);
24407
+ remaining = Math.floor(remaining / 256);
24408
+ }
24409
+ return base32Encode(Uint8Array.from(bytes));
24410
+ }
24411
+ function decodeKeyVersion(encoded) {
24412
+ const bytes = base32Decode(encoded);
24413
+ if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
24414
+ let version2 = 0;
24415
+ for (const byte of bytes) version2 = version2 * 256 + byte;
24416
+ if (version2 < 1) throw new Error("vault: bad key version");
24417
+ return version2;
24418
+ }
24419
+ function deriveSubkeys(master) {
24420
+ const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
24421
+ return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
24422
+ }
24423
+ function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
24424
+ if (pointerId.length !== POINTER_ID_BYTES) {
24425
+ throw new Error("vault: pointer id must be 16 bytes");
24426
+ }
24427
+ const head = Buffer.alloc(6);
24428
+ head.writeUInt16BE(formatVersion, 0);
24429
+ head.writeUInt32BE(keyVersion, 2);
24430
+ return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
24431
+ }
24432
+ function seal(encKey, plaintext, aad, nonce) {
24433
+ const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
24434
+ cipher.setAAD(aad);
24435
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24436
+ return { ciphertext, nonce, authTag: cipher.getAuthTag() };
24437
+ }
24438
+ function open(encKey, sealed, aad) {
23818
24439
  try {
23819
- const host = new URL(`https://${url2}`).host;
23820
- return host !== "" ? host : void 0;
24440
+ const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
24441
+ decipher.setAAD(aad);
24442
+ decipher.setAuthTag(sealed.authTag);
24443
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
23821
24444
  } catch {
23822
- return void 0;
24445
+ return null;
23823
24446
  }
23824
24447
  }
23825
- function resolveProvider() {
23826
- const parsed = ProviderEnvSchema.safeParse(process.env);
23827
- const env = parsed.success ? parsed.data : ProviderEnvSchema.parse({});
23828
- if (env.CLAUDE_CODE_USE_BEDROCK === true) return { provider: "bedrock" };
23829
- if (env.CLAUDE_CODE_USE_VERTEX === true) return { provider: "vertex" };
23830
- const baseUrl = env.ANTHROPIC_BASE_URL;
23831
- if (baseUrl !== void 0 && baseUrl !== "") {
23832
- const host = hostOf(baseUrl);
23833
- if (host !== void 0 && host !== DEFAULT_ANTHROPIC_HOST) {
23834
- return { provider: "gateway", gatewayHost: host };
23835
- }
23836
- }
23837
- return { provider: "anthropic" };
24448
+ function signPointer(signKey, keyVersion, pointerId, category) {
24449
+ return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
23838
24450
  }
23839
- function providerFromModelId(modelId) {
23840
- const id = modelId.trim();
23841
- if (id === "") return "unknown";
23842
- if (/^(?:[a-z]{2,4}\.)?anthropic\./.test(id)) return "bedrock";
23843
- if (id.includes("@")) return "vertex";
23844
- if (id.includes("/")) return "gateway";
24451
+ function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
24452
+ if (tag.length !== TAG_BYTES) return false;
24453
+ const expected = signPointer(signKey, keyVersion, pointerId, category);
24454
+ return timingSafeEqual(expected, Buffer.from(tag));
24455
+ }
24456
+ function formatPointer(category, keyVersion, pointerId, tag) {
24457
+ return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
24458
+ }
24459
+
24460
+ // ../../packages/persistence/src/vault/key-provider.ts
24461
+ import { execFileSync } from "child_process";
24462
+ import { randomBytes as randomBytes2 } from "crypto";
24463
+ import {
24464
+ chmodSync as chmodSync2,
24465
+ mkdirSync as mkdirSync2,
24466
+ readFileSync as readFileSync3,
24467
+ renameSync as renameSync4,
24468
+ rmSync as rmSync3,
24469
+ statSync,
24470
+ writeFileSync as writeFileSync2
24471
+ } from "fs";
24472
+ import { join as join5 } from "path";
24473
+ var VaultKeyEpochMissingError = class extends Error {
24474
+ version;
24475
+ constructor(version2) {
24476
+ super(`vault: key epoch ${String(version2)} is not present in the keyring`);
24477
+ this.name = "VaultKeyEpochMissingError";
24478
+ this.version = version2;
24479
+ }
24480
+ };
24481
+ var VAULT_KEY_FILENAME = "vault.key";
24482
+ var KEY_MATERIAL_BYTES2 = 32;
24483
+ var KEYCHAIN_SERVICE = "aka-vault";
24484
+ var KEYCHAIN_ACCOUNT = "keyring";
24485
+ function parseKeyring(raw) {
24486
+ const parsed = JSON.parse(raw);
24487
+ if (typeof parsed !== "object" || parsed === null) {
24488
+ throw new Error("vault key file is corrupt: not a JSON object");
24489
+ }
24490
+ const { current, keys } = parsed;
24491
+ if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
24492
+ throw new Error("vault key file is corrupt: bad current version");
24493
+ }
24494
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
24495
+ throw new Error("vault key file is corrupt: bad keys map");
24496
+ }
24497
+ const map2 = /* @__PURE__ */ new Map();
24498
+ for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
24499
+ const version2 = Number(rawVersion);
24500
+ if (!Number.isInteger(version2) || version2 < 1) {
24501
+ throw new Error("vault key file is corrupt: bad key version");
24502
+ }
24503
+ if (typeof rawMaterial !== "string") {
24504
+ throw new Error("vault key file is corrupt: bad key material");
24505
+ }
24506
+ const bytes = Buffer.from(rawMaterial, "base64");
24507
+ if (bytes.length !== KEY_MATERIAL_BYTES2) {
24508
+ throw new Error("vault key file is corrupt: bad key material length");
24509
+ }
24510
+ map2.set(version2, bytes);
24511
+ }
24512
+ if (!map2.has(current)) {
24513
+ throw new Error("vault key file is corrupt: current version has no material");
24514
+ }
24515
+ return { current, keys: map2 };
24516
+ }
24517
+ function serializeKeyring(keyring) {
24518
+ const keys = {};
24519
+ for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
24520
+ const material = keyring.keys.get(version2);
24521
+ if (material) keys[String(version2)] = material.toString("base64");
24522
+ }
24523
+ return JSON.stringify({ current: keyring.current, keys });
24524
+ }
24525
+ function mintKeyring() {
24526
+ return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
24527
+ }
24528
+ function withNextEpoch(keyring) {
24529
+ const next = Math.max(...keyring.keys.keys()) + 1;
24530
+ const keys = new Map(keyring.keys);
24531
+ keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
24532
+ return { current: next, keys };
24533
+ }
24534
+ function currentOf(keyring) {
24535
+ const material = keyring.keys.get(keyring.current);
24536
+ if (!material) throw new VaultKeyEpochMissingError(keyring.current);
24537
+ return { material, version: keyring.current };
24538
+ }
24539
+ function epochOf(keyring, version2) {
24540
+ const material = keyring.keys.get(version2);
24541
+ if (!material) throw new VaultKeyEpochMissingError(version2);
24542
+ return { material, version: version2 };
24543
+ }
24544
+ function asAsync(work) {
24545
+ try {
24546
+ return Promise.resolve(work());
24547
+ } catch (err) {
24548
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
24549
+ }
24550
+ }
24551
+ function asError(err) {
24552
+ return err instanceof Error ? err : new Error(String(err));
24553
+ }
24554
+ var ROTATION_LOCK_STALE_MS = 6e4;
24555
+ var LOCK_OWNER_FILE = "owner";
24556
+ var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
24557
+ function claimRotationLock(lock, owner) {
24558
+ try {
24559
+ mkdirSync2(lock);
24560
+ } catch (err) {
24561
+ if (err.code === "EEXIST") return false;
24562
+ throw asError(err);
24563
+ }
24564
+ try {
24565
+ writeFileSync2(join5(lock, LOCK_OWNER_FILE), `${owner}
24566
+ `, { mode: DATA_FILE_MODE });
24567
+ return true;
24568
+ } catch (err) {
24569
+ rmSync3(lock, { recursive: true, force: true });
24570
+ throw asError(err);
24571
+ }
24572
+ }
24573
+ function acquireRotationLock(keysDir2) {
24574
+ const lock = join5(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
24575
+ const owner = randomBytes2(16).toString("hex");
24576
+ if (claimRotationLock(lock, owner)) return { lock, owner };
24577
+ let held;
24578
+ try {
24579
+ held = statSync(lock);
24580
+ } catch {
24581
+ throw new Error(ROTATION_IN_PROGRESS);
24582
+ }
24583
+ if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
24584
+ const aside = `${lock}.stale.${owner}`;
24585
+ try {
24586
+ const now = statSync(lock);
24587
+ if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
24588
+ throw new Error(ROTATION_IN_PROGRESS);
24589
+ }
24590
+ renameSync4(lock, aside);
24591
+ } catch (err) {
24592
+ if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
24593
+ throw new Error(ROTATION_IN_PROGRESS, { cause: err });
24594
+ }
24595
+ rmSync3(aside, { recursive: true, force: true });
24596
+ if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
24597
+ return { lock, owner };
24598
+ }
24599
+ function releaseRotationLock(lease) {
24600
+ try {
24601
+ if (readFileSync3(join5(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
24602
+ } catch {
24603
+ return;
24604
+ }
24605
+ rmSync3(lease.lock, { recursive: true, force: true });
24606
+ }
24607
+ function withRotationLock(keysDir2, work) {
24608
+ ensureDataDirSync(keysDir2);
24609
+ const lease = acquireRotationLock(keysDir2);
24610
+ try {
24611
+ return work();
24612
+ } finally {
24613
+ releaseRotationLock(lease);
24614
+ }
24615
+ }
24616
+ var FileKeyProvider = class {
24617
+ #keysDir;
24618
+ constructor(keysDir2) {
24619
+ this.#keysDir = keysDir2;
24620
+ }
24621
+ get filePath() {
24622
+ return join5(this.#keysDir, VAULT_KEY_FILENAME);
24623
+ }
24624
+ loadOrCreate() {
24625
+ return asAsync(() => {
24626
+ const existing = this.#read();
24627
+ if (!existing) return currentOf(this.#createExclusive());
24628
+ tightenFileMode(this.filePath);
24629
+ return currentOf(existing);
24630
+ });
24631
+ }
24632
+ rotate() {
24633
+ return asAsync(
24634
+ () => withRotationLock(this.#keysDir, () => {
24635
+ const existing = this.#read();
24636
+ if (!existing) return currentOf(this.#createExclusive());
24637
+ return currentOf(this.#write(withNextEpoch(existing)));
24638
+ })
24639
+ );
24640
+ }
24641
+ materialFor(version2) {
24642
+ return asAsync(() => {
24643
+ const existing = this.#read();
24644
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24645
+ return epochOf(existing, version2);
24646
+ });
24647
+ }
24648
+ /** The keyring, or null when the file is ABSENT. A corrupt file throws. */
24649
+ #read() {
24650
+ let raw;
24651
+ try {
24652
+ raw = readFileSync3(this.filePath, "utf8");
24653
+ } catch (err) {
24654
+ if (err.code === "ENOENT") return null;
24655
+ throw err instanceof Error ? err : new Error(String(err));
24656
+ }
24657
+ return parseKeyring(raw);
24658
+ }
24659
+ /**
24660
+ * First mint: the keyring is created at its FINAL path with a
24661
+ * creation-exclusive write, so two processes racing a fresh machine cannot
24662
+ * each mint a different epoch 1 — with tmp + rename the loser's replace
24663
+ * would orphan everything the winner had already sealed. On EEXIST the
24664
+ * loser re-reads and adopts the winner's keyring; it minted nothing.
24665
+ * Atomic replace is unnecessary here: nothing can be mid-read of a file
24666
+ * that did not exist, and a torn exclusive write parses as corrupt on the
24667
+ * next read and fails secure rather than being re-minted over.
24668
+ */
24669
+ #createExclusive() {
24670
+ ensureDataDirSync(this.#keysDir);
24671
+ const keyring = mintKeyring();
24672
+ try {
24673
+ writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
24674
+ `, {
24675
+ flag: "wx",
24676
+ mode: DATA_FILE_MODE
24677
+ });
24678
+ } catch (err) {
24679
+ if (err.code !== "EEXIST") throw asError(err);
24680
+ const winner = this.#read();
24681
+ if (!winner) {
24682
+ throw new Error("vault: key file vanished during first mint", { cause: err });
24683
+ }
24684
+ return winner;
24685
+ }
24686
+ tightenFileMode(this.filePath);
24687
+ return keyring;
24688
+ }
24689
+ /**
24690
+ * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
24691
+ * Used only for rotation, under the rotation lock — first creation goes
24692
+ * through the creation-exclusive path instead.
24693
+ */
24694
+ #write(keyring) {
24695
+ ensureDataDirSync(this.#keysDir);
24696
+ const file2 = this.filePath;
24697
+ const tmp = `${file2}.tmp`;
24698
+ writeFileSync2(tmp, `${serializeKeyring(keyring)}
24699
+ `, { mode: DATA_FILE_MODE });
24700
+ renameSync4(tmp, file2);
24701
+ tightenFileMode(file2);
24702
+ return keyring;
24703
+ }
24704
+ };
24705
+ function tightenFileMode(file2) {
24706
+ try {
24707
+ chmodSync2(file2, DATA_FILE_MODE);
24708
+ } catch {
24709
+ }
24710
+ }
24711
+ var runSecurity = (args) => execFileSync("/usr/bin/security", args, {
24712
+ encoding: "utf8",
24713
+ stdio: ["ignore", "pipe", "ignore"]
24714
+ });
24715
+ var SECURITY_ITEM_NOT_FOUND = 44;
24716
+ var KeychainKeyProvider = class {
24717
+ #keysDir;
24718
+ #exec;
24719
+ constructor(keysDir2, exec = runSecurity) {
24720
+ if (exec === runSecurity && process.platform !== "darwin") {
24721
+ throw new Error(
24722
+ `keychain custody is not available on this platform (${process.platform}); use file custody`
24723
+ );
24724
+ }
24725
+ this.#keysDir = keysDir2;
24726
+ this.#exec = exec;
24727
+ }
24728
+ /** Where a fallback file provider for the same vault would keep its keyring. */
24729
+ get keysDir() {
24730
+ return this.#keysDir;
24731
+ }
24732
+ loadOrCreate() {
24733
+ return asAsync(() => {
24734
+ const existing = this.#read();
24735
+ if (existing) return currentOf(existing);
24736
+ return currentOf(this.#create(mintKeyring()));
24737
+ });
24738
+ }
24739
+ rotate() {
24740
+ return asAsync(
24741
+ () => withRotationLock(this.#keysDir, () => {
24742
+ const existing = this.#read();
24743
+ if (!existing) return currentOf(this.#create(mintKeyring()));
24744
+ return currentOf(this.#replace(withNextEpoch(existing)));
24745
+ })
24746
+ );
24747
+ }
24748
+ materialFor(version2) {
24749
+ return asAsync(() => {
24750
+ const existing = this.#read();
24751
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
24752
+ return epochOf(existing, version2);
24753
+ });
24754
+ }
24755
+ /** The keyring, or null when no item exists yet. A corrupt item throws. */
24756
+ #read() {
24757
+ let raw;
24758
+ try {
24759
+ raw = this.#exec([
24760
+ "find-generic-password",
24761
+ "-s",
24762
+ KEYCHAIN_SERVICE,
24763
+ "-a",
24764
+ KEYCHAIN_ACCOUNT,
24765
+ "-w"
24766
+ ]);
24767
+ } catch (err) {
24768
+ if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
24769
+ throw new Error(
24770
+ `vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
24771
+ { cause: err }
24772
+ );
24773
+ }
24774
+ const body = raw.trim();
24775
+ if (body.length === 0) return null;
24776
+ return parseKeyring(body);
24777
+ }
24778
+ /**
24779
+ * First mint: a plain `add-generic-password` (no `-U`) fails when an item
24780
+ * already exists, so a concurrent first mint cannot overwrite the winner's
24781
+ * keyring — the loser re-reads and adopts it instead.
24782
+ */
24783
+ #create(keyring) {
24784
+ const args = [
24785
+ "add-generic-password",
24786
+ "-s",
24787
+ KEYCHAIN_SERVICE,
24788
+ "-a",
24789
+ KEYCHAIN_ACCOUNT,
24790
+ "-w",
24791
+ serializeKeyring(keyring)
24792
+ ];
24793
+ try {
24794
+ this.#exec(args);
24795
+ } catch (err) {
24796
+ const winner = this.#read();
24797
+ if (winner) return winner;
24798
+ throw asError(err);
24799
+ }
24800
+ return keyring;
24801
+ }
24802
+ // `-U` updates the item in place, deliberately replacing the stored map with
24803
+ // one that contains it — used only for rotation, under the rotation lock.
24804
+ #replace(keyring) {
24805
+ this.#exec([
24806
+ "add-generic-password",
24807
+ "-U",
24808
+ "-s",
24809
+ KEYCHAIN_SERVICE,
24810
+ "-a",
24811
+ KEYCHAIN_ACCOUNT,
24812
+ "-w",
24813
+ serializeKeyring(keyring)
24814
+ ]);
24815
+ return keyring;
24816
+ }
24817
+ };
24818
+ function createKeyProvider(custody, keysDir2) {
24819
+ if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
24820
+ return new FileKeyProvider(keysDir2);
24821
+ }
24822
+
24823
+ // ../../packages/persistence/src/vault/vault.ts
24824
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
24825
+ var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
24826
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
24827
+ var VAULT_PURGE_POINTER_ID = "*";
24828
+ function parsePointer(token) {
24829
+ if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
24830
+ const body = token.slice("[[aka:".length, -"]]".length);
24831
+ const colon = body.indexOf(":");
24832
+ if (colon < 0) return null;
24833
+ const category = body.slice(0, colon);
24834
+ const [kv, id, tag] = body.slice(colon + 1).split(".");
24835
+ if (kv === void 0 || id === void 0 || tag === void 0) return null;
24836
+ try {
24837
+ const keyVersion = decodeKeyVersion(kv);
24838
+ const pointerId = base32Decode(id);
24839
+ const tagBytes = base32Decode(tag);
24840
+ if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
24841
+ return null;
24842
+ }
24843
+ return { category, keyVersion, pointerId, tag: tagBytes };
24844
+ } catch {
24845
+ return null;
24846
+ }
24847
+ }
24848
+ var SecretVault = class {
24849
+ #repo;
24850
+ #keys;
24851
+ #fingerprintKey;
24852
+ #isConsented;
24853
+ #verifyGrant;
24854
+ #now;
24855
+ constructor(deps) {
24856
+ this.#repo = deps.repo;
24857
+ this.#keys = deps.keys;
24858
+ this.#fingerprintKey = deps.fingerprintKey;
24859
+ this.#isConsented = deps.isConsented;
24860
+ this.#verifyGrant = deps.verifyGrant;
24861
+ this.#now = deps.now ?? (() => Date.now());
24862
+ }
24863
+ /**
24864
+ * Store a value and return the pointer that stands for it. The same value
24865
+ * always yields the same pointer on this machine — one row, one pointer id,
24866
+ * one category — which is what makes dedup and reuse counting work.
24867
+ */
24868
+ async tokenize(raw, meta3) {
24869
+ if (!this.#isConsented()) return CONSENT_ABSENT;
24870
+ const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
24871
+ const existing = this.#repo.byValueFingerprint(valueFingerprint);
24872
+ const now = this.#now();
24873
+ if (existing) {
24874
+ this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
24875
+ return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
24876
+ }
24877
+ const { material, version: version2 } = await this.#keys.loadOrCreate();
24878
+ const subkeys = deriveSubkeys(material);
24879
+ const pointerId = randomBytes3(POINTER_ID_BYTES);
24880
+ const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
24881
+ const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
24882
+ const { row } = this.#repo.upsert(
24883
+ {
24884
+ pointerId: base32Encode(pointerId),
24885
+ valueFingerprint,
24886
+ fingerprintKeyVersion: this.#fingerprintKey.version,
24887
+ keyVersion: version2,
24888
+ // Recorded so the row stays OPENABLE if the wire-format constant ever
24889
+ // moves: it is part of this row's AEAD AAD. It is not a tag input —
24890
+ // tags are pinned to the constant on both sides.
24891
+ formatVersion: POINTER_FORMAT_VERSION,
24892
+ category: meta3.category,
24893
+ ruleId: meta3.ruleId,
24894
+ maskedMatch: meta3.maskedMatch,
24895
+ provider: meta3.provider,
24896
+ ciphertext: sealed.ciphertext.toString("base64"),
24897
+ nonce: sealed.nonce.toString("base64"),
24898
+ authTag: sealed.authTag.toString("base64")
24899
+ },
24900
+ now
24901
+ );
24902
+ return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
24903
+ }
24904
+ /**
24905
+ * Resolve a pointer back to its value, for a human or (with a grant) for the
24906
+ * model. Every call that gets as far as an identified row writes an audit row.
24907
+ */
24908
+ async detokenize(token, opts) {
24909
+ const parsed = parsePointer(token);
24910
+ if (!parsed) return UNAVAILABLE;
24911
+ let signKey;
24912
+ try {
24913
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
24914
+ signKey = deriveSubkeys(epoch.material).sign;
24915
+ } catch {
24916
+ return UNAVAILABLE;
24917
+ }
24918
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
24919
+ return UNAVAILABLE;
24920
+ }
24921
+ const pointerId = base32Encode(parsed.pointerId);
24922
+ const row = this.#repo.byPointerId(pointerId);
24923
+ if (!row) {
24924
+ this.#audit(pointerId, opts, "unavailable");
24925
+ return UNAVAILABLE;
24926
+ }
24927
+ if (row.category !== parsed.category) return UNAVAILABLE;
24928
+ if (opts.target === "model") {
24929
+ const grantId = opts.grantId;
24930
+ const verify = this.#verifyGrant;
24931
+ if (verify === void 0 || grantId === void 0 || grantId === "") {
24932
+ this.#audit(pointerId, opts, "refused");
24933
+ return UNAVAILABLE;
24934
+ }
24935
+ let covered;
24936
+ try {
24937
+ covered = await verify(grantId, {
24938
+ ruleId: row.ruleId,
24939
+ valueFingerprint: row.valueFingerprint,
24940
+ fingerprintKeyVersion: row.fingerprintKeyVersion
24941
+ });
24942
+ } catch {
24943
+ covered = false;
24944
+ }
24945
+ if (!covered) {
24946
+ this.#audit(pointerId, opts, "refused");
24947
+ return UNAVAILABLE;
24948
+ }
24949
+ }
24950
+ let raw;
24951
+ try {
24952
+ const epoch = await this.#keys.materialFor(row.keyVersion);
24953
+ raw = open(
24954
+ deriveSubkeys(epoch.material).enc,
24955
+ {
24956
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
24957
+ nonce: Buffer.from(row.nonce, "base64"),
24958
+ authTag: Buffer.from(row.authTag, "base64")
24959
+ },
24960
+ // Sealed under the ROW's epoch and format version. Rotation may have
24961
+ // moved the epoch past the one this token names, and a format bump may
24962
+ // have moved the constant past the generation this row was sealed
24963
+ // under — the AAD follows the row in both cases, never the token.
24964
+ bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
24965
+ );
24966
+ } catch {
24967
+ raw = null;
24968
+ }
24969
+ if (raw === null) {
24970
+ this.#audit(pointerId, opts, "unavailable");
24971
+ return UNAVAILABLE;
24972
+ }
24973
+ this.#audit(pointerId, opts, "revealed");
24974
+ return raw;
24975
+ }
24976
+ /**
24977
+ * Owner-surface reveal by row id: the dashboard shows a row the owner can
24978
+ * already see and asks for its value. There is no wire token here to verify —
24979
+ * the tag exists to stop FORGED tokens arriving in untrusted text, and a row
24980
+ * id selected server-side from the owner's own store is not that — so this
24981
+ * loads the row directly, opens its ciphertext under the row's epoch, and
24982
+ * audits exactly like a human-target de-reference. Never callable with
24983
+ * target 'model': the wire-token path with its grant gate is the only road
24984
+ * raw travels toward the model.
24985
+ */
24986
+ async revealEntry(pointerId, opts) {
24987
+ const row = this.#repo.byPointerId(pointerId);
24988
+ if (!row) {
24989
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
24990
+ return UNAVAILABLE;
24991
+ }
24992
+ const raw = await this.#openRow(row);
24993
+ if (raw === null) {
24994
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
24995
+ return UNAVAILABLE;
24996
+ }
24997
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
24998
+ return raw;
24999
+ }
25000
+ /** Badge and listing data. No raw value, no fingerprint, and no audit row. */
25001
+ async describePointer(token) {
25002
+ const row = await this.#rowFor(token);
25003
+ if (!row) return null;
25004
+ return {
25005
+ category: row.category,
25006
+ ...row.provider === void 0 ? {} : { provider: row.provider },
25007
+ maskedMatch: row.maskedMatch,
25008
+ occurrences: row.occurrenceCount,
25009
+ firstSeen: new Date(row.firstSeen).toISOString(),
25010
+ lastSeen: new Date(row.lastSeen).toISOString()
25011
+ };
25012
+ }
25013
+ /**
25014
+ * The raw-free row identity a reveal grant matches on. Deliberately not fed to
25015
+ * view surfaces: the keyed fingerprint is a correlation key and must not reach
25016
+ * a presentation layer.
25017
+ */
25018
+ async resolvePointerIdentity(token) {
25019
+ const row = await this.#rowFor(token);
25020
+ if (!row) return null;
25021
+ return {
25022
+ ruleId: row.ruleId,
25023
+ valueFingerprint: row.valueFingerprint,
25024
+ fingerprintKeyVersion: row.fingerprintKeyVersion
25025
+ };
25026
+ }
25027
+ /**
25028
+ * Mint the next vault key epoch and re-encrypt every entry under it. Pointers
25029
+ * already emitted keep verifying: their tag is checked against the historical
25030
+ * epoch they name, which the key provider retains.
25031
+ *
25032
+ * Safe to interrupt — each row carries the epoch its ciphertext is sealed
25033
+ * under, so a half-finished pass leaves every row openable.
25034
+ *
25035
+ * The rotation lock covers only the keyring mint inside `rotate()`; the
25036
+ * re-seal pass below runs unlocked. Two concurrent rotations therefore
25037
+ * serialize on the keyring but interleave over the rows, so a slower pass can
25038
+ * re-seal a row back to an epoch a faster one already moved past, and
25039
+ * `reEncrypted` can double-count. No value is lost either way — every epoch is
25040
+ * retained and every row stays openable — but "after rotation every row sits
25041
+ * at the newest epoch" does not hold under concurrency. Holding the lock
25042
+ * across the whole pass requires an async-aware lock, since a callback that
25043
+ * awaits would release the lock at its first suspension.
25044
+ */
25045
+ async rotateVaultKey() {
25046
+ const next = await this.#keys.rotate();
25047
+ const nextEnc = deriveSubkeys(next.material).enc;
25048
+ let reEncrypted = 0;
25049
+ for (const row of this.#repo.listAll()) {
25050
+ if (row.keyVersion === next.version) continue;
25051
+ const pointerId = base32Decode(row.pointerId);
25052
+ let raw;
25053
+ try {
25054
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25055
+ raw = open(
25056
+ deriveSubkeys(epoch.material).enc,
25057
+ {
25058
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25059
+ nonce: Buffer.from(row.nonce, "base64"),
25060
+ authTag: Buffer.from(row.authTag, "base64")
25061
+ },
25062
+ bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
25063
+ );
25064
+ } catch {
25065
+ raw = null;
25066
+ }
25067
+ if (raw === null) continue;
25068
+ const sealed = seal(
25069
+ nextEnc,
25070
+ raw,
25071
+ bindingInput(next.version, pointerId, row.category, row.formatVersion),
25072
+ randomBytes3(NONCE_BYTES)
25073
+ );
25074
+ this.#repo.replaceCiphertext(row.pointerId, {
25075
+ keyVersion: next.version,
25076
+ ciphertext: sealed.ciphertext.toString("base64"),
25077
+ nonce: sealed.nonce.toString("base64"),
25078
+ authTag: sealed.authTag.toString("base64")
25079
+ });
25080
+ reEncrypted += 1;
25081
+ }
25082
+ return { version: next.version, reEncrypted };
25083
+ }
25084
+ /**
25085
+ * Re-key every entry's value fingerprint after the exception key rotates,
25086
+ * PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
25087
+ * because the raw values are gone — the vault still holds the values, so
25088
+ * determinism, dedup, and every outstanding pointer survive the rotation.
25089
+ *
25090
+ * Every fingerprint-key rotation must run this: a row left at the old epoch
25091
+ * still resolves, but the same value detected again fingerprints under the
25092
+ * NEW key, misses the dedup lookup, and mints a second row and a second
25093
+ * pointer — one value, two tokens in circulation.
25094
+ *
25095
+ * Per-row best-effort: a row that cannot open, or whose refreshed
25096
+ * fingerprint collides with a row already refreshed, is skipped rather than
25097
+ * aborting the pass — one damaged entry must not strand the re-key of every
25098
+ * other. A skipped row keeps resolving under its old fingerprint epoch.
25099
+ */
25100
+ async refreshFingerprints(next) {
25101
+ let refreshed = 0;
25102
+ for (const row of this.#repo.listAll()) {
25103
+ try {
25104
+ const raw = await this.#openRow(row);
25105
+ if (raw === null) continue;
25106
+ this.#repo.refreshFingerprint(row.pointerId, {
25107
+ valueFingerprint: fingerprintValue(next, raw),
25108
+ fingerprintKeyVersion: next.version
25109
+ });
25110
+ refreshed += 1;
25111
+ } catch {
25112
+ continue;
25113
+ }
25114
+ }
25115
+ return refreshed;
25116
+ }
25117
+ /**
25118
+ * Destroy every entry, making all outstanding pointers permanently
25119
+ * unresolvable.
25120
+ *
25121
+ * The count comes from `purgeAll` rather than a separate `countEntries` —
25122
+ * `purgeAll` counts inside the same transaction that deletes, so the audit row
25123
+ * reports what was actually destroyed. Counting beforehand would let a
25124
+ * concurrent write land between the two statements and put a number in the
25125
+ * durable record that never matched reality.
25126
+ */
25127
+ purgeVault() {
25128
+ const destroyed = this.#repo.purgeAll();
25129
+ this.#repo.recordDeref({
25130
+ id: randomUUID10(),
25131
+ pointerId: VAULT_PURGE_POINTER_ID,
25132
+ at: this.#now(),
25133
+ target: "human",
25134
+ reason: "purge",
25135
+ outcome: "unavailable",
25136
+ pointerCount: Math.max(destroyed, 1)
25137
+ });
25138
+ return destroyed;
25139
+ }
25140
+ // Sign under the epoch the token names — which for a re-detected value is the
25141
+ // epoch its row currently sits at rather than whatever is current.
25142
+ //
25143
+ // The row's format version is NOT a tag input. It binds the row's ciphertext
25144
+ // (it is part of the AEAD AAD, so an old row stays openable) but never the
25145
+ // wire tag, which verification checks against POINTER_FORMAT_VERSION without
25146
+ // knowing any row. Signing a token here under a row's own generation is what
25147
+ // would make the vault emit tokens it then refuses.
25148
+ async #emitToken(keyVersion, pointerIdB32, category) {
25149
+ const pointerId = base32Decode(pointerIdB32);
25150
+ const epoch = await this.#keys.materialFor(keyVersion);
25151
+ const signKey = deriveSubkeys(epoch.material).sign;
25152
+ return formatPointer(
25153
+ category,
25154
+ keyVersion,
25155
+ pointerId,
25156
+ signPointer(signKey, keyVersion, pointerId, category)
25157
+ );
25158
+ }
25159
+ async #openRow(row) {
25160
+ try {
25161
+ const epoch = await this.#keys.materialFor(row.keyVersion);
25162
+ return open(
25163
+ deriveSubkeys(epoch.material).enc,
25164
+ {
25165
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
25166
+ nonce: Buffer.from(row.nonce, "base64"),
25167
+ authTag: Buffer.from(row.authTag, "base64")
25168
+ },
25169
+ bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
25170
+ );
25171
+ } catch {
25172
+ return null;
25173
+ }
25174
+ }
25175
+ // Shared lookup for the read-only surfaces. It verifies the tag exactly as
25176
+ // detokenize does: a descriptor is not raw, but a token nobody can vouch for
25177
+ // should not resolve to anything at all — otherwise a fabricated pointer, or a
25178
+ // lookalike planted in a file, would still yield a category and a masked
25179
+ // preview. Verifying needs the historical epoch's key, which is why these
25180
+ // surfaces are async.
25181
+ async #rowFor(token) {
25182
+ const parsed = parsePointer(token);
25183
+ if (!parsed) return null;
25184
+ try {
25185
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
25186
+ const signKey = deriveSubkeys(epoch.material).sign;
25187
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
25188
+ return null;
25189
+ }
25190
+ } catch {
25191
+ return null;
25192
+ }
25193
+ const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
25194
+ if (row?.category !== parsed.category) return null;
25195
+ return row;
25196
+ }
25197
+ #audit(pointerId, opts, outcome) {
25198
+ this.#repo.recordDeref({
25199
+ id: randomUUID10(),
25200
+ pointerId,
25201
+ at: this.#now(),
25202
+ target: opts.target,
25203
+ reason: opts.reason,
25204
+ outcome,
25205
+ ...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
25206
+ // Only the batched reasons carry a count above one; a model crossing is
25207
+ // always its own row.
25208
+ pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
25209
+ });
25210
+ }
25211
+ };
25212
+
25213
+ // ../../packages/persistence/src/warn-era-cap.ts
25214
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
25215
+ import { join as join6 } from "path";
25216
+ var MARKER = "warn-era-capped";
25217
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
25218
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
25219
+ const marker = join6(dataDir2, MARKER);
25220
+ if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
25221
+ const capped = db.policies.capCategoryActions();
25222
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
25223
+ `, { mode: DATA_FILE_MODE });
25224
+ return { capped };
25225
+ }
25226
+
25227
+ // ../../packages/plugin-sdk/src/provider-env.ts
25228
+ var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
25229
+ var booleanish = external_exports.string().optional().transform((v) => {
25230
+ if (v === void 0) return void 0;
25231
+ const t = v.trim().toLowerCase();
25232
+ if (t === "" || t === "false" || t === "0") return false;
25233
+ return true;
25234
+ }).catch(void 0);
25235
+ var optionalBaseUrl = external_exports.preprocess((v) => {
25236
+ if (typeof v === "string" && v.trim() === "") return void 0;
25237
+ return v;
25238
+ }, external_exports.string().optional()).catch(void 0);
25239
+ var providerEnvShape = {
25240
+ CLAUDE_CODE_USE_BEDROCK: booleanish,
25241
+ CLAUDE_CODE_USE_VERTEX: booleanish,
25242
+ ANTHROPIC_BASE_URL: optionalBaseUrl
25243
+ };
25244
+ var ProviderEnvSchema = external_exports.object(providerEnvShape);
25245
+
25246
+ // ../../packages/plugin-sdk/src/provider.ts
25247
+ function hostOf(url2) {
25248
+ try {
25249
+ const host = new URL(url2).host;
25250
+ if (host !== "") return host;
25251
+ } catch {
25252
+ }
25253
+ try {
25254
+ const host = new URL(`https://${url2}`).host;
25255
+ return host !== "" ? host : void 0;
25256
+ } catch {
25257
+ return void 0;
25258
+ }
25259
+ }
25260
+ function resolveProvider() {
25261
+ const parsed = ProviderEnvSchema.safeParse(process.env);
25262
+ const env = parsed.success ? parsed.data : ProviderEnvSchema.parse({});
25263
+ if (env.CLAUDE_CODE_USE_BEDROCK === true) return { provider: "bedrock" };
25264
+ if (env.CLAUDE_CODE_USE_VERTEX === true) return { provider: "vertex" };
25265
+ const baseUrl = env.ANTHROPIC_BASE_URL;
25266
+ if (baseUrl !== void 0 && baseUrl !== "") {
25267
+ const host = hostOf(baseUrl);
25268
+ if (host !== void 0 && host !== DEFAULT_ANTHROPIC_HOST) {
25269
+ return { provider: "gateway", gatewayHost: host };
25270
+ }
25271
+ }
25272
+ return { provider: "anthropic" };
25273
+ }
25274
+ function providerFromModelId(modelId) {
25275
+ const id = modelId.trim();
25276
+ if (id === "") return "unknown";
25277
+ if (/^(?:[a-z]{2,4}\.)?anthropic\./.test(id)) return "bedrock";
25278
+ if (id.includes("@")) return "vertex";
25279
+ if (id.includes("/")) return "gateway";
23845
25280
  if (id.includes(":")) return "gateway";
23846
25281
  if (id.startsWith("claude-")) return "anthropic";
23847
25282
  return "unknown";
@@ -23851,7 +25286,7 @@ function providerFromModelId(modelId) {
23851
25286
  function loadConfig(base = defaultDataDir()) {
23852
25287
  try {
23853
25288
  ensureLayoutDirSync(base);
23854
- const settingsFile = join6(settingsDir(base), "settings.json");
25289
+ const settingsFile = join7(settingsDir(base), "settings.json");
23855
25290
  if (existsSync4(settingsFile)) tightenFile(settingsFile);
23856
25291
  } catch {
23857
25292
  }
@@ -23875,9 +25310,9 @@ function resolveProviderSafe() {
23875
25310
  }
23876
25311
 
23877
25312
  // ../../packages/plugin-sdk/src/config-inventory.ts
23878
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
25313
+ import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
23879
25314
  import { homedir as homedir2 } from "os";
23880
- import { basename as basename2, join as join8 } from "path";
25315
+ import { basename as basename2, join as join9 } from "path";
23881
25316
 
23882
25317
  // ../../packages/detections/src/egress/registry.ts
23883
25318
  var EXTRACTOR_VERSION = "1";
@@ -24665,12 +26100,12 @@ function redact(text, findings) {
24665
26100
  const regions = [];
24666
26101
  for (const f of sorted) {
24667
26102
  const rank = SEVERITY_RANK2[f.severity];
24668
- const open = regions[regions.length - 1];
24669
- if (open && f.span.start < open.end) {
24670
- open.end = Math.max(open.end, f.span.end);
24671
- if (rank > open.rank) {
24672
- open.rank = rank;
24673
- open.category = f.category;
26103
+ const open2 = regions[regions.length - 1];
26104
+ if (open2 && f.span.start < open2.end) {
26105
+ open2.end = Math.max(open2.end, f.span.end);
26106
+ if (rank > open2.rank) {
26107
+ open2.rank = rank;
26108
+ open2.category = f.category;
24674
26109
  }
24675
26110
  } else {
24676
26111
  regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
@@ -24699,6 +26134,24 @@ function maskMatch(raw) {
24699
26134
  return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
24700
26135
  }
24701
26136
 
26137
+ // ../../packages/detections/src/pointer-shield.ts
26138
+ function shieldPointers(text) {
26139
+ const spans = [];
26140
+ let out = null;
26141
+ for (const match of text.matchAll(pointerTokenScanner())) {
26142
+ spans.push({ start: match.index, end: match.index + match[0].length });
26143
+ out ??= text;
26144
+ out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
26145
+ }
26146
+ return { text: out ?? text, spans };
26147
+ }
26148
+ function dropShieldedFindings(findings, spans) {
26149
+ if (spans.length === 0) return findings;
26150
+ return findings.filter(
26151
+ (finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
26152
+ );
26153
+ }
26154
+
24702
26155
  // ../../packages/detections/src/posture/config-posture.ts
24703
26156
  var RULE_VERSION = "1";
24704
26157
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
@@ -26832,7 +28285,8 @@ function scanText(text, ruleVersions) {
26832
28285
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
26833
28286
  try {
26834
28287
  const rules = getLoadedRules();
26835
- const matches = scan(text, rules);
28288
+ const shielded = shieldPointers(text);
28289
+ const matches = dropShieldedFindings(scan(shielded.text, rules), shielded.spans);
26836
28290
  if (matches.length === 0) return { masked: text, findings: [] };
26837
28291
  const byId = new Map(rules.map((r) => [r.id, r]));
26838
28292
  const findings = matches.map((m) => {
@@ -26855,8 +28309,8 @@ function scanText(text, ruleVersions) {
26855
28309
  }
26856
28310
 
26857
28311
  // ../../packages/plugin-sdk/src/repo.ts
26858
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
26859
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
28312
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
28313
+ import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
26860
28314
  function resolveRepoIdentity(cwd) {
26861
28315
  try {
26862
28316
  const root = findGitRoot(cwd);
@@ -26889,36 +28343,36 @@ function resolveRepoNwo(cwd) {
26889
28343
  function findGitRoot(start) {
26890
28344
  let dir = start;
26891
28345
  for (; ; ) {
26892
- if (existsSync5(join7(dir, ".git"))) return dir;
28346
+ if (existsSync5(join8(dir, ".git"))) return dir;
26893
28347
  const parent = dirname(dir);
26894
28348
  if (parent === dir) return void 0;
26895
28349
  dir = parent;
26896
28350
  }
26897
28351
  }
26898
28352
  function resolveGitContext(root) {
26899
- const dotGit = join7(root, ".git");
28353
+ const dotGit = join8(root, ".git");
26900
28354
  try {
26901
- if (statSync(dotGit).isDirectory()) {
26902
- return { configPath: join7(dotGit, "config"), headRoot: root };
28355
+ if (statSync2(dotGit).isDirectory()) {
28356
+ return { configPath: join8(dotGit, "config"), headRoot: root };
26903
28357
  }
26904
28358
  } catch {
26905
28359
  return void 0;
26906
28360
  }
26907
28361
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
26908
28362
  if (!target) return void 0;
26909
- const gitdir = isAbsolute(target) ? target : join7(root, target);
26910
- if (existsSync5(join7(gitdir, "config"))) {
26911
- return { configPath: join7(gitdir, "config"), headRoot: root };
28363
+ const gitdir = isAbsolute(target) ? target : join8(root, target);
28364
+ if (existsSync5(join8(gitdir, "config"))) {
28365
+ return { configPath: join8(gitdir, "config"), headRoot: root };
26912
28366
  }
26913
- const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
28367
+ const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
26914
28368
  if (!commonRaw) return void 0;
26915
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
28369
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
26916
28370
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
26917
- return { configPath: join7(commonGitDir, "config"), headRoot };
28371
+ return { configPath: join8(commonGitDir, "config"), headRoot };
26918
28372
  }
26919
28373
  function safeRead(path) {
26920
28374
  try {
26921
- return readFileSync3(path, "utf8");
28375
+ return readFileSync4(path, "utf8");
26922
28376
  } catch {
26923
28377
  return void 0;
26924
28378
  }
@@ -26964,67 +28418,362 @@ function nwoFromUrl(url2) {
26964
28418
  } else {
26965
28419
  path = noGit;
26966
28420
  }
26967
- const parts = path.split("/").filter(Boolean);
26968
- return parts.length >= 2 ? parts.join("/") : void 0;
26969
- }
26970
-
26971
- // ../../packages/plugin-sdk/src/events.ts
26972
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
26973
-
26974
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
26975
- import { arch, hostname as hostname3, platform, release } from "os";
26976
- function resolveInventoryContext(input) {
26977
- const host = {
26978
- objectType: "host",
26979
- // Stable-ish machine id; os/arch live in the descriptive bag (a
26980
- // harder machine id can replace this without a schema change).
26981
- identityKey: hostname3(),
26982
- title: hostname3(),
26983
- attributes: { host_name: hostname3(), os: platform(), os_version: release(), arch: arch() }
26984
- };
26985
- const harnessAttributes = {};
26986
- if (input.harnessVersion != null) harnessAttributes.harness_version = input.harnessVersion;
26987
- if (input.harnessInterface != null) harnessAttributes.interface = input.harnessInterface;
26988
- const harness = {
26989
- objectType: "harness",
26990
- identityKey: input.tool,
26991
- title: input.tool,
26992
- attributes: harnessAttributes
26993
- };
26994
- const ctx = { host, harness };
26995
- const repo = resolveRepoIdentity(input.cwd);
26996
- if (repo) {
26997
- const project = { url: repo.url, name: repo.name, attributes: {} };
26998
- ctx.project = project;
28421
+ const parts = path.split("/").filter(Boolean);
28422
+ return parts.length >= 2 ? parts.join("/") : void 0;
28423
+ }
28424
+
28425
+ // ../../packages/plugin-sdk/src/events.ts
28426
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
28427
+
28428
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
28429
+ import { arch, hostname as hostname3, platform, release } from "os";
28430
+ function resolveInventoryContext(input) {
28431
+ const host = {
28432
+ objectType: "host",
28433
+ // Stable-ish machine id; os/arch live in the descriptive bag (a
28434
+ // harder machine id can replace this without a schema change).
28435
+ identityKey: hostname3(),
28436
+ title: hostname3(),
28437
+ attributes: { host_name: hostname3(), os: platform(), os_version: release(), arch: arch() }
28438
+ };
28439
+ const harnessAttributes = {};
28440
+ if (input.harnessVersion != null) harnessAttributes.harness_version = input.harnessVersion;
28441
+ if (input.harnessInterface != null) harnessAttributes.interface = input.harnessInterface;
28442
+ const harness = {
28443
+ objectType: "harness",
28444
+ identityKey: input.tool,
28445
+ title: input.tool,
28446
+ attributes: harnessAttributes
28447
+ };
28448
+ const ctx = { host, harness };
28449
+ const repo = resolveRepoIdentity(input.cwd);
28450
+ if (repo) {
28451
+ const project = { url: repo.url, name: repo.name, attributes: {} };
28452
+ ctx.project = project;
28453
+ }
28454
+ return ctx;
28455
+ }
28456
+
28457
+ // ../../packages/plugin-sdk/src/nudge.ts
28458
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
28459
+ import { join as join10 } from "path";
28460
+
28461
+ // ../../packages/plugin-sdk/src/paths.ts
28462
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
28463
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
28464
+
28465
+ // ../../packages/plugin-sdk/src/project-files.ts
28466
+ var import_ignore = __toESM(require_ignore(), 1);
28467
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28468
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
28469
+
28470
+ // ../../packages/plugin-sdk/src/runtime.ts
28471
+ import { randomUUID as randomUUID12 } from "crypto";
28472
+
28473
+ // ../../packages/plugin-sdk/src/suppressions.ts
28474
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28475
+
28476
+ // ../../packages/plugin-sdk/src/throttle.ts
28477
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
28478
+ import { join as join12 } from "path";
28479
+
28480
+ // ../../packages/plugin-sdk/src/tokenize.ts
28481
+ function redactedPlaceholder(category) {
28482
+ return `[REDACTED:${category.toUpperCase()}]`;
28483
+ }
28484
+ var POINTER_UNAVAILABLE_TEXT = "[unavailable]";
28485
+ var SEVERITY_RANK3 = { critical: 3, high: 2, medium: 1, low: 0 };
28486
+ function groupSpans(text, findings) {
28487
+ 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);
28488
+ const groups = [];
28489
+ for (const finding of sorted) {
28490
+ const last = groups[groups.length - 1];
28491
+ if (last && finding.span.start < last.end) {
28492
+ last.end = Math.max(last.end, finding.span.end);
28493
+ if ((SEVERITY_RANK3[finding.severity] ?? 0) > (SEVERITY_RANK3[last.severity] ?? 0)) {
28494
+ last.category = finding.category;
28495
+ last.severity = finding.severity;
28496
+ }
28497
+ delete last.finding;
28498
+ continue;
28499
+ }
28500
+ groups.push({
28501
+ start: finding.span.start,
28502
+ end: finding.span.end,
28503
+ finding,
28504
+ category: finding.category,
28505
+ severity: finding.severity
28506
+ });
28507
+ }
28508
+ return groups;
28509
+ }
28510
+ var NULL_RESOLVER = () => Promise.resolve(null);
28511
+ var SecretVaultGlue = class {
28512
+ #vault;
28513
+ revealGrantResolver;
28514
+ // Set only when THIS glue opened the store, so a glue over an injected vault
28515
+ // never closes a handle it does not own.
28516
+ #release;
28517
+ constructor(vault, revealGrantResolver = NULL_RESOLVER, release2) {
28518
+ this.#vault = vault;
28519
+ this.revealGrantResolver = revealGrantResolver;
28520
+ this.#release = release2;
28521
+ }
28522
+ close() {
28523
+ const release2 = this.#release;
28524
+ this.#release = void 0;
28525
+ try {
28526
+ release2?.();
28527
+ } catch {
28528
+ }
28529
+ }
28530
+ async tokenizeValue(raw, meta3) {
28531
+ try {
28532
+ const result = await this.#vault.tokenize(raw, meta3);
28533
+ return typeof result === "string" ? result : redactedPlaceholder(meta3.category);
28534
+ } catch {
28535
+ return redactedPlaceholder(meta3.category);
28536
+ }
28537
+ }
28538
+ async tokenizeText(text, opts) {
28539
+ try {
28540
+ const findings = opts?.findings ?? this.#selfScan(text);
28541
+ if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
28542
+ if (findings.length === 0) return { text, pointers: [], degraded: [] };
28543
+ const groups = groupSpans(text, findings);
28544
+ const pointers = [];
28545
+ const degraded = [];
28546
+ let out = text;
28547
+ for (const group of [...groups].reverse()) {
28548
+ const original = text.slice(group.start, group.end);
28549
+ const finding = group.finding;
28550
+ let replacement;
28551
+ if (finding === void 0) {
28552
+ replacement = redactedPlaceholder(group.category);
28553
+ degraded.unshift({ category: group.category });
28554
+ } else if (original !== finding.rawMatch) {
28555
+ replacement = redactedPlaceholder(group.category);
28556
+ degraded.unshift({ category: group.category });
28557
+ } else {
28558
+ replacement = await this.tokenizeValue(finding.rawMatch, {
28559
+ ruleId: finding.ruleId,
28560
+ category: finding.category,
28561
+ maskedMatch: maskMatch(finding.rawMatch)
28562
+ });
28563
+ if (replacement.startsWith("[[aka:")) pointers.unshift(replacement);
28564
+ else degraded.unshift({ category: finding.category });
28565
+ }
28566
+ out = out.slice(0, group.start) + replacement + out.slice(group.end);
28567
+ }
28568
+ if (opts?.sighting && pointers.length > 0) {
28569
+ for (const pointer of pointers) {
28570
+ try {
28571
+ const id = pointer.split(".")[1];
28572
+ if (id !== void 0) this.#vault.recordSighting?.(id, opts.sighting);
28573
+ } catch {
28574
+ }
28575
+ }
28576
+ }
28577
+ return { text: out, pointers, degraded };
28578
+ } catch {
28579
+ return { text: "[REDACTED]", pointers: [], degraded: [] };
28580
+ }
28581
+ }
28582
+ async detokenizeText(text, opts) {
28583
+ try {
28584
+ const matches = [...text.matchAll(pointerTokenScanner())];
28585
+ if (matches.length === 0) return { text, revealed: 0 };
28586
+ const occurrences = /* @__PURE__ */ new Map();
28587
+ for (const match of matches) {
28588
+ occurrences.set(match[0], (occurrences.get(match[0]) ?? 0) + 1);
28589
+ }
28590
+ const resolved = /* @__PURE__ */ new Map();
28591
+ for (const [pointer, count] of occurrences) {
28592
+ try {
28593
+ const value = await this.#vault.detokenize(pointer, {
28594
+ target: "human",
28595
+ reason: opts.reason,
28596
+ pointerCount: count
28597
+ });
28598
+ resolved.set(pointer, typeof value === "string" ? value : null);
28599
+ } catch {
28600
+ resolved.set(pointer, null);
28601
+ }
28602
+ }
28603
+ let out = text;
28604
+ let revealed = 0;
28605
+ for (const match of [...matches].reverse()) {
28606
+ const value = resolved.get(match[0]);
28607
+ const replacement = value ?? POINTER_UNAVAILABLE_TEXT;
28608
+ if (value !== null && value !== void 0) revealed += 1;
28609
+ out = out.slice(0, match.index) + replacement + out.slice(match.index + match[0].length);
28610
+ }
28611
+ return { text: out, revealed };
28612
+ } catch {
28613
+ return { text, revealed: 0 };
28614
+ }
28615
+ }
28616
+ // Scan with the bundled packs, as the mask path does. Pointers already in the
28617
+ // text are blanked first so a pointer is never re-tokenized. Returns null
28618
+ // when the registry or the scan itself failed — the caller must then treat
28619
+ // the whole text as unclassifiable.
28620
+ #selfScan(text) {
28621
+ try {
28622
+ registerBundledPacks();
28623
+ const shielded = shieldPointers(text);
28624
+ return dropShieldedFindings(scan(shielded.text, getLoadedRules()), shielded.spans);
28625
+ } catch {
28626
+ return null;
28627
+ }
28628
+ }
28629
+ async describePointerSafe(token) {
28630
+ try {
28631
+ return await this.#vault.describePointer(token);
28632
+ } catch {
28633
+ return null;
28634
+ }
28635
+ }
28636
+ async probeModelPointers(text, opts) {
28637
+ const granted = /* @__PURE__ */ new Map();
28638
+ const ungranted = [];
28639
+ try {
28640
+ for (const pointer of new Set([...text.matchAll(pointerTokenScanner())].map((m) => m[0]))) {
28641
+ try {
28642
+ const grantId = await opts.resolveGrant(pointer);
28643
+ if (grantId === null) ungranted.push(pointer);
28644
+ else granted.set(pointer, grantId);
28645
+ } catch {
28646
+ ungranted.push(pointer);
28647
+ }
28648
+ }
28649
+ return { granted, ungranted };
28650
+ } catch {
28651
+ return { granted: /* @__PURE__ */ new Map(), ungranted };
28652
+ }
28653
+ }
28654
+ async substituteModelPointers(text, opts) {
28655
+ try {
28656
+ const matches = [...text.matchAll(pointerTokenScanner())];
28657
+ if (matches.length === 0) return { text, revealed: [], unresolved: [], grantIds: [] };
28658
+ const resolved = /* @__PURE__ */ new Map();
28659
+ for (const pointer of new Set(matches.map((m) => m[0]))) {
28660
+ try {
28661
+ const grantId = await opts.resolveGrant(pointer);
28662
+ if (grantId === null) {
28663
+ await this.#vault.detokenize(pointer, { target: "model", reason: "model-input" });
28664
+ resolved.set(pointer, null);
28665
+ continue;
28666
+ }
28667
+ const value = await this.#vault.detokenize(pointer, {
28668
+ target: "model",
28669
+ reason: "model-input",
28670
+ grantId
28671
+ });
28672
+ resolved.set(pointer, typeof value === "string" ? { value, grantId } : null);
28673
+ } catch {
28674
+ resolved.set(pointer, null);
28675
+ }
28676
+ }
28677
+ const spentGrants = /* @__PURE__ */ new Set();
28678
+ for (const entry of resolved.values()) {
28679
+ if (entry === null || spentGrants.has(entry.grantId)) continue;
28680
+ spentGrants.add(entry.grantId);
28681
+ try {
28682
+ await this.#vault.consumeGrant?.(entry.grantId);
28683
+ } catch {
28684
+ }
28685
+ }
28686
+ let out = text;
28687
+ const revealed = /* @__PURE__ */ new Set();
28688
+ const unresolved = /* @__PURE__ */ new Set();
28689
+ for (const match of [...matches].reverse()) {
28690
+ const entry = resolved.get(match[0]);
28691
+ if (entry === null || entry === void 0) {
28692
+ unresolved.add(match[0]);
28693
+ continue;
28694
+ }
28695
+ revealed.add(match[0]);
28696
+ out = out.slice(0, match.index) + entry.value + out.slice(match.index + match[0].length);
28697
+ }
28698
+ return {
28699
+ text: out,
28700
+ revealed: [...revealed],
28701
+ unresolved: [...unresolved],
28702
+ grantIds: [...spentGrants]
28703
+ };
28704
+ } catch {
28705
+ return { text, revealed: [], unresolved: [], grantIds: [] };
28706
+ }
28707
+ }
28708
+ };
28709
+ function createVaultGlue(options) {
28710
+ if (options?.vault) return new SecretVaultGlue(options.vault, options.revealResolver);
28711
+ const base = options?.base ?? defaultDataDir();
28712
+ try {
28713
+ const dir = dataDir(base);
28714
+ const db = openLocalDatabase(dir);
28715
+ const settings = readWorkspaceSettings(base);
28716
+ const provider = options?.policyProvider ?? new UserGrantPolicyProvider(db.exceptions);
28717
+ const vault = new SecretVault({
28718
+ repo: db.secretVault,
28719
+ keys: createKeyProvider(settings.vaultKeyCustody, keysDir(base)),
28720
+ fingerprintKey: loadOrCreateFingerprintKey(dir),
28721
+ // Read live so a revocation applies to the very next call, not the next
28722
+ // process.
28723
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent),
28724
+ // This is the one construction site that reveals to the model, so it is
28725
+ // the one that supplies the last gate. The decision is re-taken from the
28726
+ // ROW's identity at the moment of crossing, which closes the window
28727
+ // between resolving a grant and spending it: a grant revoked in between
28728
+ // refuses here.
28729
+ //
28730
+ // The re-decision is on the identity alone, never on the grant id
28731
+ // matching the one the resolver returned. ExceptionPolicyProvider
28732
+ // promises no id stability across calls — a provider deciding from
28733
+ // external policy may well mint a fresh id each time — so comparing ids
28734
+ // would silently refuse every crossing for such a provider while looking
28735
+ // like a security check. `allow` for this row is the whole question.
28736
+ verifyGrant: async (_grantId, identity) => {
28737
+ const decision = await provider.decideReveal(identity);
28738
+ return decision.allow;
28739
+ }
28740
+ });
28741
+ const vaultWithSightings = {
28742
+ tokenize: (raw, meta3) => vault.tokenize(raw, meta3),
28743
+ detokenize: (token, opts) => vault.detokenize(token, opts),
28744
+ describePointer: (token) => vault.describePointer(token),
28745
+ resolvePointerIdentity: (token) => vault.resolvePointerIdentity(token),
28746
+ recordSighting: (pointerId, sighting) => {
28747
+ db.secretVault.recordSighting({ pointerId, ...sighting }, Date.now());
28748
+ },
28749
+ consumeGrant: (grantId) => db.exceptions.consume(grantId)
28750
+ };
28751
+ const revealGrantResolver = async (pointer) => {
28752
+ try {
28753
+ const identity = await vault.resolvePointerIdentity(pointer);
28754
+ if (identity === null) return null;
28755
+ const decision = await provider.decideReveal(identity);
28756
+ return decision.allow ? decision.grantId : null;
28757
+ } catch {
28758
+ return null;
28759
+ }
28760
+ };
28761
+ return new SecretVaultGlue(vaultWithSightings, revealGrantResolver, () => {
28762
+ db.close();
28763
+ });
28764
+ } catch {
28765
+ return new SecretVaultGlue(UNOPENABLE_VAULT);
26999
28766
  }
27000
- return ctx;
27001
28767
  }
27002
-
27003
- // ../../packages/plugin-sdk/src/nudge.ts
27004
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27005
- import { join as join9 } from "path";
27006
-
27007
- // ../../packages/plugin-sdk/src/paths.ts
27008
- import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
27009
- import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
27010
-
27011
- // ../../packages/plugin-sdk/src/project-files.ts
27012
- var import_ignore = __toESM(require_ignore(), 1);
27013
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27014
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
27015
-
27016
- // ../../packages/plugin-sdk/src/runtime.ts
27017
- import { randomUUID as randomUUID10 } from "crypto";
27018
-
27019
- // ../../packages/plugin-sdk/src/suppressions.ts
27020
- var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27021
-
27022
- // ../../packages/plugin-sdk/src/throttle.ts
27023
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27024
- import { join as join11 } from "path";
28768
+ var UNOPENABLE_VAULT = {
28769
+ tokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
28770
+ detokenize: () => Promise.resolve(/* @__PURE__ */ Symbol("aka.vault.unopenable")),
28771
+ describePointer: () => Promise.resolve(null),
28772
+ resolvePointerIdentity: () => Promise.resolve(null)
28773
+ };
27025
28774
 
27026
28775
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
27027
- import { randomUUID as randomUUID11 } from "crypto";
28776
+ import { randomUUID as randomUUID13 } from "crypto";
27028
28777
 
27029
28778
  // ../../packages/plugin-runtime/src/recorder.ts
27030
28779
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -27063,12 +28812,12 @@ var StandaloneDataGateway = class {
27063
28812
  // reconciler drops the whole pass and recovers it idempotently on the next read.
27064
28813
  recordLlmCalls(inputs) {
27065
28814
  if (inputs.length === 0) return Promise.resolve();
27066
- return new Promise((resolve, reject) => {
28815
+ return new Promise((resolve2, reject) => {
27067
28816
  try {
27068
28817
  this.db.auditEvents.runInTransaction(() => {
27069
28818
  for (const input of inputs) this.db.auditEvents.insertLlmCall(input);
27070
28819
  });
27071
- resolve();
28820
+ resolve2();
27072
28821
  } catch (err) {
27073
28822
  reject(err instanceof Error ? err : new Error(String(err)));
27074
28823
  }
@@ -27080,12 +28829,12 @@ var StandaloneDataGateway = class {
27080
28829
  // drops the whole pass and recovers it idempotently next time.
27081
28830
  recordToolCalls(inputs) {
27082
28831
  if (inputs.length === 0) return Promise.resolve();
27083
- return new Promise((resolve, reject) => {
28832
+ return new Promise((resolve2, reject) => {
27084
28833
  try {
27085
28834
  this.db.auditEvents.runInTransaction(() => {
27086
28835
  for (const input of inputs) this.writeToolCall(input);
27087
28836
  });
27088
- resolve();
28837
+ resolve2();
27089
28838
  } catch (err) {
27090
28839
  reject(err instanceof Error ? err : new Error(String(err)));
27091
28840
  }
@@ -27186,7 +28935,7 @@ var StandaloneDataGateway = class {
27186
28935
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
27187
28936
  const installed = this.installedScanRules();
27188
28937
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
27189
- id: randomUUID11(),
28938
+ id: randomUUID13(),
27190
28939
  scope: "global",
27191
28940
  target: { ruleId },
27192
28941
  action,
@@ -27339,97 +29088,20 @@ function resolveDataGateway(config2, meta3, gatewayFactory = standaloneGatewayFa
27339
29088
  }
27340
29089
 
27341
29090
  // ../../packages/plugin-runtime/src/handle-session-start.ts
27342
- import { randomUUID as randomUUID12 } from "crypto";
29091
+ import { randomUUID as randomUUID14 } from "crypto";
27343
29092
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
27344
29093
 
27345
- // src/history/tail.ts
27346
- import { createHash as createHash5 } from "crypto";
27347
- import {
27348
- closeSync,
27349
- fstatSync,
27350
- mkdirSync as mkdirSync4,
27351
- openSync,
27352
- readFileSync as readFileSync7,
27353
- readSync,
27354
- writeFileSync as writeFileSync5
27355
- } from "fs";
27356
- import { join as join12 } from "path";
27357
- function offsetsDir(dataDir2) {
27358
- return join12(dataDir2, "usage-offsets");
27359
- }
27360
- var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
27361
- function safeSessionId(sessionId) {
27362
- if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
27363
- return sessionId;
27364
- }
27365
- return createHash5("sha256").update(sessionId).digest("hex");
27366
- }
27367
- function offsetPath(dataDir2, sessionId) {
27368
- return join12(offsetsDir(dataDir2), safeSessionId(sessionId));
27369
- }
27370
- function readOffset(dataDir2, sessionId) {
27371
- try {
27372
- const raw = readFileSync7(offsetPath(dataDir2, sessionId), "utf8");
27373
- const parsed = JSON.parse(raw);
27374
- if (typeof parsed === "object" && parsed !== null) {
27375
- const rec = parsed;
27376
- const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
27377
- const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
27378
- return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
27379
- }
27380
- } catch {
27381
- }
27382
- return { offset: 0 };
27383
- }
27384
- function writeOffset(dataDir2, sessionId, value) {
27385
- try {
27386
- mkdirSync4(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
27387
- const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
27388
- writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
27389
- mode: DATA_FILE_MODE
27390
- });
27391
- } catch {
27392
- }
27393
- }
27394
- function readTail(transcriptPath, startOffset) {
27395
- let fd;
27396
- try {
27397
- fd = openSync(transcriptPath, "r");
27398
- } catch {
27399
- return { chunk: "", nextOffset: startOffset };
27400
- }
27401
- try {
27402
- const size = fstatSync(fd).size;
27403
- const from = size < startOffset ? 0 : startOffset;
27404
- if (from >= size) return { chunk: "", nextOffset: from };
27405
- const length = size - from;
27406
- const buf = Buffer.allocUnsafe(length);
27407
- let filled = 0;
27408
- while (filled < length) {
27409
- const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
27410
- if (bytesRead === 0) break;
27411
- filled += bytesRead;
27412
- }
27413
- const slice = buf.subarray(0, filled);
27414
- const lastNl = slice.lastIndexOf(10);
27415
- if (lastNl === -1) return { chunk: "", nextOffset: from };
27416
- const consumedBytes = lastNl + 1;
27417
- const chunk = slice.subarray(0, consumedBytes).toString("utf8");
27418
- return { chunk, nextOffset: from + consumedBytes };
27419
- } catch {
27420
- return { chunk: "", nextOffset: startOffset };
27421
- } finally {
27422
- try {
27423
- closeSync(fd);
27424
- } catch {
27425
- }
27426
- }
27427
- }
29094
+ // src/remediation/redact.ts
29095
+ import { readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
29096
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
27428
29097
 
27429
29098
  // src/history/transcripts.ts
27430
29099
  import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
27431
29100
  import { homedir as homedir3 } from "os";
27432
29101
  import { join as join13 } from "path";
29102
+ function transcriptsDir(home) {
29103
+ return join13(home ?? homedir3(), ".claude", "projects");
29104
+ }
27433
29105
  function isRecord(value) {
27434
29106
  return typeof value === "object" && value !== null;
27435
29107
  }
@@ -27631,6 +29303,156 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
27631
29303
  }
27632
29304
  var DAY_MS5 = 24 * 60 * 60 * 1e3;
27633
29305
 
29306
+ // src/remediation/redact.ts
29307
+ function platformRedactionScope(home) {
29308
+ return { artifactRoots: [transcriptsDir(home)] };
29309
+ }
29310
+ function realPathOrNull(path) {
29311
+ try {
29312
+ return realpathSync3(path);
29313
+ } catch {
29314
+ return null;
29315
+ }
29316
+ }
29317
+ function isWithinRoot(realTarget, root) {
29318
+ const realRoot = realPathOrNull(root);
29319
+ if (realRoot === null) return false;
29320
+ const rel = relative2(realRoot, realTarget);
29321
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
29322
+ }
29323
+ function resolveRedactableArtifact(filePath, scope) {
29324
+ const realTarget = realPathOrNull(resolve(filePath));
29325
+ if (realTarget === null) return null;
29326
+ return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
29327
+ }
29328
+
29329
+ // src/history/tail.ts
29330
+ import { createHash as createHash5 } from "crypto";
29331
+ import {
29332
+ closeSync,
29333
+ fstatSync,
29334
+ mkdirSync as mkdirSync5,
29335
+ openSync,
29336
+ readFileSync as readFileSync10,
29337
+ readSync,
29338
+ writeFileSync as writeFileSync7
29339
+ } from "fs";
29340
+ import { join as join14 } from "path";
29341
+ function offsetsDir(dataDir2) {
29342
+ return join14(dataDir2, "usage-offsets");
29343
+ }
29344
+ var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
29345
+ function safeSessionId(sessionId) {
29346
+ if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
29347
+ return sessionId;
29348
+ }
29349
+ return createHash5("sha256").update(sessionId).digest("hex");
29350
+ }
29351
+ function offsetPath(dataDir2, sessionId) {
29352
+ return join14(offsetsDir(dataDir2), safeSessionId(sessionId));
29353
+ }
29354
+ function readOffset(dataDir2, sessionId) {
29355
+ try {
29356
+ const raw = readFileSync10(offsetPath(dataDir2, sessionId), "utf8");
29357
+ const parsed = JSON.parse(raw);
29358
+ if (typeof parsed === "object" && parsed !== null) {
29359
+ const rec = parsed;
29360
+ const offset = typeof rec.offset === "number" && Number.isFinite(rec.offset) && rec.offset >= 0 ? rec.offset : 0;
29361
+ const lastPromptId = typeof rec.lastPromptId === "string" ? rec.lastPromptId : void 0;
29362
+ return lastPromptId !== void 0 ? { offset, lastPromptId } : { offset };
29363
+ }
29364
+ } catch {
29365
+ }
29366
+ return { offset: 0 };
29367
+ }
29368
+ function writeOffset(dataDir2, sessionId, value) {
29369
+ try {
29370
+ mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
29371
+ const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
29372
+ writeFileSync7(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
29373
+ mode: DATA_FILE_MODE
29374
+ });
29375
+ } catch {
29376
+ }
29377
+ }
29378
+ function readTail(transcriptPath, startOffset) {
29379
+ let fd;
29380
+ try {
29381
+ fd = openSync(transcriptPath, "r");
29382
+ } catch {
29383
+ return { chunk: "", nextOffset: startOffset };
29384
+ }
29385
+ try {
29386
+ const size = fstatSync(fd).size;
29387
+ const from = size < startOffset ? 0 : startOffset;
29388
+ if (from >= size) return { chunk: "", nextOffset: from };
29389
+ const length = size - from;
29390
+ const buf = Buffer.allocUnsafe(length);
29391
+ let filled = 0;
29392
+ while (filled < length) {
29393
+ const bytesRead = readSync(fd, buf, filled, length - filled, from + filled);
29394
+ if (bytesRead === 0) break;
29395
+ filled += bytesRead;
29396
+ }
29397
+ const slice = buf.subarray(0, filled);
29398
+ const lastNl = slice.lastIndexOf(10);
29399
+ if (lastNl === -1) return { chunk: "", nextOffset: from };
29400
+ const consumedBytes = lastNl + 1;
29401
+ const chunk = slice.subarray(0, consumedBytes).toString("utf8");
29402
+ return { chunk, nextOffset: from + consumedBytes };
29403
+ } catch {
29404
+ return { chunk: "", nextOffset: startOffset };
29405
+ } finally {
29406
+ try {
29407
+ closeSync(fd);
29408
+ } catch {
29409
+ }
29410
+ }
29411
+ }
29412
+
29413
+ // src/history/tail-scrub.ts
29414
+ import { readFileSync as readFileSync11, renameSync as renameSync6, rmSync as rmSync5, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
29415
+ var DEFAULT_MAX_SCRUB_BYTES = 32 * 1024 * 1024;
29416
+ async function scrubTranscriptTail(filePath, deps) {
29417
+ try {
29418
+ const realPath = resolveRedactableArtifact(filePath, deps.scope);
29419
+ if (realPath === null) return null;
29420
+ const statBefore = statSync5(realPath);
29421
+ if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
29422
+ const content = readFileSync11(realPath, "utf8");
29423
+ const lines = content.split("\n");
29424
+ let rewritten = 0;
29425
+ for (const [i, line] of lines.entries()) {
29426
+ if (line === "") continue;
29427
+ const result = await deps.tokenizeText(line);
29428
+ if (result.text === line) continue;
29429
+ if (result.pointers.length === 0 && result.degraded.length === 0) return null;
29430
+ lines[i] = result.text;
29431
+ rewritten += 1;
29432
+ }
29433
+ if (rewritten === 0) return { rewritten: 0 };
29434
+ const tmpPath = `${realPath}.aka-scrub.tmp`;
29435
+ try {
29436
+ writeFileSync8(tmpPath, lines.join("\n"), { mode: statBefore.mode & 511 });
29437
+ const statNow = statSync5(realPath);
29438
+ if (statNow.size !== statBefore.size || statNow.mtimeMs !== statBefore.mtimeMs) {
29439
+ rmSync5(tmpPath, { force: true, recursive: true });
29440
+ return null;
29441
+ }
29442
+ renameSync6(tmpPath, realPath);
29443
+ } catch {
29444
+ try {
29445
+ rmSync5(tmpPath, { force: true, recursive: true });
29446
+ } catch {
29447
+ }
29448
+ return null;
29449
+ }
29450
+ return { rewritten };
29451
+ } catch {
29452
+ return null;
29453
+ }
29454
+ }
29455
+
27634
29456
  // src/history/usage.ts
27635
29457
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
27636
29458
  var MAX_TARGET_LEN = 500;
@@ -27760,6 +29582,24 @@ async function reconcileSessionTail(config2, sessionId, transcriptPath) {
27760
29582
  offset: nextOffset,
27761
29583
  lastPromptId: result.lastPromptId
27762
29584
  });
29585
+ if (isVaultConsentValid(config2.settings.vaultConsent)) {
29586
+ try {
29587
+ const glue = createVaultGlue();
29588
+ const scrubbed = await scrubTranscriptTail(transcriptPath, {
29589
+ tokenizeText: (text) => glue.tokenizeText(text, {
29590
+ sighting: { location: transcriptPath, kind: "transcript" }
29591
+ }),
29592
+ scope: platformRedactionScope()
29593
+ });
29594
+ if (scrubbed !== null && scrubbed.rewritten > 0) {
29595
+ writeOffset(config2.dataDir, sessionId, {
29596
+ offset: 0,
29597
+ lastPromptId: result.lastPromptId
29598
+ });
29599
+ }
29600
+ } catch {
29601
+ }
29602
+ }
27763
29603
  return { llmCalls: result.llmCalls, skipped: result.skipped, toolCalls };
27764
29604
  } finally {
27765
29605
  await gateway.close();