@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.
@@ -492,7 +492,7 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/persistence/src/database.ts
495
- import { randomUUID as randomUUID8 } from "crypto";
495
+ import { randomUUID as randomUUID9 } from "crypto";
496
496
  import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
497
497
  import { join, sep } from "path";
498
498
  import { DatabaseSync } from "node:sqlite";
@@ -558,6 +558,22 @@ var SQLITE_MIGRATIONS = [
558
558
  {
559
559
  tag: "0014_drop_legacy_events_findings",
560
560
  sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
561
+ },
562
+ {
563
+ tag: "0015_busy_vengeance",
564
+ sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
565
+ },
566
+ {
567
+ tag: "0016_breezy_zodiak",
568
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
569
+ },
570
+ {
571
+ tag: "0017_rainy_kat_farrell",
572
+ sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
573
+ },
574
+ {
575
+ tag: "0018_serious_tana_nile",
576
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
561
577
  }
562
578
  ];
563
579
 
@@ -16214,6 +16230,7 @@ var ExceptionConditions = external_exports.object({
16214
16230
  sourceTool: external_exports.string().optional(),
16215
16231
  provider: external_exports.string().optional()
16216
16232
  }).strict();
16233
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
16217
16234
  var DetectionException = external_exports.object({
16218
16235
  id: external_exports.guid(),
16219
16236
  ruleId: external_exports.string(),
@@ -16230,6 +16247,7 @@ var DetectionException = external_exports.object({
16230
16247
  keyVersion: external_exports.number().int().positive(),
16231
16248
  // maskMatch() preview of the approved value — never the raw value.
16232
16249
  maskedValue: external_exports.string(),
16250
+ capability: ExceptionCapability.default("suppress"),
16233
16251
  scope: ExceptionScope,
16234
16252
  expiresAt: external_exports.iso.datetime().nullable(),
16235
16253
  maxUses: external_exports.number().int().positive().nullable(),
@@ -16253,6 +16271,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16253
16271
  ruleId: true,
16254
16272
  valueFingerprint: true,
16255
16273
  keyVersion: true,
16274
+ capability: true,
16256
16275
  expiresAt: true,
16257
16276
  maxUses: true,
16258
16277
  useCount: true,
@@ -17362,8 +17381,118 @@ var PatchInstalledPackRequest = external_exports.object({
17362
17381
  message: "At least one field must be provided"
17363
17382
  }).meta({ id: "PatchInstalledPackRequest" });
17364
17383
 
17384
+ // ../../packages/schema/src/zod/vault.ts
17385
+ var POINTER_FORMAT_VERSION = 2;
17386
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
17387
+ var POINTER_TOKEN_PATTERN = new RegExp(
17388
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
17389
+ );
17390
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
17391
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
17392
+ var ParsedPointer = external_exports.object({
17393
+ category: DetectionCategory,
17394
+ keyVersion: external_exports.number().int().positive(),
17395
+ pointerId: external_exports.string(),
17396
+ tag: external_exports.string()
17397
+ });
17398
+ var VaultEntry = external_exports.object({
17399
+ pointerId: external_exports.string(),
17400
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
17401
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
17402
+ // independently of the vault encryption key below.
17403
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17404
+ fingerprintKeyVersion: external_exports.number().int().positive(),
17405
+ // The vault-key epoch this row's ciphertext was sealed under.
17406
+ keyVersion: external_exports.number().int().positive(),
17407
+ // Fixed at first mint and never updated: the same value detected later under a
17408
+ // different rule's category keeps the category it was minted with, so one
17409
+ // value always produces exactly one wire token.
17410
+ category: DetectionCategory,
17411
+ ruleId: external_exports.string(),
17412
+ // Partial-reveal preview for badges and listings. Never the raw value.
17413
+ maskedMatch: external_exports.string(),
17414
+ provider: external_exports.string().optional(),
17415
+ ciphertext: external_exports.string(),
17416
+ nonce: external_exports.string(),
17417
+ authTag: external_exports.string(),
17418
+ // How many times this value has been detected on this machine — the reuse
17419
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
17420
+ occurrenceCount: external_exports.number().int().nonnegative(),
17421
+ firstSeen: external_exports.string(),
17422
+ lastSeen: external_exports.string()
17423
+ });
17424
+ var PointerDescriptor = external_exports.object({
17425
+ category: DetectionCategory,
17426
+ provider: external_exports.string().optional(),
17427
+ maskedMatch: external_exports.string(),
17428
+ occurrences: external_exports.number().int().nonnegative(),
17429
+ firstSeen: external_exports.string(),
17430
+ lastSeen: external_exports.string()
17431
+ });
17432
+ var PointerIdentity = external_exports.object({
17433
+ ruleId: external_exports.string(),
17434
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
17435
+ fingerprintKeyVersion: external_exports.number().int().positive()
17436
+ });
17437
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
17438
+ var VaultDerefReason = external_exports.enum([
17439
+ "display",
17440
+ "explicit-reveal",
17441
+ "view-render",
17442
+ "model-input",
17443
+ "remediation",
17444
+ "purge"
17445
+ ]);
17446
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
17447
+ var VaultDeref = external_exports.object({
17448
+ id: external_exports.guid(),
17449
+ pointerId: external_exports.string(),
17450
+ at: external_exports.string(),
17451
+ target: DetokenizeTarget,
17452
+ reason: VaultDerefReason,
17453
+ outcome: VaultDerefOutcome,
17454
+ // Present only on a model-target crossing that a reveal grant authorized.
17455
+ grantId: external_exports.string().optional(),
17456
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
17457
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
17458
+ pointerCount: external_exports.number().int().positive().default(1)
17459
+ });
17460
+ var VaultSightingKind = external_exports.enum([
17461
+ "prompt",
17462
+ "tool-input",
17463
+ "tool-output",
17464
+ "file",
17465
+ "transcript"
17466
+ ]);
17467
+ var VaultSighting = external_exports.object({
17468
+ location: external_exports.string(),
17469
+ kind: VaultSightingKind,
17470
+ firstSeen: external_exports.string(),
17471
+ lastSeen: external_exports.string()
17472
+ });
17473
+ var VaultInventoryEntry = external_exports.object({
17474
+ pointerId: external_exports.string(),
17475
+ category: DetectionCategory,
17476
+ provider: external_exports.string().optional(),
17477
+ maskedMatch: external_exports.string(),
17478
+ occurrences: external_exports.number().int().nonnegative(),
17479
+ firstSeen: external_exports.string(),
17480
+ lastSeen: external_exports.string(),
17481
+ // The active reveal-to-model grant covering this value, when one exists —
17482
+ // the inventory badges it, the row links to revocation.
17483
+ revealGrantId: external_exports.string().nullable(),
17484
+ sightings: external_exports.array(VaultSighting)
17485
+ });
17486
+ var VaultKeyCustody = external_exports.string();
17487
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
17488
+ var VAULT_CONSENT_VERSION = 1;
17489
+ var VaultConsent = external_exports.object({
17490
+ acknowledgedAt: external_exports.iso.datetime(),
17491
+ version: external_exports.number().int().positive()
17492
+ });
17493
+
17365
17494
  // ../../packages/schema/src/zod/local.ts
17366
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17495
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
17367
17496
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
17368
17497
  var RunMode = external_exports.enum(["standalone"]);
17369
17498
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
@@ -17386,6 +17515,16 @@ var WorkspaceSettings = external_exports.object({
17386
17515
  // In-place egress extraction on the scan paths; disable to stop all Data
17387
17516
  // Shares writes.
17388
17517
  dataSharesInPlace: external_exports.boolean().default(true),
17518
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
17519
+ // vault, instead of destroying them. Absent by default: this is a custody
17520
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
17521
+ // Revoking stops future vaulting; it does not erase what is already stored —
17522
+ // purging the vault is the eraser.
17523
+ vaultConsent: VaultConsent.optional(),
17524
+ // Where the vault master key lives.
17525
+ vaultKeyCustody: VaultKeyCustody.default("file"),
17526
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
17527
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
17389
17528
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17390
17529
  onboardedAt: external_exports.iso.datetime().optional(),
17391
17530
  // Records that the user consented to sending findings to the model API for
@@ -19789,6 +19928,9 @@ var AmbiguousExceptionIdError = class extends Error {
19789
19928
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19790
19929
  AND (expires_at IS NULL OR expires_at > :now)
19791
19930
  AND (max_uses IS NULL OR use_count < max_uses)`;
19931
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
19932
+ AND conditions IS NULL
19933
+ AND ${ACTIVE_PREDICATE}`;
19792
19934
  var SqliteExceptionsRepository = class {
19793
19935
  constructor(db) {
19794
19936
  this.db = db;
@@ -19880,11 +20022,11 @@ var SqliteExceptionsRepository = class {
19880
20022
  this.db.prepare(
19881
20023
  `INSERT INTO exceptions (
19882
20024
  id, rule_id, category, value_fingerprint, key_version, masked_value,
19883
- scope, expires_at, max_uses, use_count, last_used_at, justification,
19884
- conditions, created_by, created_via, created_at, updated_at
20025
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
20026
+ justification, conditions, created_by, created_via, created_at, updated_at
19885
20027
  ) VALUES (
19886
20028
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
19887
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
20029
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
19888
20030
  :conditions, :createdBy, :createdVia, :now, :now
19889
20031
  )`
19890
20032
  ).run({
@@ -19894,6 +20036,7 @@ var SqliteExceptionsRepository = class {
19894
20036
  valueFingerprint: input.valueFingerprint,
19895
20037
  keyVersion: input.keyVersion,
19896
20038
  maskedValue: input.maskedValue,
20039
+ capability: input.capability ?? "suppress",
19897
20040
  scope: input.scope,
19898
20041
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
19899
20042
  maxUses: input.maxUses,
@@ -19987,6 +20130,7 @@ var SqliteExceptionsRepository = class {
19987
20130
  ruleId: row.rule_id,
19988
20131
  valueFingerprint: row.value_fingerprint,
19989
20132
  keyVersion: row.key_version,
20133
+ capability: row.capability,
19990
20134
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19991
20135
  maxUses: row.max_uses,
19992
20136
  useCount: row.use_count,
@@ -20041,6 +20185,35 @@ var SqliteExceptionsRepository = class {
20041
20185
  }))
20042
20186
  );
20043
20187
  }
20188
+ /**
20189
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
20190
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
20191
+ * suppression uses — plus the capability: a suppression grant must never
20192
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
20193
+ * revealed value re-enters the detection scan immediately afterward and the
20194
+ * suppression match there claims the use — one crossing, one use.
20195
+ *
20196
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
20197
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
20198
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
20199
+ */
20200
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
20201
+ try {
20202
+ const row = getRow(
20203
+ this.db.prepare(
20204
+ `SELECT id FROM exceptions
20205
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
20206
+ AND key_version = :keyVersion
20207
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
20208
+ LIMIT 1`
20209
+ ),
20210
+ { ruleId, valueFingerprint, keyVersion, now }
20211
+ );
20212
+ return Promise.resolve(row ?? null);
20213
+ } catch (err) {
20214
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
20215
+ }
20216
+ }
20044
20217
  /**
20045
20218
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
20046
20219
  * exhausted) whose last transition is older than the retention window.
@@ -20068,6 +20241,7 @@ function parseExceptionRow(row) {
20068
20241
  valueFingerprint: row.value_fingerprint,
20069
20242
  keyVersion: row.key_version,
20070
20243
  maskedValue: row.masked_value,
20244
+ capability: row.capability,
20071
20245
  scope: row.scope,
20072
20246
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20073
20247
  maxUses: row.max_uses,
@@ -22233,6 +22407,287 @@ var SqliteScanLedgerRepository = class {
22233
22407
  }
22234
22408
  };
22235
22409
 
22410
+ // ../../packages/persistence/src/repositories/secret-vault.ts
22411
+ import { randomUUID as randomUUID7 } from "crypto";
22412
+ var SELECT_COLUMNS = `
22413
+ pointer_id AS pointerId,
22414
+ value_fingerprint AS valueFingerprint,
22415
+ fingerprint_key_version AS fingerprintKeyVersion,
22416
+ key_version AS keyVersion,
22417
+ format_version AS formatVersion,
22418
+ category,
22419
+ rule_id AS ruleId,
22420
+ masked_match AS maskedMatch,
22421
+ provider,
22422
+ ciphertext,
22423
+ nonce,
22424
+ auth_tag AS authTag,
22425
+ occurrence_count AS occurrenceCount,
22426
+ first_seen AS firstSeen,
22427
+ last_seen AS lastSeen`;
22428
+ function toRow(raw) {
22429
+ const { provider, ...rest } = raw;
22430
+ return provider === null ? rest : { ...rest, provider };
22431
+ }
22432
+ var SqliteSecretVaultRepository = class {
22433
+ constructor(db) {
22434
+ this.db = db;
22435
+ this.insertStmt = db.prepare(
22436
+ `INSERT INTO secret_vault (
22437
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
22438
+ format_version, category, rule_id, masked_match, provider,
22439
+ ciphertext, nonce, auth_tag,
22440
+ occurrence_count, first_seen, last_seen
22441
+ ) VALUES (
22442
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
22443
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
22444
+ :ciphertext, :nonce, :authTag,
22445
+ 1, :now, :now
22446
+ )`
22447
+ );
22448
+ this.bumpStmt = db.prepare(
22449
+ `UPDATE secret_vault
22450
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
22451
+ WHERE value_fingerprint = :valueFingerprint`
22452
+ );
22453
+ this.byPointerStmt = db.prepare(
22454
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
22455
+ );
22456
+ this.byFingerprintStmt = db.prepare(
22457
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
22458
+ );
22459
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
22460
+ this.replaceCiphertextStmt = db.prepare(
22461
+ `UPDATE secret_vault
22462
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
22463
+ WHERE pointer_id = :pointerId`
22464
+ );
22465
+ this.refreshFingerprintStmt = db.prepare(
22466
+ `UPDATE secret_vault
22467
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
22468
+ WHERE pointer_id = :pointerId`
22469
+ );
22470
+ this.derefStmt = db.prepare(
22471
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
22472
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
22473
+ );
22474
+ }
22475
+ db;
22476
+ insertStmt;
22477
+ bumpStmt;
22478
+ byPointerStmt;
22479
+ byFingerprintStmt;
22480
+ listStmt;
22481
+ replaceCiphertextStmt;
22482
+ refreshFingerprintStmt;
22483
+ derefStmt;
22484
+ /**
22485
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
22486
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
22487
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
22488
+ * pointer, category and ciphertext, so the same secret always resolves to one
22489
+ * wire token. `minted` is true only when this call created the row.
22490
+ *
22491
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
22492
+ * writers cannot both decide they are minting.
22493
+ */
22494
+ upsert(input, now) {
22495
+ let minted = false;
22496
+ withTransaction(
22497
+ this.db,
22498
+ () => {
22499
+ const existing = getRow(this.byFingerprintStmt, {
22500
+ valueFingerprint: input.valueFingerprint
22501
+ });
22502
+ if (existing === void 0) {
22503
+ this.insertStmt.run(
22504
+ bindParams({
22505
+ pointerId: input.pointerId,
22506
+ valueFingerprint: input.valueFingerprint,
22507
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
22508
+ keyVersion: input.keyVersion,
22509
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
22510
+ category: input.category,
22511
+ ruleId: input.ruleId,
22512
+ maskedMatch: input.maskedMatch,
22513
+ provider: input.provider,
22514
+ ciphertext: input.ciphertext,
22515
+ nonce: input.nonce,
22516
+ authTag: input.authTag,
22517
+ now
22518
+ })
22519
+ );
22520
+ minted = true;
22521
+ return;
22522
+ }
22523
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
22524
+ },
22525
+ "IMMEDIATE"
22526
+ );
22527
+ const row = getRow(this.byFingerprintStmt, {
22528
+ valueFingerprint: input.valueFingerprint
22529
+ });
22530
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
22531
+ return { row: toRow(row), minted };
22532
+ }
22533
+ byPointerId(pointerId) {
22534
+ const raw = getRow(this.byPointerStmt, { pointerId });
22535
+ return raw === void 0 ? null : toRow(raw);
22536
+ }
22537
+ byValueFingerprint(fingerprint) {
22538
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
22539
+ return raw === void 0 ? null : toRow(raw);
22540
+ }
22541
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
22542
+ recordDeref(entry) {
22543
+ this.derefStmt.run(
22544
+ bindParams({
22545
+ id: entry.id,
22546
+ pointerId: entry.pointerId,
22547
+ at: entry.at,
22548
+ target: entry.target,
22549
+ reason: entry.reason,
22550
+ outcome: entry.outcome,
22551
+ grantId: entry.grantId,
22552
+ pointerCount: entry.pointerCount ?? 1
22553
+ })
22554
+ );
22555
+ }
22556
+ listAll() {
22557
+ return allRows(this.listStmt).map(toRow);
22558
+ }
22559
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
22560
+ replaceCiphertext(pointerId, next) {
22561
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
22562
+ }
22563
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
22564
+ refreshFingerprint(pointerId, next) {
22565
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
22566
+ }
22567
+ /**
22568
+ * Destroy every vaulted value and report how many were destroyed. The deref
22569
+ * audit is left alone on purpose — see the table note above.
22570
+ */
22571
+ purgeAll() {
22572
+ let destroyed = 0;
22573
+ withTransaction(
22574
+ this.db,
22575
+ () => {
22576
+ destroyed = this.countEntries();
22577
+ this.db.exec("DELETE FROM secret_vault");
22578
+ },
22579
+ "IMMEDIATE"
22580
+ );
22581
+ return destroyed;
22582
+ }
22583
+ /**
22584
+ * Record (or re-stamp) one place a pointer has been written. One row per
22585
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
22586
+ * on hook paths — a failure must never affect the rewrite that triggered it,
22587
+ * so callers wrap this, not the other way around.
22588
+ */
22589
+ recordSighting(entry, now) {
22590
+ this.db.prepare(
22591
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
22592
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
22593
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
22594
+ ).run({
22595
+ id: randomUUID7(),
22596
+ pointerId: entry.pointerId,
22597
+ location: entry.location,
22598
+ kind: entry.kind,
22599
+ now
22600
+ });
22601
+ }
22602
+ listSightings(pointerId) {
22603
+ const rows = allRows(
22604
+ this.db.prepare(
22605
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
22606
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
22607
+ ),
22608
+ { pointerId }
22609
+ );
22610
+ return rows.map((r) => ({
22611
+ location: r.location,
22612
+ kind: r.kind,
22613
+ firstSeen: new Date(r.first_seen).toISOString(),
22614
+ lastSeen: new Date(r.last_seen).toISOString()
22615
+ }));
22616
+ }
22617
+ /**
22618
+ * The dashboard inventory: every vaulted value's descriptor data joined with
22619
+ * its sightings and the active reveal-to-model grant when one exists.
22620
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
22621
+ * columns are selected.
22622
+ */
22623
+ listInventory(now = Date.now()) {
22624
+ const rows = allRows(
22625
+ this.db.prepare(
22626
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
22627
+ v.occurrence_count, v.first_seen, v.last_seen,
22628
+ (SELECT e.id FROM exceptions e
22629
+ WHERE e.rule_id = v.rule_id
22630
+ AND e.value_fingerprint = v.value_fingerprint
22631
+ AND e.key_version = v.fingerprint_key_version
22632
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22633
+ LIMIT 1) AS grant_id
22634
+ FROM secret_vault v
22635
+ ORDER BY v.last_seen DESC`
22636
+ ),
22637
+ { now }
22638
+ );
22639
+ return rows.map((r) => ({
22640
+ pointerId: r.pointer_id,
22641
+ category: r.category,
22642
+ ...r.provider === null ? {} : { provider: r.provider },
22643
+ maskedMatch: r.masked_match,
22644
+ occurrences: r.occurrence_count,
22645
+ firstSeen: new Date(r.first_seen).toISOString(),
22646
+ lastSeen: new Date(r.last_seen).toISOString(),
22647
+ revealGrantId: r.grant_id,
22648
+ sightings: this.listSightings(r.pointer_id)
22649
+ }));
22650
+ }
22651
+ /**
22652
+ * The de-reference trail, newest first. By default the batched, high-volume
22653
+ * reasons (display, view-render) are hidden and counted instead — the rows
22654
+ * that matter as a signal are the model crossings, and burying them under
22655
+ * render noise would defeat the audit's purpose.
22656
+ */
22657
+ listDerefs(opts) {
22658
+ const limit = opts?.limit ?? 200;
22659
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
22660
+ const rows = allRows(
22661
+ this.db.prepare(
22662
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
22663
+ FROM secret_vault_deref ${where}
22664
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
22665
+ ),
22666
+ { limit }
22667
+ );
22668
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
22669
+ this.db,
22670
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
22671
+ );
22672
+ return {
22673
+ rows: rows.map((r) => ({
22674
+ id: r.id,
22675
+ pointerId: r.pointer_id,
22676
+ at: new Date(r.at).toISOString(),
22677
+ target: r.target,
22678
+ reason: r.reason,
22679
+ outcome: r.outcome,
22680
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
22681
+ pointerCount: r.pointer_count
22682
+ })),
22683
+ hiddenBatched
22684
+ };
22685
+ }
22686
+ countEntries() {
22687
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
22688
+ }
22689
+ };
22690
+
22236
22691
  // ../../packages/persistence/src/repositories/security.ts
22237
22692
  var DAY_MS4 = 864e5;
22238
22693
  var SEVERITIES = ["critical", "high", "medium", "low"];
@@ -22578,7 +23033,7 @@ var SqliteSecurityRepository = class {
22578
23033
  };
22579
23034
 
22580
23035
  // ../../packages/persistence/src/repositories/shares.ts
22581
- import { randomUUID as randomUUID7 } from "crypto";
23036
+ import { randomUUID as randomUUID8 } from "crypto";
22582
23037
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22583
23038
  var IN_CHUNK = 500;
22584
23039
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -22834,7 +23289,7 @@ var SqliteSharesRepository = class {
22834
23289
  (id, destination_id, host, decision, created_at, updated_at)
22835
23290
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22836
23291
  ).run({
22837
- id: randomUUID7(),
23292
+ id: randomUUID8(),
22838
23293
  destinationId,
22839
23294
  host: dest.host,
22840
23295
  decision,
@@ -22983,7 +23438,7 @@ var SqliteSharesRepository = class {
22983
23438
  let destinationId = destIds.get(hit.host);
22984
23439
  if (destinationId === void 0) {
22985
23440
  destStmt.run({
22986
- id: randomUUID7(),
23441
+ id: randomUUID8(),
22987
23442
  kind: hit.kind,
22988
23443
  name: hit.name,
22989
23444
  host: hit.host,
@@ -22999,7 +23454,7 @@ var SqliteSharesRepository = class {
22999
23454
  let endpointId = endpointIds.get(endpointKey);
23000
23455
  if (endpointId === void 0) {
23001
23456
  endpointStmt.run({
23002
- id: randomUUID7(),
23457
+ id: randomUUID8(),
23003
23458
  destinationId,
23004
23459
  method: hit.method,
23005
23460
  transport: hit.transport,
@@ -23012,7 +23467,7 @@ var SqliteSharesRepository = class {
23012
23467
  endpointIds.set(endpointKey, endpointId);
23013
23468
  }
23014
23469
  siteStmt.run({
23015
- id: randomUUID7(),
23470
+ id: randomUUID8(),
23016
23471
  endpointId,
23017
23472
  project: input.project,
23018
23473
  projectKey: input.projectKey,
@@ -23428,6 +23883,7 @@ function openAndInitialize(file2) {
23428
23883
  policies,
23429
23884
  installedPacks,
23430
23885
  scanLedger: new SqliteScanLedgerRepository(db),
23886
+ secretVault: new SqliteSecretVaultRepository(db),
23431
23887
  exceptions: new SqliteExceptionsRepository(db),
23432
23888
  resolutions: new SqliteResolutionsRepository(db),
23433
23889
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -23463,6 +23919,7 @@ function openLocalDatabase(dir) {
23463
23919
  policies,
23464
23920
  installedPacks,
23465
23921
  scanLedger,
23922
+ secretVault,
23466
23923
  exceptions,
23467
23924
  resolutions,
23468
23925
  ruleProbeCache,
@@ -23571,7 +24028,7 @@ function openLocalDatabase(dir) {
23571
24028
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23572
24029
  if (!definitionId) continue;
23573
24030
  inspectionFindings.insertFinding({
23574
- id: randomUUID8(),
24031
+ id: randomUUID9(),
23575
24032
  auditEventId: record2.scanEvent.id,
23576
24033
  inspectionDefinitionId: definitionId,
23577
24034
  span: finding.span,
@@ -23648,6 +24105,7 @@ function openLocalDatabase(dir) {
23648
24105
  policies,
23649
24106
  installedPacks,
23650
24107
  scanLedger,
24108
+ secretVault,
23651
24109
  exceptions,
23652
24110
  resolutions,
23653
24111
  ruleProbeCache,
@@ -23762,23 +24220,49 @@ function readJson(file2) {
23762
24220
  return parseJsonObject(text) ?? null;
23763
24221
  }
23764
24222
 
23765
- // ../../packages/persistence/src/warn-era-cap.ts
23766
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
24223
+ // ../../packages/persistence/src/vault/crypto.ts
24224
+ import {
24225
+ createCipheriv,
24226
+ createDecipheriv,
24227
+ createHmac as createHmac2,
24228
+ hkdfSync,
24229
+ timingSafeEqual
24230
+ } from "crypto";
24231
+
24232
+ // ../../packages/persistence/src/vault/key-provider.ts
24233
+ import { execFileSync } from "child_process";
24234
+ import { randomBytes as randomBytes2 } from "crypto";
24235
+ import {
24236
+ chmodSync as chmodSync2,
24237
+ mkdirSync as mkdirSync2,
24238
+ readFileSync as readFileSync3,
24239
+ renameSync as renameSync4,
24240
+ rmSync as rmSync3,
24241
+ statSync,
24242
+ writeFileSync as writeFileSync2
24243
+ } from "fs";
23767
24244
  import { join as join5 } from "path";
24245
+
24246
+ // ../../packages/persistence/src/vault/vault.ts
24247
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
24248
+
24249
+ // ../../packages/persistence/src/warn-era-cap.ts
24250
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
24251
+ import { join as join6 } from "path";
23768
24252
  var MARKER = "warn-era-capped";
23769
24253
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23770
24254
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
23771
- const marker = join5(dataDir2, MARKER);
24255
+ const marker = join6(dataDir2, MARKER);
23772
24256
  if (existsSync3(marker)) return { capped: 0, skipped: "already-run" };
23773
24257
  const capped = db.policies.capCategoryActions();
23774
- writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
24258
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23775
24259
  `, { mode: DATA_FILE_MODE });
23776
24260
  return { capped };
23777
24261
  }
23778
24262
 
23779
24263
  // ../../packages/plugin-sdk/src/config.ts
23780
24264
  import { existsSync as existsSync4 } from "fs";
23781
- import { join as join6 } from "path";
24265
+ import { join as join7 } from "path";
23782
24266
 
23783
24267
  // ../../packages/plugin-sdk/src/provider-env.ts
23784
24268
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -23832,7 +24316,7 @@ function resolveProvider() {
23832
24316
  function loadConfig(base = defaultDataDir()) {
23833
24317
  try {
23834
24318
  ensureLayoutDirSync(base);
23835
- const settingsFile = join6(settingsDir(base), "settings.json");
24319
+ const settingsFile = join7(settingsDir(base), "settings.json");
23836
24320
  if (existsSync4(settingsFile)) tightenFile(settingsFile);
23837
24321
  } catch {
23838
24322
  }
@@ -23856,9 +24340,9 @@ function resolveProviderSafe() {
23856
24340
  }
23857
24341
 
23858
24342
  // ../../packages/plugin-sdk/src/config-inventory.ts
23859
- import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
24343
+ import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync3 } from "fs";
23860
24344
  import { homedir as homedir2 } from "os";
23861
- import { basename as basename2, join as join8 } from "path";
24345
+ import { basename as basename2, join as join9 } from "path";
23862
24346
 
23863
24347
  // ../../packages/detections/src/egress/registry.ts
23864
24348
  var EXTRACTOR_VERSION = "1";
@@ -24569,18 +25053,18 @@ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].m
24569
25053
  );
24570
25054
 
24571
25055
  // ../../packages/plugin-sdk/src/repo.ts
24572
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
24573
- import { basename, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
25056
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
25057
+ import { basename, dirname, isAbsolute, join as join8, sep as sep2 } from "path";
24574
25058
 
24575
25059
  // ../../packages/plugin-sdk/src/events.ts
24576
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
25060
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
24577
25061
 
24578
25062
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
24579
25063
  import { arch, hostname as hostname3, platform, release } from "os";
24580
25064
 
24581
25065
  // ../../packages/plugin-sdk/src/nudge.ts
24582
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24583
- import { join as join9 } from "path";
25066
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
25067
+ import { join as join10 } from "path";
24584
25068
 
24585
25069
  // ../../packages/plugin-sdk/src/paths.ts
24586
25070
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -24598,18 +25082,18 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
24598
25082
 
24599
25083
  // ../../packages/plugin-sdk/src/project-files.ts
24600
25084
  var import_ignore = __toESM(require_ignore(), 1);
24601
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
24602
- import { basename as basename4, join as join10, relative, sep as sep4 } from "path";
25085
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
25086
+ import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
24603
25087
 
24604
25088
  // ../../packages/plugin-sdk/src/runtime.ts
24605
- import { randomUUID as randomUUID10 } from "crypto";
25089
+ import { randomUUID as randomUUID12 } from "crypto";
24606
25090
 
24607
25091
  // ../../packages/plugin-sdk/src/suppressions.ts
24608
25092
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
24609
25093
 
24610
25094
  // ../../packages/plugin-sdk/src/throttle.ts
24611
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24612
- import { join as join11 } from "path";
25095
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
25096
+ import { join as join12 } from "path";
24613
25097
 
24614
25098
  // src/onboard-posture.ts
24615
25099
  function parsePosture(json2) {
@@ -24737,12 +25221,29 @@ if (process.argv.includes("--model-judge-consent")) {
24737
25221
  payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
24738
25222
  };
24739
25223
  }
25224
+ var rawVaultConsent = flags.get("vault-consent");
25225
+ var vaultConsentAction;
25226
+ if (rawVaultConsent !== void 0) {
25227
+ if (rawVaultConsent === "grant" || rawVaultConsent === "revoke") {
25228
+ vaultConsentAction = rawVaultConsent;
25229
+ } else {
25230
+ fail(`invalid --vault-consent "${rawVaultConsent}" (expected grant or revoke)`);
25231
+ }
25232
+ }
25233
+ if (vaultConsentAction === "grant") {
25234
+ answers.vaultConsent = {
25235
+ acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString(),
25236
+ version: VAULT_CONSENT_VERSION
25237
+ };
25238
+ } else if (vaultConsentAction === "revoke") {
25239
+ answers.vaultConsent = void 0;
25240
+ }
24740
25241
  var rawPosture = flags.get("posture");
24741
25242
  var useFloor = process.argv.includes("--floor");
24742
25243
  var recalibrate = process.argv.includes("--recalibrate");
24743
25244
  if (useFloor && rawPosture !== void 0) fail("--floor and --posture are mutually exclusive");
24744
25245
  if (Object.keys(answers).length === 0 && rawPosture === void 0 && !useFloor) {
24745
- fail("nothing to save \u2014 pass --policy, --historical, --posture and/or --floor");
25246
+ fail("nothing to save \u2014 pass --policy, --historical, --vault-consent, --posture and/or --floor");
24746
25247
  }
24747
25248
  var wroteConsent = answers.modelJudgeConsent !== void 0;
24748
25249
  var wrotePosture = answers.policy !== void 0 || answers.historicalAccess !== void 0;
@@ -24757,6 +25258,17 @@ if (Object.keys(answers).length > 0) {
24757
25258
  show("Noted \u2014 I'll send findings to the model to rate them. You can revoke that anytime.")
24758
25259
  );
24759
25260
  }
25261
+ if (vaultConsentAction === "grant") {
25262
+ process.stdout.write(
25263
+ show("Okay \u2014 detected secrets will be kept recoverable in your local encrypted vault.")
25264
+ );
25265
+ } else if (vaultConsentAction === "revoke") {
25266
+ process.stdout.write(
25267
+ show(
25268
+ "Okay \u2014 new detections will no longer be vaulted. Anything already stored stays until you purge the vault."
25269
+ )
25270
+ );
25271
+ }
24760
25272
  if (wrotePosture) {
24761
25273
  try {
24762
25274
  const dataDir2 = loadConfig().dataDir;