@akasecurity/ai-tc-claude-code 0.9.9 → 0.9.10

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,11 +493,12 @@ var require_ignore = __commonJS({
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
495
  import { existsSync as existsSync7 } from "fs";
496
- import { join as join12 } from "path";
496
+ import { join as join13 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/attached-derived.ts
499
499
  import { rmSync } from "fs";
500
500
  import { join } from "path";
501
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
501
502
 
502
503
  // ../../packages/persistence/src/control-plane-credential.ts
503
504
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
@@ -596,6 +597,30 @@ var SQLITE_MIGRATIONS = [
596
597
  {
597
598
  tag: "0022_audit_inspection_ms",
598
599
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
600
+ },
601
+ {
602
+ tag: "0023_secret_vault_user_authorized",
603
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
604
+ },
605
+ {
606
+ tag: "0024_finding_resolution_key_created_index",
607
+ sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
608
+ },
609
+ {
610
+ tag: "0025_audit_capture_attribute_columns",
611
+ sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
612
+ },
613
+ {
614
+ tag: "0026_audit_llm_call_usage_columns",
615
+ sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
616
+ },
617
+ {
618
+ tag: "0027_audit_llm_usage_index",
619
+ sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
620
+ },
621
+ {
622
+ tag: "0028_activity_session_probe_indexes",
623
+ sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
599
624
  }
600
625
  ];
601
626
 
@@ -22134,6 +22159,26 @@ var AttachTokenResponse = external_exports.union([
22134
22159
  AttachTokenExpired,
22135
22160
  external_exports.object({ status: printable(64) })
22136
22161
  ]);
22162
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22163
+ var DeviceCommand = external_exports.object({
22164
+ id: printable(128).min(1),
22165
+ kind: DeviceCommandKind,
22166
+ issuedAt: printable(64).min(1),
22167
+ expiresAt: printable(64).min(1)
22168
+ }).strict();
22169
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22170
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22171
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22172
+ external_exports.object({
22173
+ outcome: external_exports.literal("reported"),
22174
+ projectsScanned: external_exports.number().int().nonnegative()
22175
+ }).strict(),
22176
+ external_exports.object({
22177
+ outcome: external_exports.literal("failed"),
22178
+ reason: DeviceCommandFailureReason,
22179
+ projectsScanned: external_exports.number().int().nonnegative()
22180
+ }).strict()
22181
+ ]);
22137
22182
 
22138
22183
  // ../../packages/schema/src/zod/registry.ts
22139
22184
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22300,7 +22345,7 @@ var PackManifest = external_exports.object({
22300
22345
  }).meta({ id: "PackManifest" });
22301
22346
 
22302
22347
  // ../../packages/schema/src/zod/detection.ts
22303
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22348
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22304
22349
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22305
22350
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22306
22351
  var DetectionCounts = external_exports.object({
@@ -22437,14 +22482,17 @@ function optional2(key, parsed, raw) {
22437
22482
  function isStringArray(value) {
22438
22483
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22439
22484
  }
22485
+ var ORIGIN_VALUES = { library: true, custom: true };
22486
+ function resolveOrigin(origin) {
22487
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22488
+ }
22440
22489
  function summaryToDetectionListItem(s) {
22441
22490
  return {
22442
22491
  id: `${s.namespace}/${s.packId}`,
22443
22492
  name: s.name,
22444
22493
  version: s.version,
22445
22494
  enabled: s.enabled,
22446
- origin: "library",
22447
- // v1: every installed pack is library origin
22495
+ origin: resolveOrigin(s.origin),
22448
22496
  namespace: s.namespace,
22449
22497
  packId: s.packId,
22450
22498
  ruleCount: s.ruleCount,
@@ -22496,7 +22544,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22496
22544
  name: row.name,
22497
22545
  version: row.version,
22498
22546
  enabled: row.enabled,
22499
- origin: "library",
22547
+ origin: resolveOrigin(row.origin),
22500
22548
  namespace: row.namespace,
22501
22549
  packId: row.packId,
22502
22550
  ruleCount: row.rules.length,
@@ -22516,16 +22564,20 @@ function splitDetectionId(id) {
22516
22564
  }
22517
22565
  function buildDetectionsList(summaries, query) {
22518
22566
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22567
+ const originOf = (s) => resolveOrigin(s.origin);
22519
22568
  const counts = {
22520
22569
  all: summaries.length,
22521
- library: summaries.length,
22522
- // all origin=library in v1
22523
- custom: 0,
22570
+ library: summaries.filter((s) => originOf(s) === "library").length,
22571
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22572
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22573
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22574
+ // place, and that state does not exist — editing a library pack forks it. See
22575
+ // OriginEnum.
22524
22576
  customized: 0,
22525
22577
  updates: withUpdate.length
22526
22578
  };
22527
22579
  const filter = query.filter;
22528
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22580
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22529
22581
  if (query.q) {
22530
22582
  const q = query.q.toLowerCase();
22531
22583
  filtered = filtered.filter(
@@ -22605,8 +22657,9 @@ var Event = external_exports.object({
22605
22657
  metadata: EventMetadata.optional()
22606
22658
  }).meta({ id: "Event" });
22607
22659
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22660
+ var INGEST_BATCH_MAX = 100;
22608
22661
  var IngestBatch = external_exports.object({
22609
- events: external_exports.array(IngestEvent).min(1).max(100),
22662
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22610
22663
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22611
22664
  // additionally rejects any event whose contentHash the store has already
22612
22665
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23162,6 +23215,252 @@ var PatchInstalledPackRequest = external_exports.object({
23162
23215
  message: "At least one field must be provided"
23163
23216
  }).meta({ id: "PatchInstalledPackRequest" });
23164
23217
 
23218
+ // ../../packages/schema/src/zod/policy.ts
23219
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23220
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23221
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23222
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23223
+ var Policy = external_exports.object({
23224
+ id: external_exports.guid(),
23225
+ scope: PolicyScope,
23226
+ target: PolicyTarget,
23227
+ action: ActionTaken,
23228
+ enabled: external_exports.boolean().default(true),
23229
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23230
+ // Display name — optional so older policy rows without name still parse.
23231
+ // Added for the findings API (policy.name column migration).
23232
+ name: external_exports.string().optional(),
23233
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23234
+ // which row this is. A producer that collapses several rows onto one target
23235
+ // must carry the marker onto whichever row survives, or the collapse decides
23236
+ // the answer; a survivor may therefore be a built-in expansion still marked
23237
+ // 'authored' because an authored sibling targeted the same thing.
23238
+ // Optional so an older producer — and an older on-disk cache — still parses;
23239
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23240
+ //
23241
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23242
+ // built-in archetype catalog entry a policy is, which every catalog surface
23243
+ // reads and which a caller may state. This one is a statement the PRODUCER
23244
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23245
+ // — the CRUD routes neither accept nor set it.
23246
+ //
23247
+ // A device consumes this in exactly one direction: an 'authored' policy
23248
+ // arriving from a control plane marks the rules it targets as not
23249
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23250
+ // which is what makes it safe to honour from an unsigned cache — the same
23251
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23252
+ provenance: PolicyProvenance.optional()
23253
+ }).meta({ id: "Policy" });
23254
+ var PolicyBundle = external_exports.object({
23255
+ version: external_exports.string(),
23256
+ policies: external_exports.array(Policy),
23257
+ // Rules from the installed marketplace packs (snapshotted by the
23258
+ // control plane). The plugin registers these in addition to its bundled
23259
+ // packs. Optional so older backends — and older on-disk caches — that omit
23260
+ // the field still parse; consumers read `bundle.rules ?? []`.
23261
+ rules: external_exports.array(Rule).optional(),
23262
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23263
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23264
+ // after reading the user's installed snapshot (installed_packs, enabled
23265
+ // packs only), which is how detection updates stay manual: new bundled
23266
+ // rules run only after the user applies the pack update. Absent/false keeps
23267
+ // the historical composition (bundled packs + rules) — older caches.
23268
+ rulesComplete: external_exports.boolean().optional(),
23269
+ // Active detection exceptions, evaluation subset only (see
23270
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23271
+ // on-disk caches — that omit the field still parse; consumers read
23272
+ // `bundle.exceptions ?? []`.
23273
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23274
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23275
+ // A second axis over the same `redact` action, carried beside the policies
23276
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23277
+ // widening Policy itself would change a persisted shape to express something
23278
+ // only the in-memory bundle needs. Optional so an older producer — or an
23279
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23280
+ // pre-existing one-way behaviour, which is the safe direction to default.
23281
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23282
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23283
+ // from a versioned installed pack. Optional so older backends — and older
23284
+ // on-disk caches — that omit the field still parse; consumers fall back to
23285
+ // the rule's own spec version. NOT the bundle version above — see
23286
+ // installedRuleset's ruleVersions for the source of truth.
23287
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23288
+ // Model ids (the raw `model` string a harness reports, e.g.
23289
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23290
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23291
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23292
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23293
+ // unenforced behaviour that predates this field and the safe direction to
23294
+ // default.
23295
+ //
23296
+ // Ids, not display names: the governance decision is keyed on the exact
23297
+ // string the harness reports (`model_status_override.versionId` in the
23298
+ // control plane), so no name resolution stands between the decision and the
23299
+ // comparison.
23300
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23301
+ customKeywords: external_exports.array(external_exports.string()),
23302
+ fetchedAt: external_exports.iso.datetime()
23303
+ }).meta({ id: "PolicyBundle" });
23304
+ var POLICY_BUNDLE_SHAPE_ID = [
23305
+ ...Object.keys(PolicyBundle.shape),
23306
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23307
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23308
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23309
+ ].sort().join(",");
23310
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23311
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23312
+ var CATEGORY_PEAK_SEVERITY = {
23313
+ secret: "critical",
23314
+ financial: "critical",
23315
+ // core-financial/credit-card
23316
+ code_flaw: "critical",
23317
+ pii: "high",
23318
+ phi: "high",
23319
+ custom: "high",
23320
+ // user-defined; conservative
23321
+ code_context: "low",
23322
+ config: "low"
23323
+ // observe-only; floors to monitor regardless
23324
+ };
23325
+ function severityFloorPolicy(category) {
23326
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23327
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23328
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23329
+ }
23330
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23331
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23332
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23333
+ id: "RedactFallback"
23334
+ });
23335
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23336
+ var BUILTIN_POLICY_SPECS = {
23337
+ monitor: {
23338
+ name: "Monitor",
23339
+ action: "log",
23340
+ reversible: false,
23341
+ description: "Log every match for audit. The request is allowed through untouched."
23342
+ },
23343
+ warn: {
23344
+ name: "Warn",
23345
+ action: "warn",
23346
+ reversible: false,
23347
+ description: "Allow the request, but warn the user inline before it is sent."
23348
+ },
23349
+ redact: {
23350
+ name: "Redact",
23351
+ action: "redact",
23352
+ reversible: false,
23353
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23354
+ },
23355
+ vault: {
23356
+ name: "Redact & Vault",
23357
+ action: "redact",
23358
+ reversible: true,
23359
+ description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
23360
+ },
23361
+ block: {
23362
+ name: "Block",
23363
+ action: "block",
23364
+ reversible: false,
23365
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23366
+ }
23367
+ };
23368
+ function builtinPolicyToAction(id) {
23369
+ return BUILTIN_POLICY_SPECS[id].action;
23370
+ }
23371
+ var PALETTE_WEAKEST_FIRST = [
23372
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23373
+ ];
23374
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23375
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23376
+ );
23377
+ var ACTION_STRENGTH_ORDER = [
23378
+ ...BELOW_PALETTE,
23379
+ ...PALETTE_WEAKEST_FIRST
23380
+ ];
23381
+ function actionRank(action) {
23382
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23383
+ }
23384
+ function isActionAtLeast(action, floor) {
23385
+ return actionRank(action) >= actionRank(floor);
23386
+ }
23387
+ function strongerAction(a, b) {
23388
+ return actionRank(a) >= actionRank(b) ? a : b;
23389
+ }
23390
+ function weakestBuiltinAtLeast(floor) {
23391
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23392
+ }
23393
+ var PackPolicyFloor = external_exports.object({
23394
+ /**
23395
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23396
+ * rather than a raw ActionTaken because that is the vocabulary the user
23397
+ * picks from — a floor a UI cannot name is one it cannot explain.
23398
+ */
23399
+ floor: BuiltinPolicyId,
23400
+ /**
23401
+ * True when the organization AUTHORED a policy governing this pack rather
23402
+ * than stating a minimum: it gave the answer, so the pack is not
23403
+ * re-assignable locally in either direction.
23404
+ */
23405
+ locked: external_exports.boolean()
23406
+ }).describe("PackPolicyFloor");
23407
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23408
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23409
+ );
23410
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23411
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23412
+ );
23413
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23414
+ function builtinPolicyIsReversible(id) {
23415
+ return BUILTIN_POLICY_SPECS[id].reversible;
23416
+ }
23417
+ function policyIdIsReversible(policyId) {
23418
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23419
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23420
+ return builtinPolicyIsReversible(id);
23421
+ }
23422
+ var DEFAULT_ACTIONS = Object.fromEntries(
23423
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23424
+ );
23425
+ var BUILTIN_POLICIES = Object.fromEntries(
23426
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23427
+ );
23428
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23429
+ function policyIdToAction(policyId) {
23430
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23431
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23432
+ return BUILTIN_POLICIES[id].action;
23433
+ }
23434
+ var UsedByItem = external_exports.object({
23435
+ id: external_exports.string(),
23436
+ name: external_exports.string(),
23437
+ ruleCount: external_exports.number().int().nonnegative(),
23438
+ enabled: external_exports.boolean()
23439
+ }).meta({ id: "UsedByItem" });
23440
+ var PolicyListItem = external_exports.object({
23441
+ id: external_exports.string(),
23442
+ kind: PolicyKind,
23443
+ name: external_exports.string(),
23444
+ enabled: external_exports.boolean(),
23445
+ usedByCount: external_exports.number().int().nonnegative()
23446
+ }).meta({ id: "PolicyListItem" });
23447
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23448
+ var PolicyDetail = external_exports.object({
23449
+ specVersion: external_exports.literal(1),
23450
+ id: external_exports.string(),
23451
+ kind: PolicyKind,
23452
+ name: external_exports.string(),
23453
+ enabled: external_exports.boolean(),
23454
+ description: external_exports.string(),
23455
+ usedBy: external_exports.array(UsedByItem)
23456
+ }).meta({ id: "PolicyDetail" });
23457
+ var PolicyStatsResponse = external_exports.object({
23458
+ policies: external_exports.number().int().nonnegative(),
23459
+ builtin: external_exports.number().int().nonnegative(),
23460
+ custom: external_exports.number().int().nonnegative(),
23461
+ detectionsGoverned: external_exports.number().int().nonnegative()
23462
+ }).meta({ id: "PolicyStatsResponse" });
23463
+
23165
23464
  // ../../packages/schema/src/zod/vault.ts
23166
23465
  var POINTER_FORMAT_VERSION = 2;
23167
23466
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23202,6 +23501,14 @@ var VaultEntry = external_exports.object({
23202
23501
  // How many times this value has been detected on this machine — the reuse
23203
23502
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23204
23503
  occurrenceCount: external_exports.number().int().nonnegative(),
23504
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23505
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23506
+ // one row however many paths vault it, so this is what tells a policy sweep
23507
+ // that the row carries somebody's own instruction and not just an assignment
23508
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23509
+ // vaulting of the same value must never clear it — what the user said about
23510
+ // the value does not expire.
23511
+ userAuthorized: external_exports.boolean(),
23205
23512
  firstSeen: external_exports.string(),
23206
23513
  lastSeen: external_exports.string()
23207
23514
  });
@@ -23328,7 +23635,7 @@ function isVaultConsentValid(consent) {
23328
23635
  }
23329
23636
 
23330
23637
  // ../../packages/schema/src/zod/local.ts
23331
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23638
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23332
23639
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23333
23640
  var RunMode = external_exports.enum(["standalone", "attached"]);
23334
23641
  var ControlPlaneConnection = external_exports.object({
@@ -23370,6 +23677,19 @@ var WorkspaceSettings = external_exports.object({
23370
23677
  vaultKeyCustody: VaultKeyCustody.default("file"),
23371
23678
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23372
23679
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23680
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23681
+ // place. Not a handling policy: the policy has already resolved to redact,
23682
+ // and this only says what happens when the host offers no channel to carry it
23683
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23684
+ // Claude Code decline to mask a field that EXECUTES because masking would
23685
+ // change what runs. Per FIELD rather than per host, so a host that can
23686
+ // rewrite some inputs keeps true redaction on those.
23687
+ //
23688
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23689
+ // an attached machine's merge is `strongerAction` over the one action ladder
23690
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23691
+ // word and stays out of the stored value.
23692
+ redactFallback: RedactFallback.default("warn"),
23373
23693
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23374
23694
  onboardedAt: external_exports.iso.datetime().optional(),
23375
23695
  // Records that the user consented to sending findings to the model API for
@@ -23377,15 +23697,20 @@ var WorkspaceSettings = external_exports.object({
23377
23697
  // Absent until granted; a stale payloadVersion means the consent no longer
23378
23698
  // covers the current payload and must be re-granted.
23379
23699
  modelJudgeConsent: ModelJudgeConsent.optional(),
23380
- // Records that the user consented to sending the activity already recorded on
23381
- // this machine to the deployment it is attached to, along with the payload
23382
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23383
- // a different endpoint or an older payload no longer counts.
23700
+ // Records that the user consented to the DEFERRED send — the outbox along
23701
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23702
+ // that covers both the pre-attach backlog and undelivered captures (which
23703
+ // carry prompt/reply text in `content`); the key name predates the widening.
23704
+ // Absent until granted, and a grant for a different endpoint or an older
23705
+ // payload no longer counts.
23384
23706
  historySyncConsent: HistorySyncConsent.optional()
23385
23707
  });
23386
23708
  function defaultWorkspaceSettings() {
23387
23709
  return WorkspaceSettings.parse({});
23388
23710
  }
23711
+ function isAttached(settings) {
23712
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23713
+ }
23389
23714
  function toInventoryRow(input2, id, now) {
23390
23715
  return {
23391
23716
  id,
@@ -23505,7 +23830,8 @@ var ManagedSettingKey = external_exports.enum([
23505
23830
  "vaultKeyCustody",
23506
23831
  "vaultInlineReveal",
23507
23832
  "modelJudgeConsent",
23508
- "dataSharesInPlace"
23833
+ "dataSharesInPlace",
23834
+ "redactFallback"
23509
23835
  ]).meta({ id: "ManagedSettingKey" });
23510
23836
  var ManagedSettingsValues = external_exports.object({
23511
23837
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
@@ -23518,7 +23844,8 @@ var ManagedSettingsValues = external_exports.object({
23518
23844
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23519
23845
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23520
23846
  modelJudgeConsent: external_exports.boolean().optional(),
23521
- dataSharesInPlace: external_exports.boolean().optional()
23847
+ dataSharesInPlace: external_exports.boolean().optional(),
23848
+ redactFallback: RedactFallback.optional()
23522
23849
  }).meta({ id: "ManagedSettingsValues" });
23523
23850
  var ManagedSettings = external_exports.object({
23524
23851
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23533,186 +23860,6 @@ var ManagedSettings = external_exports.object({
23533
23860
  lockedFields: external_exports.array(ManagedSettingKey).default([])
23534
23861
  }).meta({ id: "ManagedSettings" });
23535
23862
 
23536
- // ../../packages/schema/src/zod/policy.ts
23537
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23538
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23539
- var Policy = external_exports.object({
23540
- id: external_exports.guid(),
23541
- scope: PolicyScope,
23542
- target: PolicyTarget,
23543
- action: ActionTaken,
23544
- enabled: external_exports.boolean().default(true),
23545
- customKeywords: external_exports.array(external_exports.string()).optional(),
23546
- // Display name — optional so older policy rows without name still parse.
23547
- // Added for the findings API (policy.name column migration).
23548
- name: external_exports.string().optional()
23549
- }).meta({ id: "Policy" });
23550
- var PolicyBundle = external_exports.object({
23551
- version: external_exports.string(),
23552
- policies: external_exports.array(Policy),
23553
- // Rules from the installed marketplace packs (snapshotted by the
23554
- // control plane). The plugin registers these in addition to its bundled
23555
- // packs. Optional so older backends — and older on-disk caches — that omit
23556
- // the field still parse; consumers read `bundle.rules ?? []`.
23557
- rules: external_exports.array(Rule).optional(),
23558
- // When true, `rules` IS the complete effective ruleset and the runtime must
23559
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23560
- // after reading the user's installed snapshot (installed_packs, enabled
23561
- // packs only), which is how detection updates stay manual: new bundled
23562
- // rules run only after the user applies the pack update. Absent/false keeps
23563
- // the historical composition (bundled packs + rules) — older caches.
23564
- rulesComplete: external_exports.boolean().optional(),
23565
- // Active detection exceptions, evaluation subset only (see
23566
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23567
- // on-disk caches — that omit the field still parse; consumers read
23568
- // `bundle.exceptions ?? []`.
23569
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23570
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23571
- // A second axis over the same `redact` action, carried beside the policies
23572
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23573
- // widening Policy itself would change a persisted shape to express something
23574
- // only the in-memory bundle needs. Optional so an older producer — or an
23575
- // older on-disk cache — still parses; consumers read `?? []` and get the
23576
- // pre-existing one-way behaviour, which is the safe direction to default.
23577
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23578
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23579
- // from a versioned installed pack. Optional so older backends — and older
23580
- // on-disk caches — that omit the field still parse; consumers fall back to
23581
- // the rule's own spec version. NOT the bundle version above — see
23582
- // installedRuleset's ruleVersions for the source of truth.
23583
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23584
- // Model ids (the raw `model` string a harness reports, e.g.
23585
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23586
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23587
- // one (UserPromptSubmit). Optional so an older backend — and an older
23588
- // on-disk cache — still parses; consumers read `?? []`, which is the
23589
- // unenforced behaviour that predates this field and the safe direction to
23590
- // default.
23591
- //
23592
- // Ids, not display names: the governance decision is keyed on the exact
23593
- // string the harness reports (`model_status_override.versionId` in the
23594
- // control plane), so no name resolution stands between the decision and the
23595
- // comparison.
23596
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23597
- customKeywords: external_exports.array(external_exports.string()),
23598
- fetchedAt: external_exports.iso.datetime()
23599
- }).meta({ id: "PolicyBundle" });
23600
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23601
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23602
- var CATEGORY_PEAK_SEVERITY = {
23603
- secret: "critical",
23604
- financial: "critical",
23605
- // core-financial/credit-card
23606
- code_flaw: "critical",
23607
- pii: "high",
23608
- phi: "high",
23609
- custom: "high",
23610
- // user-defined; conservative
23611
- code_context: "low",
23612
- config: "low"
23613
- // observe-only; floors to monitor regardless
23614
- };
23615
- function severityFloorPolicy(category) {
23616
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23617
- const peak = CATEGORY_PEAK_SEVERITY[category];
23618
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23619
- }
23620
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23621
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23622
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23623
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23624
- var BUILTIN_POLICY_SPECS = {
23625
- monitor: {
23626
- name: "Monitor",
23627
- action: "log",
23628
- reversible: false,
23629
- description: "Log every match for audit. The request is allowed through untouched."
23630
- },
23631
- warn: {
23632
- name: "Warn",
23633
- action: "warn",
23634
- reversible: false,
23635
- description: "Allow the request, but warn the user inline before it is sent."
23636
- },
23637
- redact: {
23638
- name: "Redact",
23639
- action: "redact",
23640
- reversible: false,
23641
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23642
- },
23643
- vault: {
23644
- name: "Redact & Vault",
23645
- action: "redact",
23646
- reversible: true,
23647
- description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
23648
- },
23649
- block: {
23650
- name: "Block",
23651
- action: "block",
23652
- reversible: false,
23653
- description: "Refuse the request entirely whenever any rule in this detection matches."
23654
- }
23655
- };
23656
- function builtinPolicyToAction(id) {
23657
- return BUILTIN_POLICY_SPECS[id].action;
23658
- }
23659
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23660
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23661
- );
23662
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23663
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23664
- );
23665
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23666
- function builtinPolicyIsReversible(id) {
23667
- return BUILTIN_POLICY_SPECS[id].reversible;
23668
- }
23669
- function policyIdIsReversible(policyId) {
23670
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23671
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23672
- return builtinPolicyIsReversible(id);
23673
- }
23674
- var DEFAULT_ACTIONS = Object.fromEntries(
23675
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23676
- );
23677
- var BUILTIN_POLICIES = Object.fromEntries(
23678
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23679
- );
23680
- var DEFAULT_PACK_POLICY_ID = "monitor";
23681
- function policyIdToAction(policyId) {
23682
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23683
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23684
- return BUILTIN_POLICIES[id].action;
23685
- }
23686
- var UsedByItem = external_exports.object({
23687
- id: external_exports.string(),
23688
- name: external_exports.string(),
23689
- ruleCount: external_exports.number().int().nonnegative(),
23690
- enabled: external_exports.boolean()
23691
- }).meta({ id: "UsedByItem" });
23692
- var PolicyListItem = external_exports.object({
23693
- id: external_exports.string(),
23694
- kind: PolicyKind,
23695
- name: external_exports.string(),
23696
- enabled: external_exports.boolean(),
23697
- usedByCount: external_exports.number().int().nonnegative()
23698
- }).meta({ id: "PolicyListItem" });
23699
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23700
- var PolicyDetail = external_exports.object({
23701
- specVersion: external_exports.literal(1),
23702
- id: external_exports.string(),
23703
- kind: PolicyKind,
23704
- name: external_exports.string(),
23705
- enabled: external_exports.boolean(),
23706
- description: external_exports.string(),
23707
- usedBy: external_exports.array(UsedByItem)
23708
- }).meta({ id: "PolicyDetail" });
23709
- var PolicyStatsResponse = external_exports.object({
23710
- policies: external_exports.number().int().nonnegative(),
23711
- builtin: external_exports.number().int().nonnegative(),
23712
- custom: external_exports.number().int().nonnegative(),
23713
- detectionsGoverned: external_exports.number().int().nonnegative()
23714
- }).meta({ id: "PolicyStatsResponse" });
23715
-
23716
23863
  // ../../packages/schema/src/zod/project-files.ts
23717
23864
  var ProjectFileInput = external_exports.object({
23718
23865
  path: external_exports.string().min(1),
@@ -23958,10 +24105,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23958
24105
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23959
24106
 
23960
24107
  // ../../packages/schema/src/zod/settings-action.ts
24108
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24109
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23961
24110
  var SaveSettingsInput = external_exports.object({
23962
24111
  historicalAccess: external_exports.string(),
23963
- modelJudgeConsent: external_exports.boolean(),
23964
- historySyncConsent: external_exports.boolean(),
24112
+ modelJudgeConsent: ModelJudgeConsentChoice,
24113
+ historySyncConsent: HistorySyncConsentChoice,
23965
24114
  vaultConsent: external_exports.string(),
23966
24115
  vaultInlineReveal: external_exports.string()
23967
24116
  });
@@ -24111,9 +24260,9 @@ function deriveReviewReasons(trust, transports) {
24111
24260
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24112
24261
  return reasons;
24113
24262
  }
24114
- function buildReviewInfo(trust, transports) {
24263
+ function buildReviewInfo(trust, transports, decided) {
24115
24264
  const reasons = deriveReviewReasons(trust, transports);
24116
- return { needsReview: reasons.length > 0, reasons };
24265
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24117
24266
  }
24118
24267
  function distinctTransports(transports) {
24119
24268
  return Array.from(new Set(transports));
@@ -24261,8 +24410,8 @@ function publishByLink(tmp, file2, data) {
24261
24410
  }
24262
24411
 
24263
24412
  // ../../packages/persistence/src/database.ts
24264
- import { randomUUID as randomUUID10 } from "crypto";
24265
- import { join as join4, sep } from "path";
24413
+ import { randomUUID as randomUUID11 } from "crypto";
24414
+ import { dirname as dirname2, join as join7, sep } from "path";
24266
24415
  import { DatabaseSync } from "node:sqlite";
24267
24416
 
24268
24417
  // ../../packages/persistence/src/ids.ts
@@ -24506,6 +24655,10 @@ function allRows(stmt, params) {
24506
24655
  if (Array.isArray(params)) return stmt.all(...params);
24507
24656
  return stmt.all(params);
24508
24657
  }
24658
+ function* iterateRows(stmt, params) {
24659
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24660
+ for (const row of rows) yield row;
24661
+ }
24509
24662
  function getRow(stmt, params) {
24510
24663
  if (params === void 0) return stmt.get();
24511
24664
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24974,10 +25127,17 @@ function ensureSyncedAtColumn(db, table) {
24974
25127
  if (!columns.includes("sync_claimed_at")) {
24975
25128
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
24976
25129
  }
25130
+ if (!columns.includes("outbox_owed")) {
25131
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25132
+ }
24977
25133
  db.exec(
24978
25134
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24979
25135
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24980
25136
  );
25137
+ db.exec(
25138
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25139
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25140
+ );
24981
25141
  db.exec(
24982
25142
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24983
25143
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25082,7 +25242,6 @@ function decodeKeysetCursor(cursor) {
25082
25242
  // ../../packages/persistence/src/repositories/activity.ts
25083
25243
  var DAY_MS = 864e5;
25084
25244
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25085
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25086
25245
  function defaultTimeZone() {
25087
25246
  try {
25088
25247
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25137,6 +25296,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25137
25296
  error: "error",
25138
25297
  active: "active"
25139
25298
  };
25299
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25140
25300
  function safeParseStringArray(raw) {
25141
25301
  if (!raw) return [];
25142
25302
  const parsed = safeJson(raw, null);
@@ -25210,6 +25370,37 @@ var TIMELINE_COLUMNS = `
25210
25370
  json_extract(attributes, '$.targetId') AS target_id,
25211
25371
  json_extract(attributes, '$.internal') AS internal,
25212
25372
  json_extract(attributes, '$.flagged') AS flagged`;
25373
+ var LLM_USAGE_SELECT = `
25374
+ SELECT root_session_id AS sessionId,
25375
+ provider,
25376
+ model,
25377
+ service_tier AS serviceTier,
25378
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25379
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25380
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25381
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25382
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25383
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25384
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25385
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25386
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25387
+ function usageLeaves(rows) {
25388
+ return rows.map((row) => {
25389
+ const attributes = {
25390
+ input_tokens: row.inputTokens,
25391
+ output_tokens: row.outputTokens,
25392
+ cache_creation_input_tokens: row.cacheCreationTokens,
25393
+ cache_read_input_tokens: row.cacheReadTokens,
25394
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25395
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25396
+ web_search_requests: row.webSearchRequests
25397
+ };
25398
+ if (row.provider !== null) attributes.provider = row.provider;
25399
+ if (row.model !== null) attributes.model = row.model;
25400
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25401
+ return { sessionId: row.sessionId, attributes };
25402
+ });
25403
+ }
25213
25404
  var SESSION_ROOT = `event_type = 'session'`;
25214
25405
  var HAS_ACTIVITY = `EXISTS (
25215
25406
  SELECT 1 FROM audit_events c
@@ -25235,16 +25426,17 @@ var SqliteActivityRepository = class {
25235
25426
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25236
25427
  const liveNow = countScalar(
25237
25428
  this.db,
25238
- `SELECT count(*) AS n FROM audit_events s
25429
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25239
25430
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25240
- AND max(
25241
- s.started_at,
25242
- coalesce(
25243
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25244
- s.started_at
25245
- )
25246
- ) >= ?`,
25247
- [liveThreshold]
25431
+ AND s.id IN (
25432
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25433
+ UNION
25434
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25435
+ WHERE started_at >= ?
25436
+ UNION
25437
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25438
+ WHERE ended_at >= ?)`,
25439
+ [liveThreshold, liveThreshold, liveThreshold]
25248
25440
  );
25249
25441
  const toolCallsToday = countScalar(
25250
25442
  this.db,
@@ -25374,7 +25566,7 @@ var SqliteActivityRepository = class {
25374
25566
  this.db.prepare(
25375
25567
  `SELECT ${TIMELINE_COLUMNS}
25376
25568
  FROM audit_events
25377
- WHERE id = ? OR root_session_id = ?
25569
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25378
25570
  ORDER BY started_at ASC, id ASC`
25379
25571
  ),
25380
25572
  [sessionId, sessionId]
@@ -25387,14 +25579,14 @@ var SqliteActivityRepository = class {
25387
25579
  coalesce(sum(output_tokens), 0) AS output,
25388
25580
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25389
25581
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25390
- FROM audit_events
25582
+ FROM audit_events INDEXED BY idx_audit_session_type
25391
25583
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25392
25584
  ),
25393
25585
  [sessionId]
25394
25586
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25395
25587
  const primaryModel = getRow(
25396
25588
  this.db.prepare(
25397
- `SELECT model, provider FROM audit_events
25589
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25398
25590
  WHERE root_session_id = ? AND event_type = 'llm_call'
25399
25591
  ORDER BY started_at ASC, id ASC
25400
25592
  LIMIT 1`
@@ -25405,7 +25597,7 @@ var SqliteActivityRepository = class {
25405
25597
  this.db.prepare(
25406
25598
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25407
25599
  count(*) AS n
25408
- FROM audit_events
25600
+ FROM audit_events INDEXED BY idx_audit_session
25409
25601
  WHERE root_session_id = ? AND event_type = 'tool_call'
25410
25602
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25411
25603
  ),
@@ -25413,7 +25605,7 @@ var SqliteActivityRepository = class {
25413
25605
  );
25414
25606
  const modelRows = allRows(
25415
25607
  this.db.prepare(
25416
- `SELECT DISTINCT model FROM audit_events
25608
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25417
25609
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25418
25610
  ORDER BY model`
25419
25611
  ),
@@ -25422,7 +25614,7 @@ var SqliteActivityRepository = class {
25422
25614
  const derivedModels = modelRows.map((r) => r.model);
25423
25615
  const commits = countScalar(
25424
25616
  this.db,
25425
- `SELECT count(*) AS n FROM audit_events
25617
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25426
25618
  WHERE root_session_id = ? AND event_type = 'commit'`,
25427
25619
  [sessionId]
25428
25620
  );
@@ -25458,25 +25650,57 @@ var SqliteActivityRepository = class {
25458
25650
  return Promise.resolve(session);
25459
25651
  }
25460
25652
  /**
25461
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25462
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25463
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25464
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25465
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25466
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25653
+ * Cross-session token report — every `llm_call` in the store (or in a
25654
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25655
+ * session, with USD cost DERIVED at read time via the shared
25656
+ * `defaultCostModel` (never stored). The caller collapses these onto
25657
+ * per-model rows with `aggregateTokenUsage`.
25658
+ *
25659
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25660
+ * the members the rollup sums — and priced once per group, which is exact
25661
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25662
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25663
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25664
+ * the index stores the values once, at write, and answers the same window in
25665
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25666
+ * planner prefers the general event-type index and fetches every row to
25667
+ * recompute the columns it could have read. The index is one every open
25668
+ * store carries, since opening runs the migrations, so the hard requirement
25669
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25670
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25671
+ * per call, no bag parsed.
25467
25672
  */
25468
25673
  tokenReports(fromMs) {
25469
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25470
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25674
+ const rows = allRows(
25675
+ this.db.prepare(
25676
+ `${LLM_USAGE_SELECT}
25677
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25678
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25679
+ ${LLM_USAGE_GROUP}`
25680
+ ),
25681
+ fromMs === void 0 ? void 0 : [fromMs]
25682
+ );
25683
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25471
25684
  }
25472
25685
  /**
25473
- * One session's token report — its `llm_call` leaves grouped per (provider,
25474
- * model) with derived cost, or `null` when the session made no `llm_call`s
25475
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25476
- * breakdown + estimated cost.
25686
+ * One session's token report — its `llm_call`s grouped per (provider,
25687
+ * model, tier) with derived cost, or `null` when the session made no
25688
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25689
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25690
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25691
+ * it replaces walked every `llm_call` in the store to find one session's.
25477
25692
  */
25478
25693
  tokenReportForSession(sessionId) {
25479
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25694
+ const rows = allRows(
25695
+ this.db.prepare(
25696
+ `${LLM_USAGE_SELECT}
25697
+ FROM audit_events
25698
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25699
+ ${LLM_USAGE_GROUP}`
25700
+ ),
25701
+ [sessionId]
25702
+ );
25703
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25480
25704
  return Promise.resolve(reports[0] ?? null);
25481
25705
  }
25482
25706
  /**
@@ -25500,42 +25724,6 @@ var SqliteActivityRepository = class {
25500
25724
  for (const row of rows) seen.add(toHarness(row.harness));
25501
25725
  return Promise.resolve([...seen]);
25502
25726
  }
25503
- /**
25504
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25505
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25506
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25507
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25508
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25509
- */
25510
- readLlmCallLeaves(opts = {}) {
25511
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25512
- const params = [];
25513
- if (opts.sessionId !== void 0) {
25514
- conditions.push("root_session_id = ?");
25515
- params.push(opts.sessionId);
25516
- }
25517
- if (opts.fromMs !== void 0) {
25518
- conditions.push("started_at >= ?");
25519
- params.push(opts.fromMs);
25520
- }
25521
- const rows = allRows(
25522
- this.db.prepare(
25523
- `SELECT root_session_id AS sessionId, attributes
25524
- FROM audit_events
25525
- WHERE ${conditions.join(" AND ")}`
25526
- ),
25527
- params
25528
- );
25529
- return mapRowsTolerant(
25530
- rows.filter(
25531
- (row) => row.sessionId !== null
25532
- ),
25533
- (row) => ({
25534
- sessionId: row.sessionId,
25535
- attributes: JSON.parse(row.attributes)
25536
- })
25537
- );
25538
- }
25539
25727
  /**
25540
25728
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25541
25729
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25550,20 +25738,23 @@ var SqliteActivityRepository = class {
25550
25738
  const inClause = placeholders(sessionIds.length);
25551
25739
  const lastActivityRows = allRows(
25552
25740
  this.db.prepare(
25553
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25554
- WHERE root_session_id IN (${inClause})
25555
- GROUP BY root_session_id`
25741
+ `SELECT ids.value AS id,
25742
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25743
+ (SELECT max(ended_at) FROM audit_events e
25744
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25745
+ FROM json_each(?) AS ids`
25556
25746
  ),
25557
- sessionIds
25747
+ [JSON.stringify(sessionIds)]
25558
25748
  );
25559
25749
  for (const row of lastActivityRows) {
25560
- if (row.id === null) continue;
25561
25750
  const entry = result.get(row.id);
25562
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25751
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25752
+ if (entry && last > 0) entry.lastActivityMs = last;
25563
25753
  }
25564
25754
  const turnsRows = allRows(
25565
25755
  this.db.prepare(
25566
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25756
+ `SELECT root_session_id AS id, count(*) AS n
25757
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25567
25758
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25568
25759
  GROUP BY root_session_id`
25569
25760
  ),
@@ -25578,7 +25769,7 @@ var SqliteActivityRepository = class {
25578
25769
  this.db.prepare(
25579
25770
  `SELECT root_session_id AS id,
25580
25771
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25581
- FROM audit_events
25772
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25582
25773
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25583
25774
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25584
25775
  GROUP BY root_session_id`
@@ -25608,7 +25799,7 @@ var SqliteActivityRepository = class {
25608
25799
  this.db.prepare(
25609
25800
  `SELECT root_session_id AS id,
25610
25801
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25611
- FROM audit_events
25802
+ FROM audit_events INDEXED BY idx_audit_session_share
25612
25803
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25613
25804
  GROUP BY root_session_id`
25614
25805
  ),
@@ -26637,7 +26828,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26637
26828
 
26638
26829
  // ../../packages/persistence/src/repositories/findings.ts
26639
26830
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26640
- var SCAN_BATCH_ROWS = 1e3;
26641
26831
  var DEFAULT_LOCATIONS_LIMIT = 100;
26642
26832
  var LOCATION_RULE_IDS_CAP = 20;
26643
26833
  function compareLocationOrder(a, b) {
@@ -26666,6 +26856,25 @@ function deriveInstanceStatus(row) {
26666
26856
  latestResolutionStatus: row.latest_status
26667
26857
  });
26668
26858
  }
26859
+ function toFlatFindingRow(r) {
26860
+ return {
26861
+ id: r.id,
26862
+ ruleId: r.rule_id,
26863
+ category: r.category,
26864
+ severity: r.severity,
26865
+ maskedMatch: r.masked_match,
26866
+ actionTaken: r.action_taken,
26867
+ confidence: r.confidence,
26868
+ occurredAt: epochMillisToIso(r.occurred_at),
26869
+ sourceTool: r.source_tool,
26870
+ repo: r.repo ?? "",
26871
+ file: r.file ?? "",
26872
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26873
+ eventId: r.event_id,
26874
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26875
+ status: deriveInstanceStatus(r)
26876
+ };
26877
+ }
26669
26878
  function encodeGroupCursor(group) {
26670
26879
  const payload = {
26671
26880
  sev: group.severity,
@@ -26741,7 +26950,7 @@ var SqliteFindingsRepository = class {
26741
26950
  this.db.prepare(
26742
26951
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26743
26952
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26744
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26953
+ e.source_tool AS source_tool,
26745
26954
  e.event_type AS kind
26746
26955
  FROM audit_events e
26747
26956
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26849,56 +27058,11 @@ var SqliteFindingsRepository = class {
26849
27058
  predicate,
26850
27059
  params: sessionParams
26851
27060
  });
26852
- const rows = allRows(
26853
- this.db.prepare(
26854
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26855
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26856
- kind, finding_key, latest_status
26857
- FROM (
26858
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26859
- d.severity AS severity, f.masked_match AS masked_match,
26860
- f.action_taken AS action_taken, f.confidence AS confidence,
26861
- e.started_at AS occurred_at,
26862
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26863
- json_extract(e.attributes, '$.repo') AS repo,
26864
- json_extract(e.attributes, '$.file_path') AS file,
26865
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26866
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26867
- e.event_type AS kind, f.finding_key AS finding_key,
26868
- latest.status AS latest_status,
26869
- ROW_NUMBER() OVER (
26870
- PARTITION BY d.rule_id
26871
- ORDER BY e.started_at DESC, f.id DESC
26872
- ) AS rn
26873
- FROM inspection_findings f
26874
- JOIN audit_events e ON e.id = f.audit_event_id
26875
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26876
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26877
- ON latest.finding_key = f.finding_key
26878
- ${predicate}
26879
- )
26880
- WHERE rn <= :cap
26881
- ORDER BY occurred_at DESC, id DESC`
26882
- ),
26883
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26884
- );
26885
- const groupable = rows.map((r) => ({
26886
- id: r.id,
26887
- ruleId: r.rule_id,
26888
- category: r.category,
26889
- severity: r.severity,
26890
- maskedMatch: r.masked_match,
26891
- actionTaken: r.action_taken,
26892
- confidence: r.confidence,
26893
- occurredAt: epochMillisToIso(r.occurred_at),
26894
- sourceTool: r.source_tool,
26895
- repo: r.repo ?? "",
26896
- file: r.file ?? "",
26897
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26898
- eventId: r.event_id,
26899
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26900
- status: deriveInstanceStatus(r)
26901
- }));
27061
+ const rows = this.previewRows(aggregates, {
27062
+ sessionId: query.sessionId,
27063
+ from: query.from
27064
+ });
27065
+ const groupable = rows.map(toFlatFindingRow);
26902
27066
  const allGroups = buildFindingGroups(groupable, { aggregates });
26903
27067
  const filterOpts = {
26904
27068
  severity: query.severity,
@@ -26984,8 +27148,10 @@ var SqliteFindingsRepository = class {
26984
27148
  *
26985
27149
  * The scan runs from the top of the scope on every request, not from the
26986
27150
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26987
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26988
- * while the counting runs, and only the page itself is retained.
27151
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27152
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27153
+ * counting runs — a generator streaming the index order, not a sequence of
27154
+ * fetched batches; only the page itself is retained.
26989
27155
  */
26990
27156
  listFindingInstances(query) {
26991
27157
  const opts = {
@@ -27001,6 +27167,10 @@ var SqliteFindingsRepository = class {
27001
27167
  };
27002
27168
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27003
27169
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27170
+ const isPastCursor = cursor === null ? () => true : (row) => {
27171
+ const rowMs = isoToEpochMillis(row.occurredAt);
27172
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27173
+ };
27004
27174
  const accumulator = createInstanceFacetAccumulator(opts);
27005
27175
  const items = [];
27006
27176
  let total = 0;
@@ -27013,6 +27183,7 @@ var SqliteFindingsRepository = class {
27013
27183
  accumulator.add(row);
27014
27184
  if (!matchesInstanceFilters(row, opts)) continue;
27015
27185
  total += 1;
27186
+ if (!isPastCursor(row)) continue;
27016
27187
  if (items.length < limit) {
27017
27188
  items.push(toInstanceDetail(row));
27018
27189
  last = row;
@@ -27021,15 +27192,6 @@ var SqliteFindingsRepository = class {
27021
27192
  }
27022
27193
  }
27023
27194
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27024
- if (cursor !== null) {
27025
- const resumed = this.pageAfter(cursor, opts, limit, query);
27026
- return Promise.resolve({
27027
- totals: { findings: total },
27028
- facets: accumulator.facets(),
27029
- items: resumed.items,
27030
- nextCursor: resumed.nextCursor
27031
- });
27032
- }
27033
27195
  return Promise.resolve({
27034
27196
  totals: { findings: total },
27035
27197
  facets: accumulator.facets(),
@@ -27037,35 +27199,6 @@ var SqliteFindingsRepository = class {
27037
27199
  nextCursor
27038
27200
  });
27039
27201
  }
27040
- /**
27041
- * The page of matching rows strictly after `cursor`. Separate from the
27042
- * counting pass because that one starts at the top of the scope by design;
27043
- * this one narrows the scan with the same keyset predicate the activity list
27044
- * uses, so a later page costs less than the first rather than more.
27045
- */
27046
- pageAfter(cursor, opts, limit, query) {
27047
- const items = [];
27048
- let last;
27049
- let hasMore = false;
27050
- for (const row of this.scanFindingRows({
27051
- sessionId: query.sessionId,
27052
- from: query.from,
27053
- after: cursor
27054
- })) {
27055
- if (!matchesInstanceFilters(row, opts)) continue;
27056
- if (items.length < limit) {
27057
- items.push(toInstanceDetail(row));
27058
- last = row;
27059
- } else {
27060
- hasMore = true;
27061
- break;
27062
- }
27063
- }
27064
- return {
27065
- items,
27066
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27067
- };
27068
- }
27069
27202
  /**
27070
27203
  * The same findings folded by location: repository, then file within it.
27071
27204
  *
@@ -27148,25 +27281,111 @@ var SqliteFindingsRepository = class {
27148
27281
  });
27149
27282
  }
27150
27283
  /**
27151
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27284
+ * Each group's newest instances, for the table's expanded rows.
27285
+ *
27286
+ * ONE index-ordered scan with early termination, and the shape is the point.
27287
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27288
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27289
+ * through a temp B-tree to keep a bounded preview of each group, and then
27290
+ * sorts the survivors again for the page order. Both sorts grow with the
27291
+ * store while the answer does not.
27292
+ *
27293
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27294
+ * (or the session or window index the scope names — see `findingScanSql`),
27295
+ * which is already the order the page wants, and keeps rows per rule until
27296
+ * each rule has as many as it can show. The aggregate the caller already holds
27297
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27298
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27299
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27300
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27301
+ * store with many firing rules widens it. The bound that DOES hold
27302
+ * unconditionally is the sorted form's floor: this scan visits at most as
27303
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27304
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27305
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27306
+ * wanted instances sitting at the tail of the scope — is one pass over
27307
+ * everything in scope with a block sort of the id tie-break only, never a
27308
+ * sort of the scope, which is still that floor.
27309
+ *
27310
+ * A row whose rule the aggregate did not see is skipped: the two statements
27311
+ * run without a shared snapshot, so a capture landing between them can add a
27312
+ * rule here that has no counts there, and the counts are what the group is
27313
+ * built from.
27314
+ */
27315
+ previewRows(aggregates, scope) {
27316
+ const wanted = /* @__PURE__ */ new Map();
27317
+ let remaining = 0;
27318
+ for (const [ruleId, agg] of aggregates) {
27319
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27320
+ wanted.set(ruleId, n);
27321
+ remaining += n;
27322
+ }
27323
+ const rows = [];
27324
+ if (remaining === 0) return rows;
27325
+ const { sql, params } = this.findingScanSql(scope);
27326
+ const taken = /* @__PURE__ */ new Map();
27327
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27328
+ const want = wanted.get(r.rule_id);
27329
+ if (want === void 0) continue;
27330
+ const have = taken.get(r.rule_id) ?? 0;
27331
+ if (have >= want) continue;
27332
+ taken.set(r.rule_id, have + 1);
27333
+ rows.push(r);
27334
+ remaining -= 1;
27335
+ if (remaining === 0) break;
27336
+ }
27337
+ return rows;
27338
+ }
27339
+ /**
27340
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27152
27341
  *
27153
27342
  * A generator so a caller streams the scope without it ever being an array:
27154
27343
  * the flat list counts and facets the whole filtered scope, which on a large
27155
- * store is far more rows than any page. Each batch advances the same keyset
27156
- * predicate the page read uses, so the scan is a sequence of bounded reads
27157
- * rather than one unbounded result set.
27158
- *
27159
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27160
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27161
- * makes it a point lookup per row, and the derived table would re-materialize
27162
- * a window over the whole resolution table once per batch.
27344
+ * store is far more rows than any page. The rows come off ONE statement,
27345
+ * iterated rather than materialized, in the index order `findingScanSql`
27346
+ * arranges so the scan is a single pass with a block sort of the id
27347
+ * tie-break only, never a sort of the scope, where a sequence of
27348
+ * keyset-bounded batches re-sorted everything below the cursor on every
27349
+ * batch and cost the square of the scope.
27163
27350
  *
27164
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27165
- * would be missing from its own facet, which is computed by excluding that
27166
- * dimension see listFindingInstances.
27351
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27352
+ * dimension narrowed here would be missing from its own facet, which is
27353
+ * computed by excluding that dimension (see listFindingInstances). There is
27354
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27355
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27356
+ * narrower statement, since the counting pass already visits every row a
27357
+ * page-2+ request would otherwise re-seek for.
27167
27358
  */
27168
27359
  *scanFindingRows(scope) {
27169
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27360
+ const { sql, params } = this.findingScanSql(scope);
27361
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27362
+ yield toFlatFindingRow(r);
27363
+ }
27364
+ }
27365
+ /**
27366
+ * The one statement both instance-level scans run: every finding in scope,
27367
+ * joined to its event and definition, newest first.
27368
+ *
27369
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27370
+ * the same two `recentFindings` documents at length, for the same reason:
27371
+ *
27372
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27373
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27374
+ * yields `started_at` order per event type, not across the four, so
27375
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27376
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27377
+ * `idx_audit_session` for a session scope, which is also `started_at`
27378
+ * ordered within the session — and the order falls out of the index.
27379
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27380
+ * JOINs the planner drives from the findings and sorts everything.
27381
+ *
27382
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27383
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27384
+ * index probe per keyed row, and a derived table over the whole resolution
27385
+ * table would be materialized before the first row streamed.
27386
+ */
27387
+ findingScanSql(scope) {
27388
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27170
27389
  const params = [];
27171
27390
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27172
27391
  conditions.push("e.root_session_id = ?");
@@ -27180,58 +27399,24 @@ var SqliteFindingsRepository = class {
27180
27399
  d.severity AS severity, f.masked_match AS masked_match,
27181
27400
  f.action_taken AS action_taken, f.confidence AS confidence,
27182
27401
  e.started_at AS occurred_at,
27183
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27184
- json_extract(e.attributes, '$.repo') AS repo,
27185
- json_extract(e.attributes, '$.file_path') AS file,
27186
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27402
+ e.source_tool AS source_tool,
27403
+ e.repo AS repo,
27404
+ e.file_path AS file,
27405
+ e.tool_name AS tool_name,
27187
27406
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27188
27407
  e.event_type AS kind, f.finding_key AS finding_key,
27189
27408
  ${latestResolutionStatusSql("f")} AS latest_status
27190
- FROM inspection_findings f
27191
- JOIN audit_events e ON e.id = f.audit_event_id
27192
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27409
+ FROM audit_events e
27410
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27411
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27193
27412
  WHERE ${conditions.join(" AND ")}
27194
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27195
- ORDER BY e.started_at DESC, f.id DESC
27196
- LIMIT ?`;
27197
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27198
- for (; ; ) {
27199
- const rows = allRows(this.db.prepare(sql), [
27200
- ...params,
27201
- after.startedAtMs,
27202
- after.startedAtMs,
27203
- after.id,
27204
- SCAN_BATCH_ROWS
27205
- ]);
27206
- for (const r of rows) {
27207
- yield {
27208
- id: r.id,
27209
- ruleId: r.rule_id,
27210
- category: r.category,
27211
- severity: r.severity,
27212
- maskedMatch: r.masked_match,
27213
- actionTaken: r.action_taken,
27214
- confidence: r.confidence,
27215
- occurredAt: epochMillisToIso(r.occurred_at),
27216
- sourceTool: r.source_tool,
27217
- repo: r.repo ?? "",
27218
- file: r.file ?? "",
27219
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27220
- eventId: r.event_id,
27221
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27222
- status: deriveInstanceStatus(r)
27223
- };
27224
- }
27225
- if (rows.length < SCAN_BATCH_ROWS) return;
27226
- const lastRow = rows[rows.length - 1];
27227
- if (lastRow === void 0) return;
27228
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27229
- }
27413
+ ORDER BY e.started_at DESC, f.id DESC`;
27414
+ return { sql, params };
27230
27415
  }
27231
27416
  groupAggregates(withSearchText, scope) {
27232
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27233
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27234
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27417
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27418
+ group_concat(DISTINCT e.file_path) AS files,
27419
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27235
27420
  const rows = this.db.prepare(
27236
27421
  `SELECT rule_id,
27237
27422
  sum(tuple_count) AS instance_count,
@@ -27249,7 +27434,7 @@ var SqliteFindingsRepository = class {
27249
27434
  coalesce(latest.status, '') AS status_tuple,
27250
27435
  count(*) AS tuple_count,
27251
27436
  max(e.started_at) AS latest_at,
27252
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27437
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27253
27438
  group_concat(DISTINCT f.action_taken) AS actions_taken
27254
27439
  ${innerSearchColumns}
27255
27440
  FROM inspection_findings f
@@ -27380,6 +27565,8 @@ function isoDay(ms) {
27380
27565
  // ../../packages/persistence/src/repositories/history-sync.ts
27381
27566
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27382
27567
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27568
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27569
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27383
27570
  var SKIPPED = -1;
27384
27571
  var ROW_COLUMNS = `id,
27385
27572
  parent_id AS parentId,
@@ -27419,6 +27606,20 @@ var SqliteHistorySyncRepository = class {
27419
27606
  ORDER BY (event_type = 'session') DESC, started_at
27420
27607
  LIMIT :limit`
27421
27608
  );
27609
+ this.captureRowsStmt = db.prepare(
27610
+ `SELECT ${ROW_COLUMNS}
27611
+ FROM audit_events
27612
+ WHERE synced_at IS NULL
27613
+ AND sync_claimed_at IS NULL
27614
+ AND outbox_owed = 1
27615
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27616
+ AND started_at < :before
27617
+ ORDER BY started_at
27618
+ LIMIT :limit`
27619
+ );
27620
+ this.markOwedStmt = db.prepare(
27621
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27622
+ );
27422
27623
  this.stampStmt = db.prepare(
27423
27624
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27424
27625
  );
@@ -27450,6 +27651,12 @@ var SqliteHistorySyncRepository = class {
27450
27651
  FROM audit_events
27451
27652
  WHERE event_type IN (${TYPE_LIST})`
27452
27653
  );
27654
+ this.captureSkipCountStmt = db.prepare(
27655
+ `SELECT COUNT(*) AS skipped
27656
+ FROM audit_events
27657
+ WHERE synced_at = ${String(SKIPPED)}
27658
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27659
+ );
27453
27660
  this.fingerprintStmt = db.prepare(
27454
27661
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27455
27662
  FROM history_sync WHERE id = 1`
@@ -27459,6 +27666,10 @@ var SqliteHistorySyncRepository = class {
27459
27666
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27460
27667
  WHERE id = 1`
27461
27668
  );
27669
+ this.disownCapturesStmt = db.prepare(
27670
+ `UPDATE audit_events SET outbox_owed = NULL
27671
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27672
+ );
27462
27673
  this.rearmStmt = db.prepare(
27463
27674
  `UPDATE audit_events SET synced_at = NULL
27464
27675
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27531,6 +27742,10 @@ var SqliteHistorySyncRepository = class {
27531
27742
  closeWindowStmt;
27532
27743
  releaseBoundaryStmt;
27533
27744
  freezeBoundaryStmt;
27745
+ captureRowsStmt;
27746
+ markOwedStmt;
27747
+ captureSkipCountStmt;
27748
+ disownCapturesStmt;
27534
27749
  partitionStmt;
27535
27750
  claimRowStmt;
27536
27751
  releaseRowStmt;
@@ -27564,6 +27779,34 @@ var SqliteHistorySyncRepository = class {
27564
27779
  pendingRows(sessionId, limit, before) {
27565
27780
  return allRows(this.rowsStmt, { sessionId, limit, before });
27566
27781
  }
27782
+ /**
27783
+ * Captures this machine still owes the deployment, oldest first.
27784
+ *
27785
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27786
+ * by a time window — see captureRowsStmt for why a window could not express
27787
+ * this. `before` is the grace window that leaves a just-recorded capture to
27788
+ * the live path.
27789
+ */
27790
+ pendingCaptureRows(limit, before) {
27791
+ return allRows(this.captureRowsStmt, { limit, before });
27792
+ }
27793
+ /**
27794
+ * Record that a capture is OWED to the deployment.
27795
+ *
27796
+ * Written by the attached forward path when a live send did not confirm
27797
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27798
+ * a fact rather than an inference: the machine was attached, the send did not
27799
+ * land, so the row is owed — which no time window can state, because the same
27800
+ * window that holds the rows a past attachment left owed also holds every
27801
+ * capture recorded while the machine was DETACHED, and those were never
27802
+ * offered to anyone.
27803
+ *
27804
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27805
+ * out of the drain's read.
27806
+ */
27807
+ markCaptureOwed(id) {
27808
+ this.markOwedStmt.run({ id });
27809
+ }
27567
27810
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27568
27811
  markSynced(ids, atMs) {
27569
27812
  this.stampAll(ids, atMs);
@@ -27647,10 +27890,12 @@ var SqliteHistorySyncRepository = class {
27647
27890
  this.countsStmt,
27648
27891
  { before }
27649
27892
  );
27893
+ const captures = getRow(this.captureSkipCountStmt);
27650
27894
  return {
27651
27895
  pending: row?.pending ?? 0,
27652
27896
  sent: row?.sent ?? 0,
27653
- skipped: row?.skipped ?? 0
27897
+ skipped: row?.skipped ?? 0,
27898
+ capturesSkipped: captures?.skipped ?? 0
27654
27899
  };
27655
27900
  }
27656
27901
  /**
@@ -27691,7 +27936,11 @@ var SqliteHistorySyncRepository = class {
27691
27936
  withTransaction(
27692
27937
  this.db,
27693
27938
  () => {
27939
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27694
27940
  this.rearmStmt.run();
27941
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27942
+ this.disownCapturesStmt.run();
27943
+ }
27695
27944
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27696
27945
  },
27697
27946
  "IMMEDIATE"
@@ -27888,7 +28137,255 @@ var SqliteInspectionFindingsRepository = class {
27888
28137
  };
27889
28138
 
27890
28139
  // ../../packages/persistence/src/repositories/installed-packs.ts
27891
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28140
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28141
+
28142
+ // ../../packages/persistence/src/policy-floor.ts
28143
+ import { readFileSync as readFileSync5 } from "fs";
28144
+ import { join as join6 } from "path";
28145
+
28146
+ // ../../packages/persistence/src/local-layout.ts
28147
+ import { renameSync as renameSync3 } from "fs";
28148
+ import { mkdir } from "fs/promises";
28149
+ import { homedir } from "os";
28150
+ import { join as join4 } from "path";
28151
+ function defaultDataDir() {
28152
+ return join4(homedir(), ".aka");
28153
+ }
28154
+ function settingsDir(base = defaultDataDir()) {
28155
+ return join4(base, "settings");
28156
+ }
28157
+ function dataDir(base = defaultDataDir()) {
28158
+ return join4(base, "data");
28159
+ }
28160
+ function dbPath(base = defaultDataDir()) {
28161
+ return join4(dataDir(base), "aka.db");
28162
+ }
28163
+ function keysDir(base = defaultDataDir()) {
28164
+ return join4(base, "keys");
28165
+ }
28166
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28167
+ ensureDataDirSync(dir);
28168
+ }
28169
+ function migrateLegacyLayout(base = defaultDataDir()) {
28170
+ const moves = [
28171
+ { name: "config.json", dest: settingsDir(base) },
28172
+ { name: "policy-cache.json", dest: dataDir(base) }
28173
+ ];
28174
+ for (const { name, dest } of moves) {
28175
+ try {
28176
+ ensureDataDirSync(dest);
28177
+ const moved = join4(dest, name);
28178
+ renameSync3(join4(base, name), moved);
28179
+ tightenFile(moved);
28180
+ } catch {
28181
+ }
28182
+ }
28183
+ }
28184
+
28185
+ // ../../packages/persistence/src/settings.ts
28186
+ import { readFileSync as readFileSync4 } from "fs";
28187
+ import { join as join5 } from "path";
28188
+
28189
+ // ../../packages/persistence/src/file-lock.ts
28190
+ import { randomUUID as randomUUID3 } from "crypto";
28191
+ import {
28192
+ closeSync,
28193
+ existsSync as existsSync2,
28194
+ openSync,
28195
+ readFileSync as readFileSync2,
28196
+ rmSync as rmSync5,
28197
+ statSync as statSync3,
28198
+ writeFileSync as writeFileSync2
28199
+ } from "fs";
28200
+ import { hostname as hostname3 } from "os";
28201
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28202
+
28203
+ // ../../packages/persistence/src/managed-settings.ts
28204
+ import { readFileSync as readFileSync3 } from "fs";
28205
+ import { posix, win32 } from "path";
28206
+ function managedSettingsPaths(platform2 = process.platform) {
28207
+ if (platform2 === "darwin") {
28208
+ return [
28209
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28210
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28211
+ ];
28212
+ }
28213
+ if (platform2 === "win32") {
28214
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28215
+ }
28216
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28217
+ }
28218
+ function readManagedSettings(paths = managedSettingsPaths()) {
28219
+ for (const path of paths) {
28220
+ let text;
28221
+ try {
28222
+ text = readFileSync3(path, "utf8");
28223
+ } catch {
28224
+ continue;
28225
+ }
28226
+ const record2 = parseJsonObject(text);
28227
+ if (!record2) continue;
28228
+ const parsed = ManagedSettings.safeParse(record2);
28229
+ if (parsed.success) return parsed.data;
28230
+ }
28231
+ return null;
28232
+ }
28233
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28234
+ if (!managed) return settings;
28235
+ const { values } = managed;
28236
+ const merged = { ...settings };
28237
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28238
+ if (values.controlPlane !== void 0) {
28239
+ merged.controlPlane = {
28240
+ ...values.controlPlane,
28241
+ // The administrator pinned WHICH deployment, not WHEN this machine
28242
+ // joined it. Keep the user's own attach time when the endpoint is
28243
+ // unchanged, so a managed machine does not appear to re-attach on every
28244
+ // read; stamp a fresh one when the administrator moved it.
28245
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28246
+ };
28247
+ }
28248
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28249
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28250
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28251
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28252
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28253
+ if (values.vaultConsent !== void 0) {
28254
+ merged.vaultConsent = values.vaultConsent ? (
28255
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28256
+ // at the current version otherwise.
28257
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28258
+ ) : void 0;
28259
+ }
28260
+ if (values.modelJudgeConsent !== void 0) {
28261
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28262
+ acknowledgedAt: now().toISOString(),
28263
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28264
+ } : void 0;
28265
+ }
28266
+ return merged;
28267
+ }
28268
+
28269
+ // ../../packages/persistence/src/settings.ts
28270
+ var SETTINGS_FILENAME = "settings.json";
28271
+ function readWorkspaceSettings(base = defaultDataDir()) {
28272
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28273
+ }
28274
+ function readUserSettings(base) {
28275
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28276
+ if (!record2) return defaultWorkspaceSettings();
28277
+ try {
28278
+ return WorkspaceSettings.parse(record2);
28279
+ } catch {
28280
+ return defaultWorkspaceSettings();
28281
+ }
28282
+ }
28283
+ function readJson(file2) {
28284
+ let text;
28285
+ try {
28286
+ text = readFileSync4(file2, "utf8");
28287
+ } catch {
28288
+ return null;
28289
+ }
28290
+ return parseJsonObject(text) ?? null;
28291
+ }
28292
+
28293
+ // ../../packages/persistence/src/policy-floor.ts
28294
+ function refusalMessage(pack, attempted, floor, refusal) {
28295
+ switch (refusal) {
28296
+ case "lock":
28297
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28298
+ case "disable":
28299
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28300
+ case "floor":
28301
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28302
+ }
28303
+ }
28304
+ var PolicyFloorError = class extends Error {
28305
+ /** `namespace/packId` of the detection whose write was refused. */
28306
+ pack;
28307
+ /**
28308
+ * The archetype the caller asked for, or null when the write named none —
28309
+ * clearing the assignment, or switching the detection off.
28310
+ */
28311
+ attempted;
28312
+ /** The weakest archetype the control plane permits for this pack. */
28313
+ floor;
28314
+ refusal;
28315
+ constructor(pack, attempted, floor, refusal) {
28316
+ super(refusalMessage(pack, attempted, floor, refusal));
28317
+ this.name = "PolicyFloorError";
28318
+ this.pack = pack;
28319
+ this.attempted = attempted;
28320
+ this.floor = floor;
28321
+ this.refusal = refusal;
28322
+ }
28323
+ };
28324
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28325
+ try {
28326
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28327
+ const parsed = JSON.parse(raw);
28328
+ if (typeof parsed !== "object" || parsed === null) return null;
28329
+ return PolicyBundle.parse(parsed.bundle);
28330
+ } catch {
28331
+ return null;
28332
+ }
28333
+ }
28334
+ function indexEnabled(policies) {
28335
+ const byRuleId = /* @__PURE__ */ new Map();
28336
+ const byCategory = /* @__PURE__ */ new Map();
28337
+ for (const policy of policies) {
28338
+ if (!policy.enabled) continue;
28339
+ if ("ruleId" in policy.target) {
28340
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28341
+ } else if (!byCategory.has(policy.target.category)) {
28342
+ byCategory.set(policy.target.category, policy.action);
28343
+ }
28344
+ }
28345
+ return { byRuleId, byCategory };
28346
+ }
28347
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28348
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28349
+ const categories = new Set(rules.map((rule) => rule.category));
28350
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28351
+ return policies.some((policy) => {
28352
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28353
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28354
+ });
28355
+ }
28356
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28357
+ const floors = openControlPlaneFloors(base);
28358
+ return floors === null ? null : floors.floorFor(rules);
28359
+ }
28360
+ function openControlPlaneFloors(base = defaultDataDir()) {
28361
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28362
+ const bundle = readCachedPolicyBundle(base);
28363
+ if (bundle === null) return null;
28364
+ const indexes = indexEnabled(bundle.policies);
28365
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28366
+ }
28367
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28368
+ let action = null;
28369
+ for (const rule of rules) {
28370
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28371
+ if (resolved === void 0) continue;
28372
+ action = action === null ? resolved : strongerAction(action, resolved);
28373
+ }
28374
+ if (action === null) return null;
28375
+ return {
28376
+ floor: weakestBuiltinAtLeast(action),
28377
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28378
+ };
28379
+ }
28380
+ function policyAssignmentRefusal(policyId, floor) {
28381
+ if (floor.locked) return "lock";
28382
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28383
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28384
+ }
28385
+ function packEnablementRefusal(enabled, floor) {
28386
+ if (floor === null || enabled) return null;
28387
+ return "disable";
28388
+ }
27892
28389
 
27893
28390
  // ../../packages/persistence/src/semver.ts
27894
28391
  function parse3(version2) {
@@ -27982,8 +28479,19 @@ function ruleIdsOf(rulesJson) {
27982
28479
  return ids;
27983
28480
  }
27984
28481
  var SqliteInstalledPacksRepository = class {
27985
- constructor(db) {
28482
+ /**
28483
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28484
+ * floor needs both halves of it (settings/ says whether this machine is
28485
+ * attached, data/ holds the cached bundle). It is optional because a caller
28486
+ * holding only a DatabaseSync — every test construction site, and any embedder
28487
+ * that opens the store itself — has no layout to point at, and such a caller
28488
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28489
+ * from `openLocalDatabase`, which is the single construction site that owns a
28490
+ * real `~/.aka`.
28491
+ */
28492
+ constructor(db, baseDir) {
27986
28493
  this.db = db;
28494
+ this.baseDir = baseDir;
27987
28495
  this.insertMissingStmt = db.prepare(
27988
28496
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27989
28497
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28005,11 +28513,17 @@ var SqliteInstalledPacksRepository = class {
28005
28513
  this.signatureStmt = db.prepare(
28006
28514
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28007
28515
  );
28516
+ this.packRulesStmt = db.prepare(
28517
+ `SELECT rules_json AS rulesJson FROM installed_packs
28518
+ WHERE namespace = ? AND pack_id = ?`
28519
+ );
28008
28520
  }
28009
28521
  db;
28522
+ baseDir;
28010
28523
  insertMissingStmt;
28011
28524
  upsertAvailableStmt;
28012
28525
  signatureStmt;
28526
+ packRulesStmt;
28013
28527
  /**
28014
28528
  * Record the running binary's detection inventory. Refreshes the
28015
28529
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28051,7 +28565,7 @@ var SqliteInstalledPacksRepository = class {
28051
28565
  let behind = false;
28052
28566
  for (const row of rows) {
28053
28567
  const params = {
28054
- id: randomUUID3(),
28568
+ id: randomUUID4(),
28055
28569
  namespace: row.namespace,
28056
28570
  packId: row.packId,
28057
28571
  version: row.version,
@@ -28063,7 +28577,7 @@ var SqliteInstalledPacksRepository = class {
28063
28577
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28064
28578
  this.upsertAvailableStmt.run({
28065
28579
  ...params,
28066
- id: randomUUID3(),
28580
+ id: randomUUID4(),
28067
28581
  recordedBy: meta4?.recordedBy ?? null
28068
28582
  });
28069
28583
  } else {
@@ -28309,9 +28823,65 @@ var SqliteInstalledPacksRepository = class {
28309
28823
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28310
28824
  // caller rather than swallowing them. Each returns whether a row matched, so the
28311
28825
  // caller can tell an edit from a no-such-detection.
28826
+ /**
28827
+ * The rules one installed pack owns, reduced to what a floor computation
28828
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28829
+ * unreadable contributes no rules to a scan either, so it is not a detection
28830
+ * the control plane can be governing, and an empty list correctly imposes no
28831
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28832
+ * the user can re-enable, and its assignment stays governed meanwhile.
28833
+ */
28834
+ packFloorRules(namespace, packId) {
28835
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28836
+ if (!row) return [];
28837
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28838
+ }
28839
+ /**
28840
+ * What the connected control plane imposes on one installed pack, or null on a
28841
+ * machine that is its own authority (standalone, no cached bundle, or a
28842
+ * repository constructed without a layout base).
28843
+ *
28844
+ * Exposed as a READ so a surface can render the constraint — grey out the
28845
+ * choices below the floor, mark a locked detection as locked — rather than
28846
+ * offer the user a picker whose selections it will then be told it may not
28847
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28848
+ */
28849
+ policyFloor(namespace, packId) {
28850
+ if (this.baseDir === void 0) return null;
28851
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28852
+ }
28853
+ /**
28854
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28855
+ * entry only for a pack the control plane actually governs.
28856
+ *
28857
+ * A surface listing every detection asks per pack, and asking through
28858
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28859
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28860
+ * answer, repeated for each row, on every render. This reads all of that once.
28861
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28862
+ * exactly as the single-pack read returns null for them.
28863
+ */
28864
+ policyFloors(packs2) {
28865
+ const floors = /* @__PURE__ */ new Map();
28866
+ if (this.baseDir === void 0) return floors;
28867
+ const source = openControlPlaneFloors(this.baseDir);
28868
+ if (source === null) return floors;
28869
+ for (const pack of packs2) {
28870
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28871
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28872
+ }
28873
+ return floors;
28874
+ }
28312
28875
  /**
28313
28876
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28314
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28877
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28878
+ *
28879
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28880
+ * write below, and a detection the organization has authored a policy for is
28881
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28882
+ * a throw rather than a silently substituted value. This is the one device-local
28883
+ * write path for the assignment, so the check belongs here rather than on any
28884
+ * surface that offers the choice.
28315
28885
  */
28316
28886
  setPolicy(namespace, packId, policyId) {
28317
28887
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28319,14 +28889,38 @@ var SqliteInstalledPacksRepository = class {
28319
28889
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28320
28890
  );
28321
28891
  }
28892
+ const requested = policyId;
28893
+ const floor = this.policyFloor(namespace, packId);
28894
+ if (floor !== null) {
28895
+ const refusal = policyAssignmentRefusal(requested, floor);
28896
+ if (refusal !== null) {
28897
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28898
+ }
28899
+ }
28322
28900
  const res = this.db.prepare(
28323
28901
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28324
28902
  WHERE namespace = :namespace AND pack_id = :packId`
28325
28903
  ).run({ policyId, now: Date.now(), namespace, packId });
28326
28904
  return Number(res.changes) > 0;
28327
28905
  }
28328
- /** Enable or disable one installed pack. */
28906
+ /**
28907
+ * Enable or disable one installed pack.
28908
+ *
28909
+ * On an ATTACHED machine a detection the organization's bundle governs at all
28910
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
28911
+ * merely another point below the floor, and why re-enabling stays open. Like
28912
+ * the assignment above, the check belongs at this write path rather than on a
28913
+ * surface: this is the one device-local writer of the column, and a refusal
28914
+ * that lived in a page would leave the CLI free.
28915
+ */
28329
28916
  setEnabled(namespace, packId, enabled) {
28917
+ const floor = this.policyFloor(namespace, packId);
28918
+ if (floor !== null) {
28919
+ const refusal = packEnablementRefusal(enabled, floor);
28920
+ if (refusal !== null) {
28921
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
28922
+ }
28923
+ }
28330
28924
  const res = this.db.prepare(
28331
28925
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28332
28926
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28412,7 +29006,7 @@ var SqliteInventoryRepository = class {
28412
29006
  };
28413
29007
 
28414
29008
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28415
- import { randomUUID as randomUUID4 } from "crypto";
29009
+ import { randomUUID as randomUUID5 } from "crypto";
28416
29010
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28417
29011
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28418
29012
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28901,7 +29495,7 @@ var SqliteInventoryAssetsRepository = class {
28901
29495
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28902
29496
  VALUES (:id, :projectId, :path, :access, :now, :now)
28903
29497
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28904
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29498
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28905
29499
  }
28906
29500
  return true;
28907
29501
  }
@@ -28922,7 +29516,7 @@ var SqliteInventoryAssetsRepository = class {
28922
29516
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28923
29517
  VALUES (:id, :assetId, :trust, :now, :now)
28924
29518
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28925
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29519
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28926
29520
  }
28927
29521
  this.configRowsCache = void 0;
28928
29522
  return "ok";
@@ -29219,7 +29813,7 @@ var SqliteInventoryAssetsRepository = class {
29219
29813
  };
29220
29814
 
29221
29815
  // ../../packages/persistence/src/repositories/policies.ts
29222
- import { randomUUID as randomUUID5 } from "crypto";
29816
+ import { randomUUID as randomUUID6 } from "crypto";
29223
29817
  var SqlitePoliciesRepository = class {
29224
29818
  constructor(db) {
29225
29819
  this.db = db;
@@ -29254,7 +29848,7 @@ var SqlitePoliciesRepository = class {
29254
29848
  failOpenTransaction(this.db, () => {
29255
29849
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29256
29850
  stmt.run({
29257
- id: randomUUID5(),
29851
+ id: randomUUID6(),
29258
29852
  target: JSON.stringify({ category }),
29259
29853
  action,
29260
29854
  now: Date.now()
@@ -29274,7 +29868,7 @@ var SqlitePoliciesRepository = class {
29274
29868
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29275
29869
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29276
29870
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29277
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29871
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29278
29872
  }
29279
29873
  // Caps every global per-category policy currently set to block/redact down
29280
29874
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29342,7 +29936,7 @@ var SqlitePolicyCatalogRepository = class {
29342
29936
  };
29343
29937
 
29344
29938
  // ../../packages/persistence/src/repositories/project-files.ts
29345
- import { randomUUID as randomUUID6 } from "crypto";
29939
+ import { randomUUID as randomUUID7 } from "crypto";
29346
29940
  var SqliteProjectFilesRepository = class {
29347
29941
  constructor(db) {
29348
29942
  this.db = db;
@@ -29374,7 +29968,7 @@ var SqliteProjectFilesRepository = class {
29374
29968
  const stamp = Math.max(now, maxStamp + 1);
29375
29969
  for (const file2 of scan2.files) {
29376
29970
  this.upsertStmt.run({
29377
- id: randomUUID6(),
29971
+ id: randomUUID7(),
29378
29972
  projectId,
29379
29973
  path: file2.path,
29380
29974
  name: file2.name,
@@ -29388,9 +29982,9 @@ var SqliteProjectFilesRepository = class {
29388
29982
  };
29389
29983
 
29390
29984
  // ../../packages/persistence/src/repositories/resolutions.ts
29391
- import { randomUUID as randomUUID7 } from "crypto";
29985
+ import { randomUUID as randomUUID8 } from "crypto";
29392
29986
  var SqliteResolutionsRepository = class {
29393
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
29987
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29394
29988
  this.db = db;
29395
29989
  this.now = now;
29396
29990
  this.newId = newId;
@@ -29603,7 +30197,7 @@ var SqliteScanLedgerRepository = class {
29603
30197
  };
29604
30198
 
29605
30199
  // ../../packages/persistence/src/repositories/secret-vault.ts
29606
- import { randomUUID as randomUUID8 } from "crypto";
30200
+ import { randomUUID as randomUUID9 } from "crypto";
29607
30201
  function pageLimit(requested, fallback) {
29608
30202
  if (requested === void 0) return fallback;
29609
30203
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29649,12 +30243,14 @@ var SELECT_COLUMNS = `
29649
30243
  ciphertext,
29650
30244
  nonce,
29651
30245
  auth_tag AS authTag,
30246
+ user_authorized AS userAuthorized,
29652
30247
  occurrence_count AS occurrenceCount,
29653
30248
  first_seen AS firstSeen,
29654
30249
  last_seen AS lastSeen`;
29655
30250
  function toRow(raw) {
29656
- const { provider, ...rest } = raw;
29657
- return provider === null ? rest : { ...rest, provider };
30251
+ const { provider, userAuthorized, ...rest } = raw;
30252
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30253
+ return provider === null ? row : { ...row, provider };
29658
30254
  }
29659
30255
  var SqliteSecretVaultRepository = class {
29660
30256
  constructor(db) {
@@ -29664,17 +30260,18 @@ var SqliteSecretVaultRepository = class {
29664
30260
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29665
30261
  format_version, category, rule_id, masked_match, provider,
29666
30262
  ciphertext, nonce, auth_tag,
29667
- occurrence_count, first_seen, last_seen
30263
+ user_authorized, occurrence_count, first_seen, last_seen
29668
30264
  ) VALUES (
29669
30265
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29670
30266
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29671
30267
  :ciphertext, :nonce, :authTag,
29672
- 1, :now, :now
30268
+ :userAuthorized, 1, :now, :now
29673
30269
  )`
29674
30270
  );
29675
30271
  this.bumpStmt = db.prepare(
29676
30272
  `UPDATE secret_vault
29677
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30273
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30274
+ user_authorized = max(user_authorized, :userAuthorized)
29678
30275
  WHERE value_fingerprint = :valueFingerprint`
29679
30276
  );
29680
30277
  this.byPointerStmt = db.prepare(
@@ -29694,6 +30291,7 @@ var SqliteSecretVaultRepository = class {
29694
30291
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29695
30292
  WHERE pointer_id = :pointerId`
29696
30293
  );
30294
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29697
30295
  this.derefStmt = db.prepare(
29698
30296
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29699
30297
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29707,6 +30305,7 @@ var SqliteSecretVaultRepository = class {
29707
30305
  listStmt;
29708
30306
  replaceCiphertextStmt;
29709
30307
  refreshFingerprintStmt;
30308
+ deleteByPointerStmt;
29710
30309
  derefStmt;
29711
30310
  /**
29712
30311
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29715,6 +30314,11 @@ var SqliteSecretVaultRepository = class {
29715
30314
  * pointer, category and ciphertext, so the same secret always resolves to one
29716
30315
  * wire token. `minted` is true only when this call created the row.
29717
30316
  *
30317
+ * `userAuthorized` is the one field a repeat call may still change, and only
30318
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30319
+ * the row is shared with every automatic path that vaults the same value. See
30320
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30321
+ *
29718
30322
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29719
30323
  * writers cannot both decide they are minting.
29720
30324
  */
@@ -29741,13 +30345,18 @@ var SqliteSecretVaultRepository = class {
29741
30345
  ciphertext: input2.ciphertext,
29742
30346
  nonce: input2.nonce,
29743
30347
  authTag: input2.authTag,
30348
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29744
30349
  now
29745
30350
  })
29746
30351
  );
29747
30352
  minted = true;
29748
30353
  return;
29749
30354
  }
29750
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30355
+ this.bumpStmt.run({
30356
+ valueFingerprint: input2.valueFingerprint,
30357
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30358
+ now
30359
+ });
29751
30360
  },
29752
30361
  "IMMEDIATE"
29753
30362
  );
@@ -29807,6 +30416,42 @@ var SqliteSecretVaultRepository = class {
29807
30416
  );
29808
30417
  return destroyed;
29809
30418
  }
30419
+ /**
30420
+ * Destroy the named entries and report WHICH ones went — the scoped
30421
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30422
+ * values back where they came from. Ids the store does not hold are absent
30423
+ * from the answer rather than an error, so a set assembled from a stale read
30424
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30425
+ * it.
30426
+ *
30427
+ * The ids come back rather than a count because the caller's next act is to
30428
+ * write a purge row per destroyed entry, and a record of destruction has to
30429
+ * be a record of what was really destroyed: a selection is a claim about a
30430
+ * read that has since gone stale, and auditing from it invents a purge for an
30431
+ * entry still sitting in the vault.
30432
+ *
30433
+ * One transaction over the whole set rather than a statement per id: the
30434
+ * caller hands this the result of a restore pass it has completed, and a
30435
+ * fault partway through must leave the vault as it was found rather than
30436
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30437
+ * stands for, so half a delete is not a state anything can recover from.
30438
+ */
30439
+ deleteByPointerIds(pointerIds) {
30440
+ if (pointerIds.length === 0) return [];
30441
+ const deleted = [];
30442
+ withTransaction(
30443
+ this.db,
30444
+ () => {
30445
+ for (const pointerId of pointerIds) {
30446
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30447
+ deleted.push(pointerId);
30448
+ }
30449
+ }
30450
+ },
30451
+ "IMMEDIATE"
30452
+ );
30453
+ return deleted;
30454
+ }
29810
30455
  /**
29811
30456
  * Record (or re-stamp) one place a pointer has been written. One row per
29812
30457
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29819,7 +30464,7 @@ var SqliteSecretVaultRepository = class {
29819
30464
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29820
30465
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29821
30466
  ).run({
29822
- id: randomUUID8(),
30467
+ id: randomUUID9(),
29823
30468
  pointerId: entry.pointerId,
29824
30469
  location: entry.location,
29825
30470
  kind: entry.kind,
@@ -30332,15 +30977,15 @@ var SqliteSecurityRepository = class {
30332
30977
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30333
30978
  const rows = allRows(
30334
30979
  this.db.prepare(
30335
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
30980
+ `SELECT e.repo AS repo, count(*) AS c
30336
30981
  FROM inspection_findings f
30337
30982
  JOIN audit_events e ON e.id = f.audit_event_id
30338
30983
  WHERE e.started_at >= :from AND e.started_at < :to
30339
30984
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30340
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30341
- AND json_extract(e.attributes, '$.repo') != ''
30342
- GROUP BY repo
30343
- ORDER BY c DESC, repo
30985
+ AND e.repo IS NOT NULL
30986
+ AND e.repo != ''
30987
+ GROUP BY e.repo
30988
+ ORDER BY c DESC, e.repo
30344
30989
  LIMIT :limit`
30345
30990
  ),
30346
30991
  { from, to: now, limit }
@@ -30402,7 +31047,7 @@ var SqliteSecurityRepository = class {
30402
31047
  `SELECT f.finding_key AS finding_key,
30403
31048
  d.rule_id AS rule_id,
30404
31049
  d.severity AS severity,
30405
- json_extract(e.attributes, '$.file_path') AS path,
31050
+ e.file_path AS path,
30406
31051
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30407
31052
  latest.resolved_at AS latest_resolved_at
30408
31053
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30455,7 +31100,7 @@ var SqliteSecurityRepository = class {
30455
31100
  };
30456
31101
 
30457
31102
  // ../../packages/persistence/src/repositories/shares.ts
30458
- import { randomUUID as randomUUID9 } from "crypto";
31103
+ import { randomUUID as randomUUID10 } from "crypto";
30459
31104
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30460
31105
  var IN_CHUNK = 500;
30461
31106
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30543,7 +31188,7 @@ function buildSummary(dest, endpoints) {
30543
31188
  callSiteCount,
30544
31189
  transports: distinctTransports(transports),
30545
31190
  dataClasses: distinctDataClasses(dataClasses),
30546
- review: buildReviewInfo(dest.trust, transports),
31191
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30547
31192
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30548
31193
  endpoints: endpoints.map(toEndpointSummary)
30549
31194
  };
@@ -30570,7 +31215,7 @@ function buildDetail(dest, endpoints, callSites) {
30570
31215
  lastSeen: new Date(lastSeenMs).toISOString(),
30571
31216
  transports: distinctTransports(transports),
30572
31217
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30573
- review: buildReviewInfo(dest.trust, transports),
31218
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30574
31219
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30575
31220
  note: dest.note,
30576
31221
  endpoints: endpoints.map((ep) => ({
@@ -30599,7 +31244,11 @@ var SqliteSharesRepository = class {
30599
31244
  FROM share_destination d
30600
31245
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30601
31246
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30602
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31247
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31248
+ AND NOT EXISTS (
31249
+ SELECT 1 FROM egress_decision_override o
31250
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31251
+ )`
30603
31252
  );
30604
31253
  const kindCounts = countBy(
30605
31254
  this.db,
@@ -30711,7 +31360,7 @@ var SqliteSharesRepository = class {
30711
31360
  (id, destination_id, host, decision, created_at, updated_at)
30712
31361
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30713
31362
  ).run({
30714
- id: randomUUID9(),
31363
+ id: randomUUID10(),
30715
31364
  destinationId,
30716
31365
  host: dest.host,
30717
31366
  decision,
@@ -30860,7 +31509,7 @@ var SqliteSharesRepository = class {
30860
31509
  let destinationId = destIds.get(hit.host);
30861
31510
  if (destinationId === void 0) {
30862
31511
  destStmt.run({
30863
- id: randomUUID9(),
31512
+ id: randomUUID10(),
30864
31513
  kind: hit.kind,
30865
31514
  name: hit.name,
30866
31515
  host: hit.host,
@@ -30876,7 +31525,7 @@ var SqliteSharesRepository = class {
30876
31525
  let endpointId = endpointIds.get(endpointKey);
30877
31526
  if (endpointId === void 0) {
30878
31527
  endpointStmt.run({
30879
- id: randomUUID9(),
31528
+ id: randomUUID10(),
30880
31529
  destinationId,
30881
31530
  method: hit.method,
30882
31531
  transport: hit.transport,
@@ -30889,7 +31538,7 @@ var SqliteSharesRepository = class {
30889
31538
  endpointIds.set(endpointKey, endpointId);
30890
31539
  }
30891
31540
  siteStmt.run({
30892
- id: randomUUID9(),
31541
+ id: randomUUID10(),
30893
31542
  endpointId,
30894
31543
  project: input2.project,
30895
31544
  projectKey: input2.projectKey,
@@ -31254,6 +31903,7 @@ function purgeSampleData(db) {
31254
31903
  }
31255
31904
 
31256
31905
  // ../../packages/persistence/src/database.ts
31906
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31257
31907
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31258
31908
  "aka.persistence.unsafeTestOnlyRawHandle"
31259
31909
  );
@@ -31301,7 +31951,7 @@ function backupLegacyStore(db, file2) {
31301
31951
  discardStore(file2, backup);
31302
31952
  return backup;
31303
31953
  }
31304
- function openAndInitialize(file2) {
31954
+ function openAndInitialize(file2, base) {
31305
31955
  let db = openWithPragmas(file2);
31306
31956
  try {
31307
31957
  if (isForeignSqliteLineage(db)) {
@@ -31314,7 +31964,7 @@ function openAndInitialize(file2) {
31314
31964
  applyMigrations(db, file2);
31315
31965
  tightenPerms(file2);
31316
31966
  const policies = new SqlitePoliciesRepository(db);
31317
- const installedPacks = new SqliteInstalledPacksRepository(db);
31967
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31318
31968
  const repositories = {
31319
31969
  events: new SqliteEventsRepository(db),
31320
31970
  findings: new SqliteFindingsRepository(db),
@@ -31350,7 +32000,7 @@ function openAndInitialize(file2) {
31350
32000
  }
31351
32001
  function openLocalDatabase(dir) {
31352
32002
  ensureDataDirSync(dir);
31353
- const file2 = join4(dir, DB_FILENAME);
32003
+ const file2 = join7(dir, DB_FILENAME);
31354
32004
  reapStalePartials(file2);
31355
32005
  const {
31356
32006
  db,
@@ -31378,7 +32028,13 @@ function openLocalDatabase(dir) {
31378
32028
  inspectionDefinitions,
31379
32029
  inspectionFindings,
31380
32030
  configInventory
31381
- } = openAndInitialize(file2);
32031
+ } = openAndInitialize(
32032
+ file2,
32033
+ // `dir` is always `<base>/data` — every caller resolves it through
32034
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32035
+ // settings/ and data/, and the pack-policy floor needs both halves.
32036
+ dirname2(dir)
32037
+ );
31382
32038
  function captureRowId(event) {
31383
32039
  return captureId(
31384
32040
  event.metadata?.sessionId ?? null,
@@ -31391,6 +32047,21 @@ function openLocalDatabase(dir) {
31391
32047
  historySync.markSynced([captureRowId(event)], atMs);
31392
32048
  });
31393
32049
  }
32050
+ function markCaptureOwed(event) {
32051
+ failOpenTransaction(db, () => {
32052
+ historySync.markCaptureOwed(captureRowId(event));
32053
+ });
32054
+ }
32055
+ function markAuditEventsDelivered(events2, atMs) {
32056
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32057
+ if (stampable.length === 0) return;
32058
+ failOpenTransaction(db, () => {
32059
+ historySync.markSynced(
32060
+ stampable.map((event) => event.id),
32061
+ atMs
32062
+ );
32063
+ });
32064
+ }
31394
32065
  function recordCapture(event, detected) {
31395
32066
  failOpenTransaction(db, () => {
31396
32067
  const sessionId = event.metadata?.sessionId;
@@ -31477,7 +32148,7 @@ function openLocalDatabase(dir) {
31477
32148
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31478
32149
  if (!definitionId) continue;
31479
32150
  inspectionFindings.insertFinding({
31480
- id: randomUUID10(),
32151
+ id: randomUUID11(),
31481
32152
  auditEventId: record2.scanEvent.id,
31482
32153
  inspectionDefinitionId: definitionId,
31483
32154
  span: finding.span,
@@ -31573,6 +32244,8 @@ function openLocalDatabase(dir) {
31573
32244
  inspectionFindings,
31574
32245
  recordCapture,
31575
32246
  markCaptureDelivered,
32247
+ markCaptureOwed,
32248
+ markAuditEventsDelivered,
31576
32249
  ensureInventory,
31577
32250
  recordConfigScan,
31578
32251
  recordProjectFiles,
@@ -31611,32 +32284,18 @@ var UserGrantPolicyProvider = class {
31611
32284
  }
31612
32285
  };
31613
32286
 
31614
- // ../../packages/persistence/src/file-lock.ts
31615
- import { randomUUID as randomUUID11 } from "crypto";
31616
- import {
31617
- closeSync,
31618
- existsSync as existsSync2,
31619
- openSync,
31620
- readFileSync as readFileSync2,
31621
- rmSync as rmSync5,
31622
- statSync as statSync3,
31623
- writeFileSync as writeFileSync2
31624
- } from "fs";
31625
- import { hostname as hostname3 } from "os";
31626
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31627
-
31628
32287
  // ../../packages/persistence/src/finding-key.ts
31629
32288
  import { createHash as createHash3 } from "crypto";
31630
32289
 
31631
32290
  // ../../packages/persistence/src/fingerprint.ts
31632
32291
  import { createHmac, randomBytes } from "crypto";
31633
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31634
- import { join as join5 } from "path";
32292
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32293
+ import { join as join8 } from "path";
31635
32294
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31636
32295
  var EXCEPTION_KEY_FILENAME = "exception.key";
31637
32296
  var KEY_MATERIAL_BYTES = 32;
31638
32297
  function keyFilePath(dataDir2) {
31639
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32298
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31640
32299
  }
31641
32300
  function parseKeyFile(raw) {
31642
32301
  const parsed = JSON.parse(raw);
@@ -31674,7 +32333,7 @@ var FloorUnreadableError = class extends Error {
31674
32333
  }
31675
32334
  };
31676
32335
  function storedKeyVersionFloor(dataDir2) {
31677
- const file2 = join5(dataDir2, DB_FILENAME);
32336
+ const file2 = join8(dataDir2, DB_FILENAME);
31678
32337
  if (!existsSync3(file2)) return 0;
31679
32338
  let db;
31680
32339
  try {
@@ -31729,7 +32388,7 @@ function occupantMessage(file2, kind) {
31729
32388
  function readFingerprintKey(dataDir2) {
31730
32389
  let raw;
31731
32390
  try {
31732
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32391
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31733
32392
  } catch (err) {
31734
32393
  if (err.code === "ENOENT") return null;
31735
32394
  throw err instanceof Error ? err : new Error(String(err));
@@ -31753,142 +32412,12 @@ function fingerprintValue(key, raw) {
31753
32412
 
31754
32413
  // ../../packages/persistence/src/history-preview.ts
31755
32414
  import { existsSync as existsSync4 } from "fs";
31756
- import { join as join6 } from "path";
32415
+ import { join as join9 } from "path";
31757
32416
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31758
32417
 
31759
- // ../../packages/persistence/src/local-layout.ts
31760
- import { renameSync as renameSync3 } from "fs";
31761
- import { mkdir } from "fs/promises";
31762
- import { homedir } from "os";
31763
- import { join as join7 } from "path";
31764
- function defaultDataDir() {
31765
- return join7(homedir(), ".aka");
31766
- }
31767
- function settingsDir(base = defaultDataDir()) {
31768
- return join7(base, "settings");
31769
- }
31770
- function dataDir(base = defaultDataDir()) {
31771
- return join7(base, "data");
31772
- }
31773
- function dbPath(base = defaultDataDir()) {
31774
- return join7(dataDir(base), "aka.db");
31775
- }
31776
- function keysDir(base = defaultDataDir()) {
31777
- return join7(base, "keys");
31778
- }
31779
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31780
- ensureDataDirSync(dir);
31781
- }
31782
- function migrateLegacyLayout(base = defaultDataDir()) {
31783
- const moves = [
31784
- { name: "config.json", dest: settingsDir(base) },
31785
- { name: "policy-cache.json", dest: dataDir(base) }
31786
- ];
31787
- for (const { name, dest } of moves) {
31788
- try {
31789
- ensureDataDirSync(dest);
31790
- const moved = join7(dest, name);
31791
- renameSync3(join7(base, name), moved);
31792
- tightenFile(moved);
31793
- } catch {
31794
- }
31795
- }
31796
- }
31797
-
31798
- // ../../packages/persistence/src/managed-settings.ts
31799
- import { readFileSync as readFileSync4 } from "fs";
31800
- import { posix, win32 } from "path";
31801
- function managedSettingsPaths(platform2 = process.platform) {
31802
- if (platform2 === "darwin") {
31803
- return [
31804
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31805
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31806
- ];
31807
- }
31808
- if (platform2 === "win32") {
31809
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31810
- }
31811
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31812
- }
31813
- function readManagedSettings(paths = managedSettingsPaths()) {
31814
- for (const path of paths) {
31815
- let text;
31816
- try {
31817
- text = readFileSync4(path, "utf8");
31818
- } catch {
31819
- continue;
31820
- }
31821
- const record2 = parseJsonObject(text);
31822
- if (!record2) continue;
31823
- const parsed = ManagedSettings.safeParse(record2);
31824
- if (parsed.success) return parsed.data;
31825
- }
31826
- return null;
31827
- }
31828
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31829
- if (!managed) return settings;
31830
- const { values } = managed;
31831
- const merged = { ...settings };
31832
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31833
- if (values.controlPlane !== void 0) {
31834
- merged.controlPlane = {
31835
- ...values.controlPlane,
31836
- // The administrator pinned WHICH deployment, not WHEN this machine
31837
- // joined it. Keep the user's own attach time when the endpoint is
31838
- // unchanged, so a managed machine does not appear to re-attach on every
31839
- // read; stamp a fresh one when the administrator moved it.
31840
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31841
- };
31842
- }
31843
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31844
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31845
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31846
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31847
- if (values.vaultConsent !== void 0) {
31848
- merged.vaultConsent = values.vaultConsent ? (
31849
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31850
- // at the current version otherwise.
31851
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31852
- ) : void 0;
31853
- }
31854
- if (values.modelJudgeConsent !== void 0) {
31855
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31856
- acknowledgedAt: now().toISOString(),
31857
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31858
- } : void 0;
31859
- }
31860
- return merged;
31861
- }
31862
-
31863
- // ../../packages/persistence/src/settings.ts
31864
- import { readFileSync as readFileSync5 } from "fs";
31865
- import { join as join8 } from "path";
31866
- var SETTINGS_FILENAME = "settings.json";
31867
- function readWorkspaceSettings(base = defaultDataDir()) {
31868
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31869
- }
31870
- function readUserSettings(base) {
31871
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31872
- if (!record2) return defaultWorkspaceSettings();
31873
- try {
31874
- return WorkspaceSettings.parse(record2);
31875
- } catch {
31876
- return defaultWorkspaceSettings();
31877
- }
31878
- }
31879
- function readJson(file2) {
31880
- let text;
31881
- try {
31882
- text = readFileSync5(file2, "utf8");
31883
- } catch {
31884
- return null;
31885
- }
31886
- return parseJsonObject(text) ?? null;
31887
- }
31888
-
31889
32418
  // ../../packages/persistence/src/store-symlinks.ts
31890
32419
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31891
- import { dirname as dirname2, join as join9, resolve } from "path";
32420
+ import { dirname as dirname3, join as join10, resolve } from "path";
31892
32421
 
31893
32422
  // ../../packages/persistence/src/vault/crypto.ts
31894
32423
  import {
@@ -32001,8 +32530,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32001
32530
  // ../../packages/persistence/src/vault/key-provider.ts
32002
32531
  import { execFileSync } from "child_process";
32003
32532
  import { randomBytes as randomBytes2 } from "crypto";
32004
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32005
- import { join as join10 } from "path";
32533
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32534
+ import { join as join11 } from "path";
32006
32535
  var VAULT_OCCUPANT_REASON = {
32007
32536
  symlink: "the path is a symlink; remove it so a keyring can be created",
32008
32537
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32101,7 +32630,7 @@ function claimRotationLock(lock, owner) {
32101
32630
  throw asError(err);
32102
32631
  }
32103
32632
  try {
32104
- writeFileSync3(join10(lock, LOCK_OWNER_FILE), `${owner}
32633
+ writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
32105
32634
  `, { mode: DATA_FILE_MODE });
32106
32635
  return true;
32107
32636
  } catch (err) {
@@ -32110,7 +32639,7 @@ function claimRotationLock(lock, owner) {
32110
32639
  }
32111
32640
  }
32112
32641
  function acquireRotationLock(keysDir2) {
32113
- const lock = join10(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32642
+ const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32114
32643
  const owner = randomBytes2(16).toString("hex");
32115
32644
  if (claimRotationLock(lock, owner)) return { lock, owner };
32116
32645
  let held;
@@ -32137,7 +32666,7 @@ function acquireRotationLock(keysDir2) {
32137
32666
  }
32138
32667
  function releaseRotationLock(lease) {
32139
32668
  try {
32140
- if (readFileSync6(join10(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32669
+ if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32141
32670
  } catch {
32142
32671
  return;
32143
32672
  }
@@ -32158,7 +32687,7 @@ var FileKeyProvider = class {
32158
32687
  this.#keysDir = keysDir2;
32159
32688
  }
32160
32689
  get filePath() {
32161
- return join10(this.#keysDir, VAULT_KEY_FILENAME);
32690
+ return join11(this.#keysDir, VAULT_KEY_FILENAME);
32162
32691
  }
32163
32692
  loadOrCreate() {
32164
32693
  return asAsync(() => {
@@ -32188,7 +32717,7 @@ var FileKeyProvider = class {
32188
32717
  #read() {
32189
32718
  let raw;
32190
32719
  try {
32191
- raw = readFileSync6(this.filePath, "utf8");
32720
+ raw = readFileSync7(this.filePath, "utf8");
32192
32721
  } catch (err) {
32193
32722
  if (err.code === "ENOENT") return null;
32194
32723
  throw err instanceof Error ? err : new Error(String(err));
@@ -32475,7 +33004,14 @@ var SecretVault = class {
32475
33004
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
32476
33005
  const now = this.#now();
32477
33006
  if (existing) {
32478
- this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
33007
+ this.#repo.upsert(
33008
+ {
33009
+ ...existing,
33010
+ provider: existing.provider ?? void 0,
33011
+ userAuthorized: meta4.userAuthorized === true
33012
+ },
33013
+ now
33014
+ );
32479
33015
  return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
32480
33016
  }
32481
33017
  const { material, version: version2 } = await this.#keys.loadOrCreate();
@@ -32497,6 +33033,7 @@ var SecretVault = class {
32497
33033
  ruleId: meta4.ruleId,
32498
33034
  maskedMatch: meta4.maskedMatch,
32499
33035
  provider: meta4.provider,
33036
+ userAuthorized: meta4.userAuthorized === true,
32500
33037
  ciphertext: sealed.ciphertext.toString("base64"),
32501
33038
  nonce: sealed.nonce.toString("base64"),
32502
33039
  authTag: sealed.authTag.toString("base64")
@@ -32816,7 +33353,7 @@ var SecretVault = class {
32816
33353
 
32817
33354
  // ../../packages/persistence/src/warn-era-cap.ts
32818
33355
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32819
- import { join as join11 } from "path";
33356
+ import { join as join12 } from "path";
32820
33357
 
32821
33358
  // ../../packages/plugin-sdk/src/provider-env.ts
32822
33359
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32870,7 +33407,7 @@ function resolveProvider() {
32870
33407
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32871
33408
  try {
32872
33409
  ensureLayoutDirSync(base);
32873
- const settingsFile = join12(settingsDir(base), "settings.json");
33410
+ const settingsFile = join13(settingsDir(base), "settings.json");
32874
33411
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
32875
33412
  } catch {
32876
33413
  }
@@ -32894,9 +33431,9 @@ function resolveProviderSafe(resolveProviderFn) {
32894
33431
  }
32895
33432
 
32896
33433
  // ../../packages/plugin-sdk/src/config-inventory.ts
32897
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33434
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32898
33435
  import { homedir as homedir2 } from "os";
32899
- import { basename as basename3, join as join14 } from "path";
33436
+ import { basename as basename3, join as join15 } from "path";
32900
33437
 
32901
33438
  // ../../packages/detections/src/egress/registry.ts
32902
33439
  var EXTRACTOR_VERSION = "1";
@@ -35854,8 +36391,8 @@ function registerBundledPacks() {
35854
36391
  }
35855
36392
 
35856
36393
  // ../../packages/plugin-sdk/src/repo.ts
35857
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
35858
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
36394
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36395
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
35859
36396
 
35860
36397
  // ../../packages/plugin-sdk/src/events.ts
35861
36398
  import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
@@ -35867,8 +36404,8 @@ import { Worker } from "worker_threads";
35867
36404
 
35868
36405
  // ../../packages/plugin-sdk/src/ignore-layers.ts
35869
36406
  var import_ignore = __toESM(require_ignore(), 1);
35870
- import { readFileSync as readFileSync9 } from "fs";
35871
- import { join as join15 } from "path";
36407
+ import { readFileSync as readFileSync10 } from "fs";
36408
+ import { join as join16 } from "path";
35872
36409
 
35873
36410
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
35874
36411
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -35879,24 +36416,24 @@ import {
35879
36416
  fstatSync,
35880
36417
  mkdirSync as mkdirSync2,
35881
36418
  openSync as openSync2,
35882
- readFileSync as readFileSync10,
36419
+ readFileSync as readFileSync11,
35883
36420
  readSync,
35884
36421
  writeFileSync as writeFileSync5
35885
36422
  } from "fs";
35886
- import { join as join16 } from "path";
36423
+ import { join as join17 } from "path";
35887
36424
  var TAIL_BYTES = 256 * 1024;
35888
36425
 
35889
36426
  // ../../packages/plugin-sdk/src/nudge.ts
35890
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
35891
- import { join as join17 } from "path";
36427
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
36428
+ import { join as join18 } from "path";
35892
36429
 
35893
36430
  // ../../packages/plugin-sdk/src/paths.ts
35894
36431
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
35895
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
36432
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
35896
36433
 
35897
36434
  // ../../packages/plugin-sdk/src/project-files.ts
35898
36435
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
35899
- import { basename as basename5, join as join18 } from "path";
36436
+ import { basename as basename5, join as join19 } from "path";
35900
36437
 
35901
36438
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35902
36439
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35932,7 +36469,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
35932
36469
 
35933
36470
  // ../../packages/plugin-sdk/src/throttle.ts
35934
36471
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35935
- import { join as join19 } from "path";
36472
+ import { join as join20 } from "path";
35936
36473
 
35937
36474
  // ../../packages/plugin-sdk/src/tokenize.ts
35938
36475
  function redactedPlaceholder(category) {
@@ -35994,14 +36531,26 @@ var SecretVaultGlue = class {
35994
36531
  }
35995
36532
  async tokenizeText(text, opts) {
35996
36533
  try {
35997
- const findings = opts?.findings ?? this.#selfScan(text);
35998
- const reversible = opts?.reversible;
35999
- const keeps = (finding) => reversible === void 0 || reversible.has(finding);
36000
- if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
36001
- if (findings.length === 0) return { text, pointers: [], degraded: [] };
36534
+ const supplied = opts?.findings;
36535
+ const resolver = opts?.resolver;
36536
+ const scanned = supplied ?? this.#selfScan(text);
36537
+ if (scanned === null) {
36538
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
36539
+ }
36540
+ const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
36541
+ (f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
36542
+ ) : scanned;
36543
+ let reversible = opts?.reversible;
36544
+ if (resolver !== void 0 && reversible === void 0) {
36545
+ reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
36546
+ }
36547
+ const reversibleSet = reversible;
36548
+ const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
36549
+ if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
36002
36550
  const groups = groupSpans(text, findings);
36003
36551
  const pointers = [];
36004
36552
  const degraded = [];
36553
+ const redacted = [];
36005
36554
  let out = text;
36006
36555
  for (const group of [...groups].reverse()) {
36007
36556
  const original = text.slice(group.start, group.end);
@@ -36015,6 +36564,7 @@ var SecretVaultGlue = class {
36015
36564
  degraded.unshift({ category: group.category });
36016
36565
  } else if (!keeps(finding)) {
36017
36566
  replacement = redactedPlaceholder(finding.category);
36567
+ redacted.unshift({ category: finding.category });
36018
36568
  } else {
36019
36569
  replacement = await this.tokenizeValue(finding.rawMatch, {
36020
36570
  ruleId: finding.ruleId,
@@ -36035,9 +36585,9 @@ var SecretVaultGlue = class {
36035
36585
  }
36036
36586
  }
36037
36587
  }
36038
- return { text: out, pointers, degraded };
36588
+ return { text: out, pointers, degraded, redacted };
36039
36589
  } catch {
36040
- return { text: "[REDACTED]", pointers: [], degraded: [] };
36590
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
36041
36591
  }
36042
36592
  }
36043
36593
  async detokenizeText(text, opts) {
@@ -36249,13 +36799,13 @@ function describePointerSafe(token) {
36249
36799
  import {
36250
36800
  mkdirSync as mkdirSync5,
36251
36801
  readdirSync as readdirSync5,
36252
- readFileSync as readFileSync12,
36802
+ readFileSync as readFileSync13,
36253
36803
  renameSync as renameSync5,
36254
36804
  rmSync as rmSync7,
36255
36805
  statSync as statSync9,
36256
36806
  writeFileSync as writeFileSync8
36257
36807
  } from "fs";
36258
- import { dirname as dirname5, join as join20 } from "path";
36808
+ import { dirname as dirname6, join as join21 } from "path";
36259
36809
  var EMPTY_CARRY = Object.freeze({
36260
36810
  tail: "",
36261
36811
  fence: null,
@@ -36452,7 +37002,7 @@ var CARRY_FILE_PREFIX = "display-carry";
36452
37002
  var STALE_CARRY_MS = 15 * 60 * 1e3;
36453
37003
  function carryFilePath(dataDir2, sessionId) {
36454
37004
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
36455
- return join20(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
37005
+ return join21(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
36456
37006
  }
36457
37007
  function parseFence(value) {
36458
37008
  if (typeof value !== "object" || value === null) return null;
@@ -36463,7 +37013,7 @@ function parseFence(value) {
36463
37013
  }
36464
37014
  function loadCarry(file2, keys) {
36465
37015
  try {
36466
- const parsed = JSON.parse(readFileSync12(file2, "utf8"));
37016
+ const parsed = JSON.parse(readFileSync13(file2, "utf8"));
36467
37017
  if (typeof parsed !== "object" || parsed === null) return EMPTY_CARRY;
36468
37018
  const record2 = parsed;
36469
37019
  const revealedCount = record2.messageKey === keys.messageKey && typeof record2.revealedCount === "number" && Number.isFinite(record2.revealedCount) ? record2.revealedCount : 0;
@@ -36516,7 +37066,7 @@ function removeStaleCarryFiles(dir, keep) {
36516
37066
  const cutoff = Date.now() - STALE_CARRY_MS;
36517
37067
  for (const name of readdirSync5(dir)) {
36518
37068
  if (!name.startsWith(CARRY_FILE_PREFIX) || !name.endsWith(".json")) continue;
36519
- const path = join20(dir, name);
37069
+ const path = join21(dir, name);
36520
37070
  if (path === keep) continue;
36521
37071
  try {
36522
37072
  if (statSync9(path).mtimeMs < cutoff) rmSync7(path, { force: true });
@@ -36528,7 +37078,7 @@ function removeStaleCarryFiles(dir, keep) {
36528
37078
  }
36529
37079
  function saveCarry(file2, keys, carry) {
36530
37080
  try {
36531
- const dir = dirname5(file2);
37081
+ const dir = dirname6(file2);
36532
37082
  mkdirSync5(dir, { recursive: true });
36533
37083
  removeStaleCarryFiles(dir, file2);
36534
37084
  writeCarryRecord(file2, keys.blockKey, keys.messageKey, carry);
@@ -36538,7 +37088,7 @@ function saveCarry(file2, keys, carry) {
36538
37088
  function finalizeCarry(file2, keys, carry) {
36539
37089
  try {
36540
37090
  if (carry.revealedCount > 0) {
36541
- mkdirSync5(dirname5(file2), { recursive: true });
37091
+ mkdirSync5(dirname6(file2), { recursive: true });
36542
37092
  writeCarryRecord(file2, null, keys.messageKey, {
36543
37093
  ...EMPTY_CARRY,
36544
37094
  revealedCount: carry.revealedCount
@@ -36547,7 +37097,7 @@ function finalizeCarry(file2, keys, carry) {
36547
37097
  }
36548
37098
  let owned = true;
36549
37099
  try {
36550
- const parsed = JSON.parse(readFileSync12(file2, "utf8"));
37100
+ const parsed = JSON.parse(readFileSync13(file2, "utf8"));
36551
37101
  if (typeof parsed === "object" && parsed !== null) {
36552
37102
  const record2 = parsed;
36553
37103
  owned = record2.blockKey === keys.blockKey || record2.messageKey === keys.messageKey;