@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.
@@ -494,6 +494,7 @@ var require_ignore = __commonJS({
494
494
  // ../../packages/persistence/src/attached-derived.ts
495
495
  import { rmSync } from "fs";
496
496
  import { join } from "path";
497
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
497
498
 
498
499
  // ../../packages/persistence/src/control-plane-credential.ts
499
500
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
@@ -592,6 +593,30 @@ var SQLITE_MIGRATIONS = [
592
593
  {
593
594
  tag: "0022_audit_inspection_ms",
594
595
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
596
+ },
597
+ {
598
+ tag: "0023_secret_vault_user_authorized",
599
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
600
+ },
601
+ {
602
+ tag: "0024_finding_resolution_key_created_index",
603
+ 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`);"
604
+ },
605
+ {
606
+ tag: "0025_audit_capture_attribute_columns",
607
+ 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;"
608
+ },
609
+ {
610
+ tag: "0026_audit_llm_call_usage_columns",
611
+ 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;"
612
+ },
613
+ {
614
+ tag: "0027_audit_llm_usage_index",
615
+ 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;"
616
+ },
617
+ {
618
+ tag: "0028_activity_session_probe_indexes",
619
+ 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"
595
620
  }
596
621
  ];
597
622
 
@@ -22130,6 +22155,26 @@ var AttachTokenResponse = external_exports.union([
22130
22155
  AttachTokenExpired,
22131
22156
  external_exports.object({ status: printable(64) })
22132
22157
  ]);
22158
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22159
+ var DeviceCommand = external_exports.object({
22160
+ id: printable(128).min(1),
22161
+ kind: DeviceCommandKind,
22162
+ issuedAt: printable(64).min(1),
22163
+ expiresAt: printable(64).min(1)
22164
+ }).strict();
22165
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22166
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22167
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22168
+ external_exports.object({
22169
+ outcome: external_exports.literal("reported"),
22170
+ projectsScanned: external_exports.number().int().nonnegative()
22171
+ }).strict(),
22172
+ external_exports.object({
22173
+ outcome: external_exports.literal("failed"),
22174
+ reason: DeviceCommandFailureReason,
22175
+ projectsScanned: external_exports.number().int().nonnegative()
22176
+ }).strict()
22177
+ ]);
22133
22178
 
22134
22179
  // ../../packages/schema/src/zod/registry.ts
22135
22180
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22296,7 +22341,7 @@ var PackManifest = external_exports.object({
22296
22341
  }).meta({ id: "PackManifest" });
22297
22342
 
22298
22343
  // ../../packages/schema/src/zod/detection.ts
22299
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22344
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22300
22345
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22301
22346
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22302
22347
  var DetectionCounts = external_exports.object({
@@ -22433,14 +22478,17 @@ function optional2(key, parsed, raw) {
22433
22478
  function isStringArray(value) {
22434
22479
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22435
22480
  }
22481
+ var ORIGIN_VALUES = { library: true, custom: true };
22482
+ function resolveOrigin(origin) {
22483
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22484
+ }
22436
22485
  function summaryToDetectionListItem(s) {
22437
22486
  return {
22438
22487
  id: `${s.namespace}/${s.packId}`,
22439
22488
  name: s.name,
22440
22489
  version: s.version,
22441
22490
  enabled: s.enabled,
22442
- origin: "library",
22443
- // v1: every installed pack is library origin
22491
+ origin: resolveOrigin(s.origin),
22444
22492
  namespace: s.namespace,
22445
22493
  packId: s.packId,
22446
22494
  ruleCount: s.ruleCount,
@@ -22492,7 +22540,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22492
22540
  name: row.name,
22493
22541
  version: row.version,
22494
22542
  enabled: row.enabled,
22495
- origin: "library",
22543
+ origin: resolveOrigin(row.origin),
22496
22544
  namespace: row.namespace,
22497
22545
  packId: row.packId,
22498
22546
  ruleCount: row.rules.length,
@@ -22512,16 +22560,20 @@ function splitDetectionId(id) {
22512
22560
  }
22513
22561
  function buildDetectionsList(summaries, query) {
22514
22562
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22563
+ const originOf = (s) => resolveOrigin(s.origin);
22515
22564
  const counts = {
22516
22565
  all: summaries.length,
22517
- library: summaries.length,
22518
- // all origin=library in v1
22519
- custom: 0,
22566
+ library: summaries.filter((s) => originOf(s) === "library").length,
22567
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22568
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22569
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22570
+ // place, and that state does not exist — editing a library pack forks it. See
22571
+ // OriginEnum.
22520
22572
  customized: 0,
22521
22573
  updates: withUpdate.length
22522
22574
  };
22523
22575
  const filter = query.filter;
22524
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22576
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22525
22577
  if (query.q) {
22526
22578
  const q = query.q.toLowerCase();
22527
22579
  filtered = filtered.filter(
@@ -22601,8 +22653,9 @@ var Event = external_exports.object({
22601
22653
  metadata: EventMetadata.optional()
22602
22654
  }).meta({ id: "Event" });
22603
22655
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22656
+ var INGEST_BATCH_MAX = 100;
22604
22657
  var IngestBatch = external_exports.object({
22605
- events: external_exports.array(IngestEvent).min(1).max(100),
22658
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22606
22659
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22607
22660
  // additionally rejects any event whose contentHash the store has already
22608
22661
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23158,6 +23211,257 @@ var PatchInstalledPackRequest = external_exports.object({
23158
23211
  message: "At least one field must be provided"
23159
23212
  }).meta({ id: "PatchInstalledPackRequest" });
23160
23213
 
23214
+ // ../../packages/schema/src/zod/policy.ts
23215
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23216
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23217
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23218
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23219
+ var Policy = external_exports.object({
23220
+ id: external_exports.guid(),
23221
+ scope: PolicyScope,
23222
+ target: PolicyTarget,
23223
+ action: ActionTaken,
23224
+ enabled: external_exports.boolean().default(true),
23225
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23226
+ // Display name — optional so older policy rows without name still parse.
23227
+ // Added for the findings API (policy.name column migration).
23228
+ name: external_exports.string().optional(),
23229
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23230
+ // which row this is. A producer that collapses several rows onto one target
23231
+ // must carry the marker onto whichever row survives, or the collapse decides
23232
+ // the answer; a survivor may therefore be a built-in expansion still marked
23233
+ // 'authored' because an authored sibling targeted the same thing.
23234
+ // Optional so an older producer — and an older on-disk cache — still parses;
23235
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23236
+ //
23237
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23238
+ // built-in archetype catalog entry a policy is, which every catalog surface
23239
+ // reads and which a caller may state. This one is a statement the PRODUCER
23240
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23241
+ // — the CRUD routes neither accept nor set it.
23242
+ //
23243
+ // A device consumes this in exactly one direction: an 'authored' policy
23244
+ // arriving from a control plane marks the rules it targets as not
23245
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23246
+ // which is what makes it safe to honour from an unsigned cache — the same
23247
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23248
+ provenance: PolicyProvenance.optional()
23249
+ }).meta({ id: "Policy" });
23250
+ var PolicyBundle = external_exports.object({
23251
+ version: external_exports.string(),
23252
+ policies: external_exports.array(Policy),
23253
+ // Rules from the installed marketplace packs (snapshotted by the
23254
+ // control plane). The plugin registers these in addition to its bundled
23255
+ // packs. Optional so older backends — and older on-disk caches — that omit
23256
+ // the field still parse; consumers read `bundle.rules ?? []`.
23257
+ rules: external_exports.array(Rule).optional(),
23258
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23259
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23260
+ // after reading the user's installed snapshot (installed_packs, enabled
23261
+ // packs only), which is how detection updates stay manual: new bundled
23262
+ // rules run only after the user applies the pack update. Absent/false keeps
23263
+ // the historical composition (bundled packs + rules) — older caches.
23264
+ rulesComplete: external_exports.boolean().optional(),
23265
+ // Active detection exceptions, evaluation subset only (see
23266
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23267
+ // on-disk caches — that omit the field still parse; consumers read
23268
+ // `bundle.exceptions ?? []`.
23269
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23270
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23271
+ // A second axis over the same `redact` action, carried beside the policies
23272
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23273
+ // widening Policy itself would change a persisted shape to express something
23274
+ // only the in-memory bundle needs. Optional so an older producer — or an
23275
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23276
+ // pre-existing one-way behaviour, which is the safe direction to default.
23277
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23278
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23279
+ // from a versioned installed pack. Optional so older backends — and older
23280
+ // on-disk caches — that omit the field still parse; consumers fall back to
23281
+ // the rule's own spec version. NOT the bundle version above — see
23282
+ // installedRuleset's ruleVersions for the source of truth.
23283
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23284
+ // Model ids (the raw `model` string a harness reports, e.g.
23285
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23286
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23287
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23288
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23289
+ // unenforced behaviour that predates this field and the safe direction to
23290
+ // default.
23291
+ //
23292
+ // Ids, not display names: the governance decision is keyed on the exact
23293
+ // string the harness reports (`model_status_override.versionId` in the
23294
+ // control plane), so no name resolution stands between the decision and the
23295
+ // comparison.
23296
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23297
+ customKeywords: external_exports.array(external_exports.string()),
23298
+ fetchedAt: external_exports.iso.datetime()
23299
+ }).meta({ id: "PolicyBundle" });
23300
+ var POLICY_BUNDLE_SHAPE_ID = [
23301
+ ...Object.keys(PolicyBundle.shape),
23302
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23303
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23304
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23305
+ ].sort().join(",");
23306
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23307
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23308
+ var CATEGORY_PEAK_SEVERITY = {
23309
+ secret: "critical",
23310
+ financial: "critical",
23311
+ // core-financial/credit-card
23312
+ code_flaw: "critical",
23313
+ pii: "high",
23314
+ phi: "high",
23315
+ custom: "high",
23316
+ // user-defined; conservative
23317
+ code_context: "low",
23318
+ config: "low"
23319
+ // observe-only; floors to monitor regardless
23320
+ };
23321
+ function severityFloorPolicy(category) {
23322
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23323
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23324
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23325
+ }
23326
+ function severityFloorPosture() {
23327
+ const out = {};
23328
+ for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23329
+ return out;
23330
+ }
23331
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23332
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23333
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23334
+ id: "RedactFallback"
23335
+ });
23336
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23337
+ var BUILTIN_POLICY_SPECS = {
23338
+ monitor: {
23339
+ name: "Monitor",
23340
+ action: "log",
23341
+ reversible: false,
23342
+ description: "Log every match for audit. The request is allowed through untouched."
23343
+ },
23344
+ warn: {
23345
+ name: "Warn",
23346
+ action: "warn",
23347
+ reversible: false,
23348
+ description: "Allow the request, but warn the user inline before it is sent."
23349
+ },
23350
+ redact: {
23351
+ name: "Redact",
23352
+ action: "redact",
23353
+ reversible: false,
23354
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23355
+ },
23356
+ vault: {
23357
+ name: "Redact & Vault",
23358
+ action: "redact",
23359
+ reversible: true,
23360
+ 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."
23361
+ },
23362
+ block: {
23363
+ name: "Block",
23364
+ action: "block",
23365
+ reversible: false,
23366
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23367
+ }
23368
+ };
23369
+ function builtinPolicyToAction(id) {
23370
+ return BUILTIN_POLICY_SPECS[id].action;
23371
+ }
23372
+ var PALETTE_WEAKEST_FIRST = [
23373
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23374
+ ];
23375
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23376
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23377
+ );
23378
+ var ACTION_STRENGTH_ORDER = [
23379
+ ...BELOW_PALETTE,
23380
+ ...PALETTE_WEAKEST_FIRST
23381
+ ];
23382
+ function actionRank(action) {
23383
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23384
+ }
23385
+ function isActionAtLeast(action, floor) {
23386
+ return actionRank(action) >= actionRank(floor);
23387
+ }
23388
+ function strongerAction(a, b) {
23389
+ return actionRank(a) >= actionRank(b) ? a : b;
23390
+ }
23391
+ function weakestBuiltinAtLeast(floor) {
23392
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23393
+ }
23394
+ var PackPolicyFloor = external_exports.object({
23395
+ /**
23396
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23397
+ * rather than a raw ActionTaken because that is the vocabulary the user
23398
+ * picks from — a floor a UI cannot name is one it cannot explain.
23399
+ */
23400
+ floor: BuiltinPolicyId,
23401
+ /**
23402
+ * True when the organization AUTHORED a policy governing this pack rather
23403
+ * than stating a minimum: it gave the answer, so the pack is not
23404
+ * re-assignable locally in either direction.
23405
+ */
23406
+ locked: external_exports.boolean()
23407
+ }).describe("PackPolicyFloor");
23408
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23409
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23410
+ );
23411
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23412
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23413
+ );
23414
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23415
+ function builtinPolicyIsReversible(id) {
23416
+ return BUILTIN_POLICY_SPECS[id].reversible;
23417
+ }
23418
+ function policyIdIsReversible(policyId) {
23419
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23420
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23421
+ return builtinPolicyIsReversible(id);
23422
+ }
23423
+ var DEFAULT_ACTIONS = Object.fromEntries(
23424
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23425
+ );
23426
+ var BUILTIN_POLICIES = Object.fromEntries(
23427
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23428
+ );
23429
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23430
+ function policyIdToAction(policyId) {
23431
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23432
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23433
+ return BUILTIN_POLICIES[id].action;
23434
+ }
23435
+ var UsedByItem = external_exports.object({
23436
+ id: external_exports.string(),
23437
+ name: external_exports.string(),
23438
+ ruleCount: external_exports.number().int().nonnegative(),
23439
+ enabled: external_exports.boolean()
23440
+ }).meta({ id: "UsedByItem" });
23441
+ var PolicyListItem = external_exports.object({
23442
+ id: external_exports.string(),
23443
+ kind: PolicyKind,
23444
+ name: external_exports.string(),
23445
+ enabled: external_exports.boolean(),
23446
+ usedByCount: external_exports.number().int().nonnegative()
23447
+ }).meta({ id: "PolicyListItem" });
23448
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23449
+ var PolicyDetail = external_exports.object({
23450
+ specVersion: external_exports.literal(1),
23451
+ id: external_exports.string(),
23452
+ kind: PolicyKind,
23453
+ name: external_exports.string(),
23454
+ enabled: external_exports.boolean(),
23455
+ description: external_exports.string(),
23456
+ usedBy: external_exports.array(UsedByItem)
23457
+ }).meta({ id: "PolicyDetail" });
23458
+ var PolicyStatsResponse = external_exports.object({
23459
+ policies: external_exports.number().int().nonnegative(),
23460
+ builtin: external_exports.number().int().nonnegative(),
23461
+ custom: external_exports.number().int().nonnegative(),
23462
+ detectionsGoverned: external_exports.number().int().nonnegative()
23463
+ }).meta({ id: "PolicyStatsResponse" });
23464
+
23161
23465
  // ../../packages/schema/src/zod/vault.ts
23162
23466
  var POINTER_FORMAT_VERSION = 2;
23163
23467
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23195,6 +23499,14 @@ var VaultEntry = external_exports.object({
23195
23499
  // How many times this value has been detected on this machine — the reuse
23196
23500
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23197
23501
  occurrenceCount: external_exports.number().int().nonnegative(),
23502
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23503
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23504
+ // one row however many paths vault it, so this is what tells a policy sweep
23505
+ // that the row carries somebody's own instruction and not just an assignment
23506
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23507
+ // vaulting of the same value must never clear it — what the user said about
23508
+ // the value does not expire.
23509
+ userAuthorized: external_exports.boolean(),
23198
23510
  firstSeen: external_exports.string(),
23199
23511
  lastSeen: external_exports.string()
23200
23512
  });
@@ -23316,7 +23628,7 @@ function isVaultConsentValid(consent) {
23316
23628
  }
23317
23629
 
23318
23630
  // ../../packages/schema/src/zod/local.ts
23319
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23631
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23320
23632
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23321
23633
  var RunMode = external_exports.enum(["standalone", "attached"]);
23322
23634
  var ControlPlaneConnection = external_exports.object({
@@ -23361,6 +23673,19 @@ var WorkspaceSettings = external_exports.object({
23361
23673
  vaultKeyCustody: VaultKeyCustody.default("file"),
23362
23674
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23363
23675
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23676
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23677
+ // place. Not a handling policy: the policy has already resolved to redact,
23678
+ // and this only says what happens when the host offers no channel to carry it
23679
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23680
+ // Claude Code decline to mask a field that EXECUTES because masking would
23681
+ // change what runs. Per FIELD rather than per host, so a host that can
23682
+ // rewrite some inputs keeps true redaction on those.
23683
+ //
23684
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23685
+ // an attached machine's merge is `strongerAction` over the one action ladder
23686
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23687
+ // word and stays out of the stored value.
23688
+ redactFallback: RedactFallback.default("warn"),
23364
23689
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23365
23690
  onboardedAt: external_exports.iso.datetime().optional(),
23366
23691
  // Records that the user consented to sending findings to the model API for
@@ -23368,15 +23693,20 @@ var WorkspaceSettings = external_exports.object({
23368
23693
  // Absent until granted; a stale payloadVersion means the consent no longer
23369
23694
  // covers the current payload and must be re-granted.
23370
23695
  modelJudgeConsent: ModelJudgeConsent.optional(),
23371
- // Records that the user consented to sending the activity already recorded on
23372
- // this machine to the deployment it is attached to, along with the payload
23373
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23374
- // a different endpoint or an older payload no longer counts.
23696
+ // Records that the user consented to the DEFERRED send — the outbox along
23697
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23698
+ // that covers both the pre-attach backlog and undelivered captures (which
23699
+ // carry prompt/reply text in `content`); the key name predates the widening.
23700
+ // Absent until granted, and a grant for a different endpoint or an older
23701
+ // payload no longer counts.
23375
23702
  historySyncConsent: HistorySyncConsent.optional()
23376
23703
  });
23377
23704
  function defaultWorkspaceSettings() {
23378
23705
  return WorkspaceSettings.parse({});
23379
23706
  }
23707
+ function isAttached(settings) {
23708
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23709
+ }
23380
23710
  function toInventoryRow(input2, id, now) {
23381
23711
  return {
23382
23712
  id,
@@ -23496,7 +23826,8 @@ var ManagedSettingKey = external_exports.enum([
23496
23826
  "vaultKeyCustody",
23497
23827
  "vaultInlineReveal",
23498
23828
  "modelJudgeConsent",
23499
- "dataSharesInPlace"
23829
+ "dataSharesInPlace",
23830
+ "redactFallback"
23500
23831
  ]).meta({ id: "ManagedSettingKey" });
23501
23832
  var ManagedSettingsValues = external_exports.object({
23502
23833
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
@@ -23509,7 +23840,8 @@ var ManagedSettingsValues = external_exports.object({
23509
23840
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23510
23841
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23511
23842
  modelJudgeConsent: external_exports.boolean().optional(),
23512
- dataSharesInPlace: external_exports.boolean().optional()
23843
+ dataSharesInPlace: external_exports.boolean().optional(),
23844
+ redactFallback: RedactFallback.optional()
23513
23845
  }).meta({ id: "ManagedSettingsValues" });
23514
23846
  var ManagedSettings = external_exports.object({
23515
23847
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23525,191 +23857,6 @@ var ManagedSettings = external_exports.object({
23525
23857
  }).meta({ id: "ManagedSettings" });
23526
23858
  var NO_MANAGED_CONTEXT = { present: false, lockedFields: [] };
23527
23859
 
23528
- // ../../packages/schema/src/zod/policy.ts
23529
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23530
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23531
- var Policy = external_exports.object({
23532
- id: external_exports.guid(),
23533
- scope: PolicyScope,
23534
- target: PolicyTarget,
23535
- action: ActionTaken,
23536
- enabled: external_exports.boolean().default(true),
23537
- customKeywords: external_exports.array(external_exports.string()).optional(),
23538
- // Display name — optional so older policy rows without name still parse.
23539
- // Added for the findings API (policy.name column migration).
23540
- name: external_exports.string().optional()
23541
- }).meta({ id: "Policy" });
23542
- var PolicyBundle = external_exports.object({
23543
- version: external_exports.string(),
23544
- policies: external_exports.array(Policy),
23545
- // Rules from the installed marketplace packs (snapshotted by the
23546
- // control plane). The plugin registers these in addition to its bundled
23547
- // packs. Optional so older backends — and older on-disk caches — that omit
23548
- // the field still parse; consumers read `bundle.rules ?? []`.
23549
- rules: external_exports.array(Rule).optional(),
23550
- // When true, `rules` IS the complete effective ruleset and the runtime must
23551
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23552
- // after reading the user's installed snapshot (installed_packs, enabled
23553
- // packs only), which is how detection updates stay manual: new bundled
23554
- // rules run only after the user applies the pack update. Absent/false keeps
23555
- // the historical composition (bundled packs + rules) — older caches.
23556
- rulesComplete: external_exports.boolean().optional(),
23557
- // Active detection exceptions, evaluation subset only (see
23558
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23559
- // on-disk caches — that omit the field still parse; consumers read
23560
- // `bundle.exceptions ?? []`.
23561
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23562
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23563
- // A second axis over the same `redact` action, carried beside the policies
23564
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23565
- // widening Policy itself would change a persisted shape to express something
23566
- // only the in-memory bundle needs. Optional so an older producer — or an
23567
- // older on-disk cache — still parses; consumers read `?? []` and get the
23568
- // pre-existing one-way behaviour, which is the safe direction to default.
23569
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23570
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23571
- // from a versioned installed pack. Optional so older backends — and older
23572
- // on-disk caches — that omit the field still parse; consumers fall back to
23573
- // the rule's own spec version. NOT the bundle version above — see
23574
- // installedRuleset's ruleVersions for the source of truth.
23575
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23576
- // Model ids (the raw `model` string a harness reports, e.g.
23577
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23578
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23579
- // one (UserPromptSubmit). Optional so an older backend — and an older
23580
- // on-disk cache — still parses; consumers read `?? []`, which is the
23581
- // unenforced behaviour that predates this field and the safe direction to
23582
- // default.
23583
- //
23584
- // Ids, not display names: the governance decision is keyed on the exact
23585
- // string the harness reports (`model_status_override.versionId` in the
23586
- // control plane), so no name resolution stands between the decision and the
23587
- // comparison.
23588
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23589
- customKeywords: external_exports.array(external_exports.string()),
23590
- fetchedAt: external_exports.iso.datetime()
23591
- }).meta({ id: "PolicyBundle" });
23592
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23593
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23594
- var CATEGORY_PEAK_SEVERITY = {
23595
- secret: "critical",
23596
- financial: "critical",
23597
- // core-financial/credit-card
23598
- code_flaw: "critical",
23599
- pii: "high",
23600
- phi: "high",
23601
- custom: "high",
23602
- // user-defined; conservative
23603
- code_context: "low",
23604
- config: "low"
23605
- // observe-only; floors to monitor regardless
23606
- };
23607
- function severityFloorPolicy(category) {
23608
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23609
- const peak = CATEGORY_PEAK_SEVERITY[category];
23610
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23611
- }
23612
- function severityFloorPosture() {
23613
- const out = {};
23614
- for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23615
- return out;
23616
- }
23617
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23618
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23619
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23620
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23621
- var BUILTIN_POLICY_SPECS = {
23622
- monitor: {
23623
- name: "Monitor",
23624
- action: "log",
23625
- reversible: false,
23626
- description: "Log every match for audit. The request is allowed through untouched."
23627
- },
23628
- warn: {
23629
- name: "Warn",
23630
- action: "warn",
23631
- reversible: false,
23632
- description: "Allow the request, but warn the user inline before it is sent."
23633
- },
23634
- redact: {
23635
- name: "Redact",
23636
- action: "redact",
23637
- reversible: false,
23638
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23639
- },
23640
- vault: {
23641
- name: "Redact & Vault",
23642
- action: "redact",
23643
- reversible: true,
23644
- 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."
23645
- },
23646
- block: {
23647
- name: "Block",
23648
- action: "block",
23649
- reversible: false,
23650
- description: "Refuse the request entirely whenever any rule in this detection matches."
23651
- }
23652
- };
23653
- function builtinPolicyToAction(id) {
23654
- return BUILTIN_POLICY_SPECS[id].action;
23655
- }
23656
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23657
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23658
- );
23659
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23660
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23661
- );
23662
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23663
- function builtinPolicyIsReversible(id) {
23664
- return BUILTIN_POLICY_SPECS[id].reversible;
23665
- }
23666
- function policyIdIsReversible(policyId) {
23667
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23668
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23669
- return builtinPolicyIsReversible(id);
23670
- }
23671
- var DEFAULT_ACTIONS = Object.fromEntries(
23672
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23673
- );
23674
- var BUILTIN_POLICIES = Object.fromEntries(
23675
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23676
- );
23677
- var DEFAULT_PACK_POLICY_ID = "monitor";
23678
- function policyIdToAction(policyId) {
23679
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23680
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23681
- return BUILTIN_POLICIES[id].action;
23682
- }
23683
- var UsedByItem = external_exports.object({
23684
- id: external_exports.string(),
23685
- name: external_exports.string(),
23686
- ruleCount: external_exports.number().int().nonnegative(),
23687
- enabled: external_exports.boolean()
23688
- }).meta({ id: "UsedByItem" });
23689
- var PolicyListItem = external_exports.object({
23690
- id: external_exports.string(),
23691
- kind: PolicyKind,
23692
- name: external_exports.string(),
23693
- enabled: external_exports.boolean(),
23694
- usedByCount: external_exports.number().int().nonnegative()
23695
- }).meta({ id: "PolicyListItem" });
23696
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23697
- var PolicyDetail = external_exports.object({
23698
- specVersion: external_exports.literal(1),
23699
- id: external_exports.string(),
23700
- kind: PolicyKind,
23701
- name: external_exports.string(),
23702
- enabled: external_exports.boolean(),
23703
- description: external_exports.string(),
23704
- usedBy: external_exports.array(UsedByItem)
23705
- }).meta({ id: "PolicyDetail" });
23706
- var PolicyStatsResponse = external_exports.object({
23707
- policies: external_exports.number().int().nonnegative(),
23708
- builtin: external_exports.number().int().nonnegative(),
23709
- custom: external_exports.number().int().nonnegative(),
23710
- detectionsGoverned: external_exports.number().int().nonnegative()
23711
- }).meta({ id: "PolicyStatsResponse" });
23712
-
23713
23860
  // ../../packages/schema/src/zod/project-files.ts
23714
23861
  var ProjectFileInput = external_exports.object({
23715
23862
  path: external_exports.string().min(1),
@@ -23955,10 +24102,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23955
24102
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23956
24103
 
23957
24104
  // ../../packages/schema/src/zod/settings-action.ts
24105
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24106
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23958
24107
  var SaveSettingsInput = external_exports.object({
23959
24108
  historicalAccess: external_exports.string(),
23960
- modelJudgeConsent: external_exports.boolean(),
23961
- historySyncConsent: external_exports.boolean(),
24109
+ modelJudgeConsent: ModelJudgeConsentChoice,
24110
+ historySyncConsent: HistorySyncConsentChoice,
23962
24111
  vaultConsent: external_exports.string(),
23963
24112
  vaultInlineReveal: external_exports.string()
23964
24113
  });
@@ -24108,9 +24257,9 @@ function deriveReviewReasons(trust, transports) {
24108
24257
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24109
24258
  return reasons;
24110
24259
  }
24111
- function buildReviewInfo(trust, transports) {
24260
+ function buildReviewInfo(trust, transports, decided) {
24112
24261
  const reasons = deriveReviewReasons(trust, transports);
24113
- return { needsReview: reasons.length > 0, reasons };
24262
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24114
24263
  }
24115
24264
  function distinctTransports(transports) {
24116
24265
  return Array.from(new Set(transports));
@@ -24201,8 +24350,8 @@ function writeOwnerOnlyFileSync(file2, data) {
24201
24350
  }
24202
24351
 
24203
24352
  // ../../packages/persistence/src/database.ts
24204
- import { randomUUID as randomUUID10 } from "crypto";
24205
- import { join as join4, sep } from "path";
24353
+ import { randomUUID as randomUUID11 } from "crypto";
24354
+ import { dirname as dirname2, join as join7, sep } from "path";
24206
24355
  import { DatabaseSync } from "node:sqlite";
24207
24356
 
24208
24357
  // ../../packages/persistence/src/ids.ts
@@ -24446,6 +24595,10 @@ function allRows(stmt, params) {
24446
24595
  if (Array.isArray(params)) return stmt.all(...params);
24447
24596
  return stmt.all(params);
24448
24597
  }
24598
+ function* iterateRows(stmt, params) {
24599
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24600
+ for (const row of rows) yield row;
24601
+ }
24449
24602
  function getRow(stmt, params) {
24450
24603
  if (params === void 0) return stmt.get();
24451
24604
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24914,10 +25067,17 @@ function ensureSyncedAtColumn(db, table2) {
24914
25067
  if (!columns.includes("sync_claimed_at")) {
24915
25068
  db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
24916
25069
  }
25070
+ if (!columns.includes("outbox_owed")) {
25071
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25072
+ }
24917
25073
  db.exec(
24918
25074
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24919
25075
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24920
25076
  );
25077
+ db.exec(
25078
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25079
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25080
+ );
24921
25081
  db.exec(
24922
25082
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24923
25083
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25022,7 +25182,6 @@ function decodeKeysetCursor(cursor) {
25022
25182
  // ../../packages/persistence/src/repositories/activity.ts
25023
25183
  var DAY_MS = 864e5;
25024
25184
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25025
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25026
25185
  function defaultTimeZone() {
25027
25186
  try {
25028
25187
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25077,6 +25236,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25077
25236
  error: "error",
25078
25237
  active: "active"
25079
25238
  };
25239
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25080
25240
  function safeParseStringArray(raw) {
25081
25241
  if (!raw) return [];
25082
25242
  const parsed = safeJson(raw, null);
@@ -25150,6 +25310,37 @@ var TIMELINE_COLUMNS = `
25150
25310
  json_extract(attributes, '$.targetId') AS target_id,
25151
25311
  json_extract(attributes, '$.internal') AS internal,
25152
25312
  json_extract(attributes, '$.flagged') AS flagged`;
25313
+ var LLM_USAGE_SELECT = `
25314
+ SELECT root_session_id AS sessionId,
25315
+ provider,
25316
+ model,
25317
+ service_tier AS serviceTier,
25318
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25319
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25320
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25321
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25322
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25323
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25324
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25325
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25326
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25327
+ function usageLeaves(rows) {
25328
+ return rows.map((row) => {
25329
+ const attributes = {
25330
+ input_tokens: row.inputTokens,
25331
+ output_tokens: row.outputTokens,
25332
+ cache_creation_input_tokens: row.cacheCreationTokens,
25333
+ cache_read_input_tokens: row.cacheReadTokens,
25334
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25335
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25336
+ web_search_requests: row.webSearchRequests
25337
+ };
25338
+ if (row.provider !== null) attributes.provider = row.provider;
25339
+ if (row.model !== null) attributes.model = row.model;
25340
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25341
+ return { sessionId: row.sessionId, attributes };
25342
+ });
25343
+ }
25153
25344
  var SESSION_ROOT = `event_type = 'session'`;
25154
25345
  var HAS_ACTIVITY = `EXISTS (
25155
25346
  SELECT 1 FROM audit_events c
@@ -25175,16 +25366,17 @@ var SqliteActivityRepository = class {
25175
25366
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25176
25367
  const liveNow = countScalar(
25177
25368
  this.db,
25178
- `SELECT count(*) AS n FROM audit_events s
25369
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25179
25370
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25180
- AND max(
25181
- s.started_at,
25182
- coalesce(
25183
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25184
- s.started_at
25185
- )
25186
- ) >= ?`,
25187
- [liveThreshold]
25371
+ AND s.id IN (
25372
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25373
+ UNION
25374
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25375
+ WHERE started_at >= ?
25376
+ UNION
25377
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25378
+ WHERE ended_at >= ?)`,
25379
+ [liveThreshold, liveThreshold, liveThreshold]
25188
25380
  );
25189
25381
  const toolCallsToday = countScalar(
25190
25382
  this.db,
@@ -25314,7 +25506,7 @@ var SqliteActivityRepository = class {
25314
25506
  this.db.prepare(
25315
25507
  `SELECT ${TIMELINE_COLUMNS}
25316
25508
  FROM audit_events
25317
- WHERE id = ? OR root_session_id = ?
25509
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25318
25510
  ORDER BY started_at ASC, id ASC`
25319
25511
  ),
25320
25512
  [sessionId, sessionId]
@@ -25327,14 +25519,14 @@ var SqliteActivityRepository = class {
25327
25519
  coalesce(sum(output_tokens), 0) AS output,
25328
25520
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25329
25521
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25330
- FROM audit_events
25522
+ FROM audit_events INDEXED BY idx_audit_session_type
25331
25523
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25332
25524
  ),
25333
25525
  [sessionId]
25334
25526
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25335
25527
  const primaryModel = getRow(
25336
25528
  this.db.prepare(
25337
- `SELECT model, provider FROM audit_events
25529
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25338
25530
  WHERE root_session_id = ? AND event_type = 'llm_call'
25339
25531
  ORDER BY started_at ASC, id ASC
25340
25532
  LIMIT 1`
@@ -25345,7 +25537,7 @@ var SqliteActivityRepository = class {
25345
25537
  this.db.prepare(
25346
25538
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25347
25539
  count(*) AS n
25348
- FROM audit_events
25540
+ FROM audit_events INDEXED BY idx_audit_session
25349
25541
  WHERE root_session_id = ? AND event_type = 'tool_call'
25350
25542
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25351
25543
  ),
@@ -25353,7 +25545,7 @@ var SqliteActivityRepository = class {
25353
25545
  );
25354
25546
  const modelRows = allRows(
25355
25547
  this.db.prepare(
25356
- `SELECT DISTINCT model FROM audit_events
25548
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25357
25549
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25358
25550
  ORDER BY model`
25359
25551
  ),
@@ -25362,7 +25554,7 @@ var SqliteActivityRepository = class {
25362
25554
  const derivedModels = modelRows.map((r) => r.model);
25363
25555
  const commits = countScalar(
25364
25556
  this.db,
25365
- `SELECT count(*) AS n FROM audit_events
25557
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25366
25558
  WHERE root_session_id = ? AND event_type = 'commit'`,
25367
25559
  [sessionId]
25368
25560
  );
@@ -25398,25 +25590,57 @@ var SqliteActivityRepository = class {
25398
25590
  return Promise.resolve(session);
25399
25591
  }
25400
25592
  /**
25401
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25402
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25403
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25404
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25405
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25406
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25593
+ * Cross-session token report — every `llm_call` in the store (or in a
25594
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25595
+ * session, with USD cost DERIVED at read time via the shared
25596
+ * `defaultCostModel` (never stored). The caller collapses these onto
25597
+ * per-model rows with `aggregateTokenUsage`.
25598
+ *
25599
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25600
+ * the members the rollup sums — and priced once per group, which is exact
25601
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25602
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25603
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25604
+ * the index stores the values once, at write, and answers the same window in
25605
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25606
+ * planner prefers the general event-type index and fetches every row to
25607
+ * recompute the columns it could have read. The index is one every open
25608
+ * store carries, since opening runs the migrations, so the hard requirement
25609
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25610
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25611
+ * per call, no bag parsed.
25407
25612
  */
25408
25613
  tokenReports(fromMs) {
25409
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25410
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25614
+ const rows = allRows(
25615
+ this.db.prepare(
25616
+ `${LLM_USAGE_SELECT}
25617
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25618
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25619
+ ${LLM_USAGE_GROUP}`
25620
+ ),
25621
+ fromMs === void 0 ? void 0 : [fromMs]
25622
+ );
25623
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25411
25624
  }
25412
25625
  /**
25413
- * One session's token report — its `llm_call` leaves grouped per (provider,
25414
- * model) with derived cost, or `null` when the session made no `llm_call`s
25415
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25416
- * breakdown + estimated cost.
25626
+ * One session's token report — its `llm_call`s grouped per (provider,
25627
+ * model, tier) with derived cost, or `null` when the session made no
25628
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25629
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25630
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25631
+ * it replaces walked every `llm_call` in the store to find one session's.
25417
25632
  */
25418
25633
  tokenReportForSession(sessionId) {
25419
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25634
+ const rows = allRows(
25635
+ this.db.prepare(
25636
+ `${LLM_USAGE_SELECT}
25637
+ FROM audit_events
25638
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25639
+ ${LLM_USAGE_GROUP}`
25640
+ ),
25641
+ [sessionId]
25642
+ );
25643
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25420
25644
  return Promise.resolve(reports[0] ?? null);
25421
25645
  }
25422
25646
  /**
@@ -25440,42 +25664,6 @@ var SqliteActivityRepository = class {
25440
25664
  for (const row of rows) seen.add(toHarness(row.harness));
25441
25665
  return Promise.resolve([...seen]);
25442
25666
  }
25443
- /**
25444
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25445
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25446
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25447
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25448
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25449
- */
25450
- readLlmCallLeaves(opts = {}) {
25451
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25452
- const params = [];
25453
- if (opts.sessionId !== void 0) {
25454
- conditions.push("root_session_id = ?");
25455
- params.push(opts.sessionId);
25456
- }
25457
- if (opts.fromMs !== void 0) {
25458
- conditions.push("started_at >= ?");
25459
- params.push(opts.fromMs);
25460
- }
25461
- const rows = allRows(
25462
- this.db.prepare(
25463
- `SELECT root_session_id AS sessionId, attributes
25464
- FROM audit_events
25465
- WHERE ${conditions.join(" AND ")}`
25466
- ),
25467
- params
25468
- );
25469
- return mapRowsTolerant(
25470
- rows.filter(
25471
- (row) => row.sessionId !== null
25472
- ),
25473
- (row) => ({
25474
- sessionId: row.sessionId,
25475
- attributes: JSON.parse(row.attributes)
25476
- })
25477
- );
25478
- }
25479
25667
  /**
25480
25668
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25481
25669
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25490,20 +25678,23 @@ var SqliteActivityRepository = class {
25490
25678
  const inClause = placeholders(sessionIds.length);
25491
25679
  const lastActivityRows = allRows(
25492
25680
  this.db.prepare(
25493
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25494
- WHERE root_session_id IN (${inClause})
25495
- GROUP BY root_session_id`
25681
+ `SELECT ids.value AS id,
25682
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25683
+ (SELECT max(ended_at) FROM audit_events e
25684
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25685
+ FROM json_each(?) AS ids`
25496
25686
  ),
25497
- sessionIds
25687
+ [JSON.stringify(sessionIds)]
25498
25688
  );
25499
25689
  for (const row of lastActivityRows) {
25500
- if (row.id === null) continue;
25501
25690
  const entry = result.get(row.id);
25502
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25691
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25692
+ if (entry && last > 0) entry.lastActivityMs = last;
25503
25693
  }
25504
25694
  const turnsRows = allRows(
25505
25695
  this.db.prepare(
25506
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25696
+ `SELECT root_session_id AS id, count(*) AS n
25697
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25507
25698
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25508
25699
  GROUP BY root_session_id`
25509
25700
  ),
@@ -25518,7 +25709,7 @@ var SqliteActivityRepository = class {
25518
25709
  this.db.prepare(
25519
25710
  `SELECT root_session_id AS id,
25520
25711
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25521
- FROM audit_events
25712
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25522
25713
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25523
25714
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25524
25715
  GROUP BY root_session_id`
@@ -25548,7 +25739,7 @@ var SqliteActivityRepository = class {
25548
25739
  this.db.prepare(
25549
25740
  `SELECT root_session_id AS id,
25550
25741
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25551
- FROM audit_events
25742
+ FROM audit_events INDEXED BY idx_audit_session_share
25552
25743
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25553
25744
  GROUP BY root_session_id`
25554
25745
  ),
@@ -26577,7 +26768,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26577
26768
 
26578
26769
  // ../../packages/persistence/src/repositories/findings.ts
26579
26770
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26580
- var SCAN_BATCH_ROWS = 1e3;
26581
26771
  var DEFAULT_LOCATIONS_LIMIT = 100;
26582
26772
  var LOCATION_RULE_IDS_CAP = 20;
26583
26773
  function compareLocationOrder(a, b) {
@@ -26606,6 +26796,25 @@ function deriveInstanceStatus(row) {
26606
26796
  latestResolutionStatus: row.latest_status
26607
26797
  });
26608
26798
  }
26799
+ function toFlatFindingRow(r) {
26800
+ return {
26801
+ id: r.id,
26802
+ ruleId: r.rule_id,
26803
+ category: r.category,
26804
+ severity: r.severity,
26805
+ maskedMatch: r.masked_match,
26806
+ actionTaken: r.action_taken,
26807
+ confidence: r.confidence,
26808
+ occurredAt: epochMillisToIso(r.occurred_at),
26809
+ sourceTool: r.source_tool,
26810
+ repo: r.repo ?? "",
26811
+ file: r.file ?? "",
26812
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26813
+ eventId: r.event_id,
26814
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26815
+ status: deriveInstanceStatus(r)
26816
+ };
26817
+ }
26609
26818
  function encodeGroupCursor(group) {
26610
26819
  const payload = {
26611
26820
  sev: group.severity,
@@ -26681,7 +26890,7 @@ var SqliteFindingsRepository = class {
26681
26890
  this.db.prepare(
26682
26891
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26683
26892
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26684
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26893
+ e.source_tool AS source_tool,
26685
26894
  e.event_type AS kind
26686
26895
  FROM audit_events e
26687
26896
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26789,56 +26998,11 @@ var SqliteFindingsRepository = class {
26789
26998
  predicate,
26790
26999
  params: sessionParams
26791
27000
  });
26792
- const rows = allRows(
26793
- this.db.prepare(
26794
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26795
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26796
- kind, finding_key, latest_status
26797
- FROM (
26798
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26799
- d.severity AS severity, f.masked_match AS masked_match,
26800
- f.action_taken AS action_taken, f.confidence AS confidence,
26801
- e.started_at AS occurred_at,
26802
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26803
- json_extract(e.attributes, '$.repo') AS repo,
26804
- json_extract(e.attributes, '$.file_path') AS file,
26805
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26806
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26807
- e.event_type AS kind, f.finding_key AS finding_key,
26808
- latest.status AS latest_status,
26809
- ROW_NUMBER() OVER (
26810
- PARTITION BY d.rule_id
26811
- ORDER BY e.started_at DESC, f.id DESC
26812
- ) AS rn
26813
- FROM inspection_findings f
26814
- JOIN audit_events e ON e.id = f.audit_event_id
26815
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26816
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26817
- ON latest.finding_key = f.finding_key
26818
- ${predicate}
26819
- )
26820
- WHERE rn <= :cap
26821
- ORDER BY occurred_at DESC, id DESC`
26822
- ),
26823
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26824
- );
26825
- const groupable = rows.map((r) => ({
26826
- id: r.id,
26827
- ruleId: r.rule_id,
26828
- category: r.category,
26829
- severity: r.severity,
26830
- maskedMatch: r.masked_match,
26831
- actionTaken: r.action_taken,
26832
- confidence: r.confidence,
26833
- occurredAt: epochMillisToIso(r.occurred_at),
26834
- sourceTool: r.source_tool,
26835
- repo: r.repo ?? "",
26836
- file: r.file ?? "",
26837
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26838
- eventId: r.event_id,
26839
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26840
- status: deriveInstanceStatus(r)
26841
- }));
27001
+ const rows = this.previewRows(aggregates, {
27002
+ sessionId: query.sessionId,
27003
+ from: query.from
27004
+ });
27005
+ const groupable = rows.map(toFlatFindingRow);
26842
27006
  const allGroups = buildFindingGroups(groupable, { aggregates });
26843
27007
  const filterOpts = {
26844
27008
  severity: query.severity,
@@ -26924,8 +27088,10 @@ var SqliteFindingsRepository = class {
26924
27088
  *
26925
27089
  * The scan runs from the top of the scope on every request, not from the
26926
27090
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26927
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26928
- * while the counting runs, and only the page itself is retained.
27091
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27092
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27093
+ * counting runs — a generator streaming the index order, not a sequence of
27094
+ * fetched batches; only the page itself is retained.
26929
27095
  */
26930
27096
  listFindingInstances(query) {
26931
27097
  const opts = {
@@ -26941,6 +27107,10 @@ var SqliteFindingsRepository = class {
26941
27107
  };
26942
27108
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
26943
27109
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27110
+ const isPastCursor = cursor === null ? () => true : (row) => {
27111
+ const rowMs = isoToEpochMillis(row.occurredAt);
27112
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27113
+ };
26944
27114
  const accumulator = createInstanceFacetAccumulator(opts);
26945
27115
  const items = [];
26946
27116
  let total = 0;
@@ -26953,6 +27123,7 @@ var SqliteFindingsRepository = class {
26953
27123
  accumulator.add(row);
26954
27124
  if (!matchesInstanceFilters(row, opts)) continue;
26955
27125
  total += 1;
27126
+ if (!isPastCursor(row)) continue;
26956
27127
  if (items.length < limit) {
26957
27128
  items.push(toInstanceDetail(row));
26958
27129
  last = row;
@@ -26961,15 +27132,6 @@ var SqliteFindingsRepository = class {
26961
27132
  }
26962
27133
  }
26963
27134
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
26964
- if (cursor !== null) {
26965
- const resumed = this.pageAfter(cursor, opts, limit, query);
26966
- return Promise.resolve({
26967
- totals: { findings: total },
26968
- facets: accumulator.facets(),
26969
- items: resumed.items,
26970
- nextCursor: resumed.nextCursor
26971
- });
26972
- }
26973
27135
  return Promise.resolve({
26974
27136
  totals: { findings: total },
26975
27137
  facets: accumulator.facets(),
@@ -26977,35 +27139,6 @@ var SqliteFindingsRepository = class {
26977
27139
  nextCursor
26978
27140
  });
26979
27141
  }
26980
- /**
26981
- * The page of matching rows strictly after `cursor`. Separate from the
26982
- * counting pass because that one starts at the top of the scope by design;
26983
- * this one narrows the scan with the same keyset predicate the activity list
26984
- * uses, so a later page costs less than the first rather than more.
26985
- */
26986
- pageAfter(cursor, opts, limit, query) {
26987
- const items = [];
26988
- let last;
26989
- let hasMore = false;
26990
- for (const row of this.scanFindingRows({
26991
- sessionId: query.sessionId,
26992
- from: query.from,
26993
- after: cursor
26994
- })) {
26995
- if (!matchesInstanceFilters(row, opts)) continue;
26996
- if (items.length < limit) {
26997
- items.push(toInstanceDetail(row));
26998
- last = row;
26999
- } else {
27000
- hasMore = true;
27001
- break;
27002
- }
27003
- }
27004
- return {
27005
- items,
27006
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27007
- };
27008
- }
27009
27142
  /**
27010
27143
  * The same findings folded by location: repository, then file within it.
27011
27144
  *
@@ -27088,25 +27221,111 @@ var SqliteFindingsRepository = class {
27088
27221
  });
27089
27222
  }
27090
27223
  /**
27091
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27224
+ * Each group's newest instances, for the table's expanded rows.
27225
+ *
27226
+ * ONE index-ordered scan with early termination, and the shape is the point.
27227
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27228
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27229
+ * through a temp B-tree to keep a bounded preview of each group, and then
27230
+ * sorts the survivors again for the page order. Both sorts grow with the
27231
+ * store while the answer does not.
27232
+ *
27233
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27234
+ * (or the session or window index the scope names — see `findingScanSql`),
27235
+ * which is already the order the page wants, and keeps rows per rule until
27236
+ * each rule has as many as it can show. The aggregate the caller already holds
27237
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27238
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27239
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27240
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27241
+ * store with many firing rules widens it. The bound that DOES hold
27242
+ * unconditionally is the sorted form's floor: this scan visits at most as
27243
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27244
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27245
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27246
+ * wanted instances sitting at the tail of the scope — is one pass over
27247
+ * everything in scope with a block sort of the id tie-break only, never a
27248
+ * sort of the scope, which is still that floor.
27249
+ *
27250
+ * A row whose rule the aggregate did not see is skipped: the two statements
27251
+ * run without a shared snapshot, so a capture landing between them can add a
27252
+ * rule here that has no counts there, and the counts are what the group is
27253
+ * built from.
27254
+ */
27255
+ previewRows(aggregates, scope) {
27256
+ const wanted = /* @__PURE__ */ new Map();
27257
+ let remaining = 0;
27258
+ for (const [ruleId, agg] of aggregates) {
27259
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27260
+ wanted.set(ruleId, n);
27261
+ remaining += n;
27262
+ }
27263
+ const rows = [];
27264
+ if (remaining === 0) return rows;
27265
+ const { sql, params } = this.findingScanSql(scope);
27266
+ const taken = /* @__PURE__ */ new Map();
27267
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27268
+ const want = wanted.get(r.rule_id);
27269
+ if (want === void 0) continue;
27270
+ const have = taken.get(r.rule_id) ?? 0;
27271
+ if (have >= want) continue;
27272
+ taken.set(r.rule_id, have + 1);
27273
+ rows.push(r);
27274
+ remaining -= 1;
27275
+ if (remaining === 0) break;
27276
+ }
27277
+ return rows;
27278
+ }
27279
+ /**
27280
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27092
27281
  *
27093
27282
  * A generator so a caller streams the scope without it ever being an array:
27094
27283
  * the flat list counts and facets the whole filtered scope, which on a large
27095
- * store is far more rows than any page. Each batch advances the same keyset
27096
- * predicate the page read uses, so the scan is a sequence of bounded reads
27097
- * rather than one unbounded result set.
27098
- *
27099
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27100
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27101
- * makes it a point lookup per row, and the derived table would re-materialize
27102
- * a window over the whole resolution table once per batch.
27284
+ * store is far more rows than any page. The rows come off ONE statement,
27285
+ * iterated rather than materialized, in the index order `findingScanSql`
27286
+ * arranges so the scan is a single pass with a block sort of the id
27287
+ * tie-break only, never a sort of the scope, where a sequence of
27288
+ * keyset-bounded batches re-sorted everything below the cursor on every
27289
+ * batch and cost the square of the scope.
27103
27290
  *
27104
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27105
- * would be missing from its own facet, which is computed by excluding that
27106
- * dimension see listFindingInstances.
27291
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27292
+ * dimension narrowed here would be missing from its own facet, which is
27293
+ * computed by excluding that dimension (see listFindingInstances). There is
27294
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27295
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27296
+ * narrower statement, since the counting pass already visits every row a
27297
+ * page-2+ request would otherwise re-seek for.
27107
27298
  */
27108
27299
  *scanFindingRows(scope) {
27109
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27300
+ const { sql, params } = this.findingScanSql(scope);
27301
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27302
+ yield toFlatFindingRow(r);
27303
+ }
27304
+ }
27305
+ /**
27306
+ * The one statement both instance-level scans run: every finding in scope,
27307
+ * joined to its event and definition, newest first.
27308
+ *
27309
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27310
+ * the same two `recentFindings` documents at length, for the same reason:
27311
+ *
27312
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27313
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27314
+ * yields `started_at` order per event type, not across the four, so
27315
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27316
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27317
+ * `idx_audit_session` for a session scope, which is also `started_at`
27318
+ * ordered within the session — and the order falls out of the index.
27319
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27320
+ * JOINs the planner drives from the findings and sorts everything.
27321
+ *
27322
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27323
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27324
+ * index probe per keyed row, and a derived table over the whole resolution
27325
+ * table would be materialized before the first row streamed.
27326
+ */
27327
+ findingScanSql(scope) {
27328
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27110
27329
  const params = [];
27111
27330
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27112
27331
  conditions.push("e.root_session_id = ?");
@@ -27120,58 +27339,24 @@ var SqliteFindingsRepository = class {
27120
27339
  d.severity AS severity, f.masked_match AS masked_match,
27121
27340
  f.action_taken AS action_taken, f.confidence AS confidence,
27122
27341
  e.started_at AS occurred_at,
27123
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27124
- json_extract(e.attributes, '$.repo') AS repo,
27125
- json_extract(e.attributes, '$.file_path') AS file,
27126
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27342
+ e.source_tool AS source_tool,
27343
+ e.repo AS repo,
27344
+ e.file_path AS file,
27345
+ e.tool_name AS tool_name,
27127
27346
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27128
27347
  e.event_type AS kind, f.finding_key AS finding_key,
27129
27348
  ${latestResolutionStatusSql("f")} AS latest_status
27130
- FROM inspection_findings f
27131
- JOIN audit_events e ON e.id = f.audit_event_id
27132
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27133
- WHERE ${conditions.join(" AND ")}
27134
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27135
- ORDER BY e.started_at DESC, f.id DESC
27136
- LIMIT ?`;
27137
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27138
- for (; ; ) {
27139
- const rows = allRows(this.db.prepare(sql), [
27140
- ...params,
27141
- after.startedAtMs,
27142
- after.startedAtMs,
27143
- after.id,
27144
- SCAN_BATCH_ROWS
27145
- ]);
27146
- for (const r of rows) {
27147
- yield {
27148
- id: r.id,
27149
- ruleId: r.rule_id,
27150
- category: r.category,
27151
- severity: r.severity,
27152
- maskedMatch: r.masked_match,
27153
- actionTaken: r.action_taken,
27154
- confidence: r.confidence,
27155
- occurredAt: epochMillisToIso(r.occurred_at),
27156
- sourceTool: r.source_tool,
27157
- repo: r.repo ?? "",
27158
- file: r.file ?? "",
27159
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27160
- eventId: r.event_id,
27161
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27162
- status: deriveInstanceStatus(r)
27163
- };
27164
- }
27165
- if (rows.length < SCAN_BATCH_ROWS) return;
27166
- const lastRow = rows[rows.length - 1];
27167
- if (lastRow === void 0) return;
27168
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27169
- }
27349
+ FROM audit_events e
27350
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27351
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27352
+ WHERE ${conditions.join(" AND ")}
27353
+ ORDER BY e.started_at DESC, f.id DESC`;
27354
+ return { sql, params };
27170
27355
  }
27171
27356
  groupAggregates(withSearchText, scope) {
27172
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27173
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27174
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27357
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27358
+ group_concat(DISTINCT e.file_path) AS files,
27359
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27175
27360
  const rows = this.db.prepare(
27176
27361
  `SELECT rule_id,
27177
27362
  sum(tuple_count) AS instance_count,
@@ -27189,7 +27374,7 @@ var SqliteFindingsRepository = class {
27189
27374
  coalesce(latest.status, '') AS status_tuple,
27190
27375
  count(*) AS tuple_count,
27191
27376
  max(e.started_at) AS latest_at,
27192
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27377
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27193
27378
  group_concat(DISTINCT f.action_taken) AS actions_taken
27194
27379
  ${innerSearchColumns}
27195
27380
  FROM inspection_findings f
@@ -27320,6 +27505,8 @@ function isoDay(ms) {
27320
27505
  // ../../packages/persistence/src/repositories/history-sync.ts
27321
27506
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27322
27507
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27508
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27509
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27323
27510
  var SKIPPED = -1;
27324
27511
  var ROW_COLUMNS = `id,
27325
27512
  parent_id AS parentId,
@@ -27359,6 +27546,20 @@ var SqliteHistorySyncRepository = class {
27359
27546
  ORDER BY (event_type = 'session') DESC, started_at
27360
27547
  LIMIT :limit`
27361
27548
  );
27549
+ this.captureRowsStmt = db.prepare(
27550
+ `SELECT ${ROW_COLUMNS}
27551
+ FROM audit_events
27552
+ WHERE synced_at IS NULL
27553
+ AND sync_claimed_at IS NULL
27554
+ AND outbox_owed = 1
27555
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27556
+ AND started_at < :before
27557
+ ORDER BY started_at
27558
+ LIMIT :limit`
27559
+ );
27560
+ this.markOwedStmt = db.prepare(
27561
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27562
+ );
27362
27563
  this.stampStmt = db.prepare(
27363
27564
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27364
27565
  );
@@ -27390,6 +27591,12 @@ var SqliteHistorySyncRepository = class {
27390
27591
  FROM audit_events
27391
27592
  WHERE event_type IN (${TYPE_LIST})`
27392
27593
  );
27594
+ this.captureSkipCountStmt = db.prepare(
27595
+ `SELECT COUNT(*) AS skipped
27596
+ FROM audit_events
27597
+ WHERE synced_at = ${String(SKIPPED)}
27598
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27599
+ );
27393
27600
  this.fingerprintStmt = db.prepare(
27394
27601
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27395
27602
  FROM history_sync WHERE id = 1`
@@ -27399,6 +27606,10 @@ var SqliteHistorySyncRepository = class {
27399
27606
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27400
27607
  WHERE id = 1`
27401
27608
  );
27609
+ this.disownCapturesStmt = db.prepare(
27610
+ `UPDATE audit_events SET outbox_owed = NULL
27611
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27612
+ );
27402
27613
  this.rearmStmt = db.prepare(
27403
27614
  `UPDATE audit_events SET synced_at = NULL
27404
27615
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27471,6 +27682,10 @@ var SqliteHistorySyncRepository = class {
27471
27682
  closeWindowStmt;
27472
27683
  releaseBoundaryStmt;
27473
27684
  freezeBoundaryStmt;
27685
+ captureRowsStmt;
27686
+ markOwedStmt;
27687
+ captureSkipCountStmt;
27688
+ disownCapturesStmt;
27474
27689
  partitionStmt;
27475
27690
  claimRowStmt;
27476
27691
  releaseRowStmt;
@@ -27504,6 +27719,34 @@ var SqliteHistorySyncRepository = class {
27504
27719
  pendingRows(sessionId, limit, before) {
27505
27720
  return allRows(this.rowsStmt, { sessionId, limit, before });
27506
27721
  }
27722
+ /**
27723
+ * Captures this machine still owes the deployment, oldest first.
27724
+ *
27725
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27726
+ * by a time window — see captureRowsStmt for why a window could not express
27727
+ * this. `before` is the grace window that leaves a just-recorded capture to
27728
+ * the live path.
27729
+ */
27730
+ pendingCaptureRows(limit, before) {
27731
+ return allRows(this.captureRowsStmt, { limit, before });
27732
+ }
27733
+ /**
27734
+ * Record that a capture is OWED to the deployment.
27735
+ *
27736
+ * Written by the attached forward path when a live send did not confirm
27737
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27738
+ * a fact rather than an inference: the machine was attached, the send did not
27739
+ * land, so the row is owed — which no time window can state, because the same
27740
+ * window that holds the rows a past attachment left owed also holds every
27741
+ * capture recorded while the machine was DETACHED, and those were never
27742
+ * offered to anyone.
27743
+ *
27744
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27745
+ * out of the drain's read.
27746
+ */
27747
+ markCaptureOwed(id) {
27748
+ this.markOwedStmt.run({ id });
27749
+ }
27507
27750
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27508
27751
  markSynced(ids, atMs) {
27509
27752
  this.stampAll(ids, atMs);
@@ -27587,10 +27830,12 @@ var SqliteHistorySyncRepository = class {
27587
27830
  this.countsStmt,
27588
27831
  { before }
27589
27832
  );
27833
+ const captures = getRow(this.captureSkipCountStmt);
27590
27834
  return {
27591
27835
  pending: row?.pending ?? 0,
27592
27836
  sent: row?.sent ?? 0,
27593
- skipped: row?.skipped ?? 0
27837
+ skipped: row?.skipped ?? 0,
27838
+ capturesSkipped: captures?.skipped ?? 0
27594
27839
  };
27595
27840
  }
27596
27841
  /**
@@ -27631,7 +27876,11 @@ var SqliteHistorySyncRepository = class {
27631
27876
  withTransaction(
27632
27877
  this.db,
27633
27878
  () => {
27879
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27634
27880
  this.rearmStmt.run();
27881
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27882
+ this.disownCapturesStmt.run();
27883
+ }
27635
27884
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27636
27885
  },
27637
27886
  "IMMEDIATE"
@@ -27727,108 +27976,651 @@ var SqliteInspectionDefinitionsRepository = class {
27727
27976
  (:id, :ruleId, :name, :category, :severity, :definition, :version)`
27728
27977
  );
27729
27978
  }
27730
- db;
27731
- insertStmt;
27732
- // Insert-if-absent; returns the content-addressed definition id. An id already
27733
- // present keeps the stored row untouched — see the class doc.
27734
- upsert(input2) {
27735
- const id = inspectionDefinitionId(input2.ruleId, input2.version);
27736
- const row = toInspectionDefinitionRow(input2, id);
27737
- this.insertStmt.run({
27738
- id: row.id,
27739
- ruleId: row.ruleId,
27740
- name: row.name,
27741
- category: row.category,
27742
- severity: row.severity,
27743
- definition: row.definition,
27744
- version: row.version
27745
- });
27746
- return id;
27979
+ db;
27980
+ insertStmt;
27981
+ // Insert-if-absent; returns the content-addressed definition id. An id already
27982
+ // present keeps the stored row untouched — see the class doc.
27983
+ upsert(input2) {
27984
+ const id = inspectionDefinitionId(input2.ruleId, input2.version);
27985
+ const row = toInspectionDefinitionRow(input2, id);
27986
+ this.insertStmt.run({
27987
+ id: row.id,
27988
+ ruleId: row.ruleId,
27989
+ name: row.name,
27990
+ category: row.category,
27991
+ severity: row.severity,
27992
+ definition: row.definition,
27993
+ version: row.version
27994
+ });
27995
+ return id;
27996
+ }
27997
+ };
27998
+
27999
+ // ../../packages/persistence/src/repositories/inspection-findings.ts
28000
+ var SqliteInspectionFindingsRepository = class {
28001
+ constructor(db) {
28002
+ this.db = db;
28003
+ this.insertStmt = db.prepare(
28004
+ `INSERT INTO inspection_findings
28005
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
28006
+ span_start, span_end, masked_match, action_taken, confidence,
28007
+ finding_key, first_detected_at)
28008
+ VALUES
28009
+ (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
28010
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
28011
+ :findingKey,
28012
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
28013
+ ON CONFLICT(id) DO UPDATE SET
28014
+ inspection_definition_id = excluded.inspection_definition_id
28015
+ ON CONFLICT (finding_key) DO UPDATE SET
28016
+ audit_event_id = excluded.audit_event_id,
28017
+ inspection_definition_id = excluded.inspection_definition_id,
28018
+ classified_data_id = excluded.classified_data_id,
28019
+ span_start = excluded.span_start,
28020
+ span_end = excluded.span_end,
28021
+ masked_match = excluded.masked_match,
28022
+ action_taken = excluded.action_taken,
28023
+ confidence = excluded.confidence`
28024
+ );
28025
+ this.sessionDupStmt = db.prepare(
28026
+ `SELECT 1 FROM inspection_findings f
28027
+ JOIN audit_events e ON e.id = f.audit_event_id
28028
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
28029
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
28030
+ AND e.root_session_id = :sessionId
28031
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
28032
+ LIMIT 1`
28033
+ );
28034
+ this.eventDupStmt = db.prepare(
28035
+ `SELECT 1 FROM inspection_findings f
28036
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
28037
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
28038
+ AND f.masked_match = :maskedMatch
28039
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
28040
+ LIMIT 1`
28041
+ );
28042
+ }
28043
+ db;
28044
+ insertStmt;
28045
+ sessionDupStmt;
28046
+ eventDupStmt;
28047
+ // True when an earlier event in the same session already recorded a finding
28048
+ // with the same rule and masked value. The current event's own findings are
28049
+ // inserted one at a time in caller order, so an earlier finding in the SAME
28050
+ // recordCapture call is visible to a later duplicate check within it too.
28051
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
28052
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
28053
+ }
28054
+ // True when this exact detection (rule + masked value + span) is already
28055
+ // recorded against the given audit event.
28056
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
28057
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
28058
+ }
28059
+ insertFinding(input2) {
28060
+ const row = toInspectionFindingRow(input2);
28061
+ this.insertStmt.run(
28062
+ bindParams({
28063
+ id: row.id,
28064
+ auditEventId: row.auditEventId,
28065
+ inspectionDefinitionId: row.inspectionDefinitionId,
28066
+ classifiedDataId: row.classifiedDataId,
28067
+ spanStart: row.spanStart,
28068
+ spanEnd: row.spanEnd,
28069
+ maskedMatch: row.maskedMatch,
28070
+ actionTaken: row.actionTaken,
28071
+ confidence: row.confidence,
28072
+ findingKey: row.findingKey,
28073
+ firstDetectedAt: row.firstDetectedAt
28074
+ })
28075
+ );
28076
+ }
28077
+ };
28078
+
28079
+ // ../../packages/persistence/src/repositories/installed-packs.ts
28080
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28081
+
28082
+ // ../../packages/persistence/src/policy-floor.ts
28083
+ import { readFileSync as readFileSync5 } from "fs";
28084
+ import { join as join6 } from "path";
28085
+
28086
+ // ../../packages/persistence/src/local-layout.ts
28087
+ import { renameSync as renameSync3 } from "fs";
28088
+ import { mkdir } from "fs/promises";
28089
+ import { homedir } from "os";
28090
+ import { join as join4 } from "path";
28091
+ function defaultDataDir() {
28092
+ return join4(homedir(), ".aka");
28093
+ }
28094
+ function settingsDir(base = defaultDataDir()) {
28095
+ return join4(base, "settings");
28096
+ }
28097
+ function dataDir(base = defaultDataDir()) {
28098
+ return join4(base, "data");
28099
+ }
28100
+ function dbPath(base = defaultDataDir()) {
28101
+ return join4(dataDir(base), "aka.db");
28102
+ }
28103
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28104
+ ensureDataDirSync(dir);
28105
+ }
28106
+ function migrateLegacyLayout(base = defaultDataDir()) {
28107
+ const moves = [
28108
+ { name: "config.json", dest: settingsDir(base) },
28109
+ { name: "policy-cache.json", dest: dataDir(base) }
28110
+ ];
28111
+ for (const { name, dest } of moves) {
28112
+ try {
28113
+ ensureDataDirSync(dest);
28114
+ const moved = join4(dest, name);
28115
+ renameSync3(join4(base, name), moved);
28116
+ tightenFile(moved);
28117
+ } catch {
28118
+ }
28119
+ }
28120
+ }
28121
+
28122
+ // ../../packages/persistence/src/settings.ts
28123
+ import { readFileSync as readFileSync4 } from "fs";
28124
+ import { join as join5 } from "path";
28125
+
28126
+ // ../../packages/persistence/src/file-lock.ts
28127
+ import { randomUUID as randomUUID3 } from "crypto";
28128
+ import {
28129
+ closeSync,
28130
+ existsSync as existsSync2,
28131
+ openSync,
28132
+ readFileSync as readFileSync2,
28133
+ rmSync as rmSync5,
28134
+ statSync as statSync3,
28135
+ writeFileSync as writeFileSync2
28136
+ } from "fs";
28137
+ import { hostname as hostname3 } from "os";
28138
+ var LOCK_SUFFIX = ".lock";
28139
+ var DEFAULT_TIMEOUT_MS = 5e3;
28140
+ var DEFAULT_STALE_MS = 2e3;
28141
+ var RETRY_INTERVAL_MS = 5;
28142
+ var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
28143
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28144
+ function sleepSync(ms) {
28145
+ Atomics.wait(PARK, 0, 0, ms);
28146
+ }
28147
+ var FileLockError = class extends Error {
28148
+ reason;
28149
+ file;
28150
+ holderPid;
28151
+ constructor(reason, file2, detail, holderPid, options) {
28152
+ super(
28153
+ `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
28154
+ options
28155
+ );
28156
+ this.name = "FileLockError";
28157
+ this.reason = reason;
28158
+ this.file = file2;
28159
+ this.holderPid = holderPid;
28160
+ }
28161
+ };
28162
+ function lockPathFor(file2) {
28163
+ return `${file2}${LOCK_SUFFIX}`;
28164
+ }
28165
+ function readLockBody(lock) {
28166
+ let raw;
28167
+ try {
28168
+ raw = readFileSync2(lock, "utf8");
28169
+ } catch {
28170
+ return null;
28171
+ }
28172
+ try {
28173
+ const parsed = JSON.parse(raw);
28174
+ const { pid, token, at, host } = parsed;
28175
+ if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
28176
+ return { pid, token, at, host: typeof host === "string" ? host : "" };
28177
+ } catch {
28178
+ return null;
28179
+ }
28180
+ }
28181
+ function holderIsAlive(pid) {
28182
+ if (!Number.isInteger(pid) || pid <= 0) return false;
28183
+ try {
28184
+ process.kill(pid, 0);
28185
+ return true;
28186
+ } catch (err) {
28187
+ return err.code !== "ESRCH";
28188
+ }
28189
+ }
28190
+ function directoryAcceptsCreates(lock) {
28191
+ const probe = `${lock}.probe-${randomUUID3()}`;
28192
+ try {
28193
+ closeSync(openSync(probe, "wx", DATA_FILE_MODE));
28194
+ return true;
28195
+ } catch {
28196
+ return false;
28197
+ } finally {
28198
+ try {
28199
+ rmSync5(probe, { force: true });
28200
+ } catch {
28201
+ }
28202
+ }
28203
+ }
28204
+ function tryAcquire(lock, file2) {
28205
+ const token = randomUUID3();
28206
+ let fd;
28207
+ try {
28208
+ fd = openSync(lock, "wx", DATA_FILE_MODE);
28209
+ } catch (err) {
28210
+ const code = err.code ?? "";
28211
+ if (code === "EEXIST") return null;
28212
+ if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync2(lock) || directoryAcceptsCreates(lock))) {
28213
+ return null;
28214
+ }
28215
+ throw new FileLockError(
28216
+ "unavailable",
28217
+ file2,
28218
+ err instanceof Error ? err.message : String(err),
28219
+ void 0,
28220
+ { cause: err }
28221
+ );
28222
+ }
28223
+ const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
28224
+ try {
28225
+ writeFileSync2(fd, `${JSON.stringify(body)}
28226
+ `);
28227
+ } catch {
28228
+ try {
28229
+ closeSync(fd);
28230
+ } catch {
28231
+ }
28232
+ rmSync5(lock, { force: true });
28233
+ return null;
28234
+ }
28235
+ try {
28236
+ closeSync(fd);
28237
+ } catch {
28238
+ }
28239
+ return token;
28240
+ }
28241
+ function isAbandoned(body, lock, staleMs) {
28242
+ if (!body) {
28243
+ try {
28244
+ return Date.now() - statSync3(lock).mtimeMs >= staleMs;
28245
+ } catch {
28246
+ return false;
28247
+ }
28248
+ }
28249
+ if (Date.now() - body.at < staleMs) return false;
28250
+ if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
28251
+ return Date.now() - body.at >= abandonWindow(staleMs);
28252
+ }
28253
+ function breakIfStale(lock, staleMs) {
28254
+ const breaker2 = `${lock}.break`;
28255
+ let fd;
28256
+ try {
28257
+ fd = openSync(breaker2, "wx", DATA_FILE_MODE);
28258
+ } catch {
28259
+ reapAbandonedBreaker(breaker2);
28260
+ return false;
28261
+ }
28262
+ try {
28263
+ closeSync(fd);
28264
+ } catch {
28265
+ }
28266
+ try {
28267
+ if (!existsSync2(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
28268
+ rmSync5(lock, { force: true });
28269
+ return true;
28270
+ } catch {
28271
+ return false;
28272
+ } finally {
28273
+ try {
28274
+ rmSync5(breaker2, { force: true });
28275
+ } catch {
28276
+ }
28277
+ }
28278
+ }
28279
+ var BREAKER_ABANDONED_MS = 1e4;
28280
+ function reapAbandonedBreaker(breaker2) {
28281
+ try {
28282
+ if (Date.now() - statSync3(breaker2).mtimeMs >= BREAKER_ABANDONED_MS) {
28283
+ rmSync5(breaker2, { force: true });
28284
+ }
28285
+ } catch {
28286
+ }
28287
+ }
28288
+ function abandonWindow(staleMs) {
28289
+ return Math.max(staleMs * 30, 6e4);
28290
+ }
28291
+ function release(lock, token) {
28292
+ try {
28293
+ if (readLockBody(lock)?.token !== token) return;
28294
+ rmSync5(lock, { force: true });
28295
+ } catch {
28296
+ }
28297
+ }
28298
+ function withFileLock(file2, fn, options = {}) {
28299
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28300
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
28301
+ const lock = lockPathFor(file2);
28302
+ const deadline = Date.now() + timeoutMs;
28303
+ let token = tryAcquire(lock, file2);
28304
+ while (token === null) {
28305
+ if (breakIfStale(lock, staleMs)) {
28306
+ token = tryAcquire(lock, file2);
28307
+ continue;
28308
+ }
28309
+ if (Date.now() >= deadline) {
28310
+ throw new FileLockError(
28311
+ "timeout",
28312
+ file2,
28313
+ `still held after ${String(timeoutMs)}ms`,
28314
+ readLockBody(lock)?.pid
28315
+ );
28316
+ }
28317
+ sleepSync(RETRY_INTERVAL_MS);
28318
+ token = tryAcquire(lock, file2);
28319
+ }
28320
+ try {
28321
+ const result = fn();
28322
+ if (isThenable(result)) {
28323
+ void result.then(
28324
+ () => void 0,
28325
+ () => void 0
28326
+ );
28327
+ throw new TypeError(
28328
+ `withFileLock(${file2}) was given an async body; the lock is released as soon as it returns, so the awaited work would run unguarded. Pass a synchronous function.`
28329
+ );
28330
+ }
28331
+ return result;
28332
+ } finally {
28333
+ release(lock, token);
28334
+ }
28335
+ }
28336
+ function isThenable(value) {
28337
+ return typeof value === "object" && value !== null && typeof value.then === "function";
28338
+ }
28339
+
28340
+ // ../../packages/persistence/src/managed-settings.ts
28341
+ import { readFileSync as readFileSync3 } from "fs";
28342
+ import { posix, win32 } from "path";
28343
+ function managedSettingsPaths(platform2 = process.platform) {
28344
+ if (platform2 === "darwin") {
28345
+ return [
28346
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28347
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28348
+ ];
28349
+ }
28350
+ if (platform2 === "win32") {
28351
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28352
+ }
28353
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28354
+ }
28355
+ function readManagedSettings(paths = managedSettingsPaths()) {
28356
+ for (const path of paths) {
28357
+ let text;
28358
+ try {
28359
+ text = readFileSync3(path, "utf8");
28360
+ } catch {
28361
+ continue;
28362
+ }
28363
+ const record2 = parseJsonObject(text);
28364
+ if (!record2) continue;
28365
+ const parsed = ManagedSettings.safeParse(record2);
28366
+ if (parsed.success) return parsed.data;
28367
+ }
28368
+ return null;
28369
+ }
28370
+ function managedContextOf(managed) {
28371
+ if (!managed) return NO_MANAGED_CONTEXT;
28372
+ return {
28373
+ present: true,
28374
+ ...managed.organization === void 0 ? {} : { organization: managed.organization },
28375
+ lockedFields: managed.lockedFields
28376
+ };
28377
+ }
28378
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28379
+ if (!managed) return settings;
28380
+ const { values } = managed;
28381
+ const merged = { ...settings };
28382
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28383
+ if (values.controlPlane !== void 0) {
28384
+ merged.controlPlane = {
28385
+ ...values.controlPlane,
28386
+ // The administrator pinned WHICH deployment, not WHEN this machine
28387
+ // joined it. Keep the user's own attach time when the endpoint is
28388
+ // unchanged, so a managed machine does not appear to re-attach on every
28389
+ // read; stamp a fresh one when the administrator moved it.
28390
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28391
+ };
28392
+ }
28393
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28394
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28395
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28396
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28397
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28398
+ if (values.vaultConsent !== void 0) {
28399
+ merged.vaultConsent = values.vaultConsent ? (
28400
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28401
+ // at the current version otherwise.
28402
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28403
+ ) : void 0;
28404
+ }
28405
+ if (values.modelJudgeConsent !== void 0) {
28406
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28407
+ acknowledgedAt: now().toISOString(),
28408
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28409
+ } : void 0;
28410
+ }
28411
+ return merged;
28412
+ }
28413
+ function lockedAmong(context, requested) {
28414
+ if (!context.present) return [];
28415
+ return requested.filter((key) => context.lockedFields.includes(key));
28416
+ }
28417
+
28418
+ // ../../packages/persistence/src/settings.ts
28419
+ var SETTINGS_FILENAME = "settings.json";
28420
+ function readWorkspaceSettings(base = defaultDataDir()) {
28421
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28422
+ }
28423
+ function readUserSettings(base) {
28424
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28425
+ if (!record2) return defaultWorkspaceSettings();
28426
+ try {
28427
+ return WorkspaceSettings.parse(record2);
28428
+ } catch {
28429
+ return defaultWorkspaceSettings();
28430
+ }
28431
+ }
28432
+ var ManagedFieldError = class extends Error {
28433
+ fields;
28434
+ constructor(fields) {
28435
+ super(`refusing to write administratively locked settings: ${fields.join(", ")}`);
28436
+ this.name = "ManagedFieldError";
28437
+ this.fields = fields;
27747
28438
  }
27748
28439
  };
27749
-
27750
- // ../../packages/persistence/src/repositories/inspection-findings.ts
27751
- var SqliteInspectionFindingsRepository = class {
27752
- constructor(db) {
27753
- this.db = db;
27754
- this.insertStmt = db.prepare(
27755
- `INSERT INTO inspection_findings
27756
- (id, audit_event_id, inspection_definition_id, classified_data_id,
27757
- span_start, span_end, masked_match, action_taken, confidence,
27758
- finding_key, first_detected_at)
27759
- VALUES
27760
- (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
27761
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
27762
- :findingKey,
27763
- COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
27764
- ON CONFLICT(id) DO UPDATE SET
27765
- inspection_definition_id = excluded.inspection_definition_id
27766
- ON CONFLICT (finding_key) DO UPDATE SET
27767
- audit_event_id = excluded.audit_event_id,
27768
- inspection_definition_id = excluded.inspection_definition_id,
27769
- classified_data_id = excluded.classified_data_id,
27770
- span_start = excluded.span_start,
27771
- span_end = excluded.span_end,
27772
- masked_match = excluded.masked_match,
27773
- action_taken = excluded.action_taken,
27774
- confidence = excluded.confidence`
27775
- );
27776
- this.sessionDupStmt = db.prepare(
27777
- `SELECT 1 FROM inspection_findings f
27778
- JOIN audit_events e ON e.id = f.audit_event_id
27779
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27780
- WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
27781
- AND e.root_session_id = :sessionId
27782
- AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27783
- LIMIT 1`
27784
- );
27785
- this.eventDupStmt = db.prepare(
27786
- `SELECT 1 FROM inspection_findings f
27787
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27788
- WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
27789
- AND f.masked_match = :maskedMatch
27790
- AND f.span_start = :spanStart AND f.span_end = :spanEnd
27791
- LIMIT 1`
27792
- );
28440
+ function lockableKeysTouched(current, applied) {
28441
+ const keys = [];
28442
+ const changed = (key) => key in applied && applied[key] !== current[key];
28443
+ const descriptorChanged = "controlPlane" in applied && (applied.controlPlane?.endpoint !== current.controlPlane?.endpoint || applied.controlPlane?.label !== current.controlPlane?.label);
28444
+ if (changed("runMode") || descriptorChanged) keys.push("runMode");
28445
+ if (changed("historicalAccess")) keys.push("historicalAccess");
28446
+ if (changed("vaultKeyCustody")) keys.push("vaultKeyCustody");
28447
+ if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
28448
+ if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
28449
+ if (changed("redactFallback")) keys.push("redactFallback");
28450
+ if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
28451
+ keys.push("vaultConsent");
27793
28452
  }
27794
- db;
27795
- insertStmt;
27796
- sessionDupStmt;
27797
- eventDupStmt;
27798
- // True when an earlier event in the same session already recorded a finding
27799
- // with the same rule and masked value. The current event's own findings are
27800
- // inserted one at a time in caller order, so an earlier finding in the SAME
27801
- // recordCapture call is visible to a later duplicate check within it too.
27802
- isSessionDuplicate(ruleId, maskedMatch, sessionId) {
27803
- return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
28453
+ if ("modelJudgeConsent" in applied && isModelJudgeConsentValid(applied.modelJudgeConsent) !== isModelJudgeConsentValid(current.modelJudgeConsent)) {
28454
+ keys.push("modelJudgeConsent");
27804
28455
  }
27805
- // True when this exact detection (rule + masked value + span) is already
27806
- // recorded against the given audit event.
27807
- isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
27808
- return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
28456
+ return keys;
28457
+ }
28458
+ function pinnedKeys(managed) {
28459
+ if (!managed) return [];
28460
+ const { values } = managed;
28461
+ const keys = [];
28462
+ if (values.runMode !== void 0 || values.controlPlane !== void 0) keys.push("runMode");
28463
+ if (values.historicalAccess !== void 0) keys.push("historicalAccess");
28464
+ if (values.vaultConsent !== void 0) keys.push("vaultConsent");
28465
+ if (values.vaultKeyCustody !== void 0) keys.push("vaultKeyCustody");
28466
+ if (values.vaultInlineReveal !== void 0) keys.push("vaultInlineReveal");
28467
+ if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
28468
+ if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
28469
+ if (values.redactFallback !== void 0) keys.push("redactFallback");
28470
+ return keys;
28471
+ }
28472
+ function withoutManagedKeys(applied, managed, pinned, touched) {
28473
+ if (!managed.present) return applied;
28474
+ const strip = (key) => (managed.lockedFields.includes(key) || pinned.includes(key)) && !touched.includes(key);
28475
+ const out = { ...applied };
28476
+ if (strip("runMode")) {
28477
+ delete out.runMode;
28478
+ delete out.controlPlane;
27809
28479
  }
27810
- insertFinding(input2) {
27811
- const row = toInspectionFindingRow(input2);
27812
- this.insertStmt.run(
27813
- bindParams({
27814
- id: row.id,
27815
- auditEventId: row.auditEventId,
27816
- inspectionDefinitionId: row.inspectionDefinitionId,
27817
- classifiedDataId: row.classifiedDataId,
27818
- spanStart: row.spanStart,
27819
- spanEnd: row.spanEnd,
27820
- maskedMatch: row.maskedMatch,
27821
- actionTaken: row.actionTaken,
27822
- confidence: row.confidence,
27823
- findingKey: row.findingKey,
27824
- firstDetectedAt: row.firstDetectedAt
27825
- })
27826
- );
28480
+ if (strip("historicalAccess")) delete out.historicalAccess;
28481
+ if (strip("vaultConsent")) delete out.vaultConsent;
28482
+ if (strip("vaultKeyCustody")) delete out.vaultKeyCustody;
28483
+ if (strip("vaultInlineReveal")) delete out.vaultInlineReveal;
28484
+ if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
28485
+ if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
28486
+ if (strip("redactFallback")) delete out.redactFallback;
28487
+ return out;
28488
+ }
28489
+ function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
28490
+ const dir = settingsDir(base);
28491
+ ensureDataDirSync(dir);
28492
+ const file2 = join5(dir, SETTINGS_FILENAME);
28493
+ const managedSettings = managedOverride === void 0 ? readManagedSettings() : managedOverride;
28494
+ const managed = managedContextOf(managedSettings);
28495
+ return withFileLock(file2, () => {
28496
+ const current = readUserSettings(base);
28497
+ const applied = typeof answers2 === "function" ? answers2(current) : answers2;
28498
+ const effective = overlayManagedSettings(current, managedSettings);
28499
+ const touched = lockableKeysTouched(effective, applied);
28500
+ const refused = lockedAmong(managed, touched);
28501
+ if (refused.length > 0) throw new ManagedFieldError(refused);
28502
+ const merged = WorkspaceSettings.parse({
28503
+ ...current,
28504
+ // Locked keys are stripped rather than merged. Everything still here is,
28505
+ // by the refusal above, an unchanged ECHO of the administrator's value —
28506
+ // so dropping it discards no answer of the user's, and writing it would
28507
+ // persist the pin into their file, where it would outlive the managed
28508
+ // file and read as their own choice once the lock was gone.
28509
+ ...withoutManagedKeys(applied, managed, pinnedKeys(managedSettings), touched),
28510
+ // First setup stamps the time; later edits keep the original completion mark.
28511
+ onboardedAt: applied.onboardedAt ?? current.onboardedAt ?? (/* @__PURE__ */ new Date()).toISOString()
28512
+ });
28513
+ writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
28514
+ `);
28515
+ return merged;
28516
+ });
28517
+ }
28518
+ function readJson(file2) {
28519
+ let text;
28520
+ try {
28521
+ text = readFileSync4(file2, "utf8");
28522
+ } catch {
28523
+ return null;
27827
28524
  }
27828
- };
28525
+ return parseJsonObject(text) ?? null;
28526
+ }
27829
28527
 
27830
- // ../../packages/persistence/src/repositories/installed-packs.ts
27831
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28528
+ // ../../packages/persistence/src/policy-floor.ts
28529
+ function refusalMessage(pack, attempted, floor, refusal) {
28530
+ switch (refusal) {
28531
+ case "lock":
28532
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28533
+ case "disable":
28534
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28535
+ case "floor":
28536
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28537
+ }
28538
+ }
28539
+ var PolicyFloorError = class extends Error {
28540
+ /** `namespace/packId` of the detection whose write was refused. */
28541
+ pack;
28542
+ /**
28543
+ * The archetype the caller asked for, or null when the write named none —
28544
+ * clearing the assignment, or switching the detection off.
28545
+ */
28546
+ attempted;
28547
+ /** The weakest archetype the control plane permits for this pack. */
28548
+ floor;
28549
+ refusal;
28550
+ constructor(pack, attempted, floor, refusal) {
28551
+ super(refusalMessage(pack, attempted, floor, refusal));
28552
+ this.name = "PolicyFloorError";
28553
+ this.pack = pack;
28554
+ this.attempted = attempted;
28555
+ this.floor = floor;
28556
+ this.refusal = refusal;
28557
+ }
28558
+ };
28559
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28560
+ try {
28561
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28562
+ const parsed = JSON.parse(raw);
28563
+ if (typeof parsed !== "object" || parsed === null) return null;
28564
+ return PolicyBundle.parse(parsed.bundle);
28565
+ } catch {
28566
+ return null;
28567
+ }
28568
+ }
28569
+ function indexEnabled(policies) {
28570
+ const byRuleId = /* @__PURE__ */ new Map();
28571
+ const byCategory = /* @__PURE__ */ new Map();
28572
+ for (const policy of policies) {
28573
+ if (!policy.enabled) continue;
28574
+ if ("ruleId" in policy.target) {
28575
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28576
+ } else if (!byCategory.has(policy.target.category)) {
28577
+ byCategory.set(policy.target.category, policy.action);
28578
+ }
28579
+ }
28580
+ return { byRuleId, byCategory };
28581
+ }
28582
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28583
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28584
+ const categories = new Set(rules.map((rule) => rule.category));
28585
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28586
+ return policies.some((policy) => {
28587
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28588
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28589
+ });
28590
+ }
28591
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28592
+ const floors = openControlPlaneFloors(base);
28593
+ return floors === null ? null : floors.floorFor(rules);
28594
+ }
28595
+ function openControlPlaneFloors(base = defaultDataDir()) {
28596
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28597
+ const bundle = readCachedPolicyBundle(base);
28598
+ if (bundle === null) return null;
28599
+ const indexes = indexEnabled(bundle.policies);
28600
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28601
+ }
28602
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28603
+ let action = null;
28604
+ for (const rule of rules) {
28605
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28606
+ if (resolved === void 0) continue;
28607
+ action = action === null ? resolved : strongerAction(action, resolved);
28608
+ }
28609
+ if (action === null) return null;
28610
+ return {
28611
+ floor: weakestBuiltinAtLeast(action),
28612
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28613
+ };
28614
+ }
28615
+ function policyAssignmentRefusal(policyId, floor) {
28616
+ if (floor.locked) return "lock";
28617
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28618
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28619
+ }
28620
+ function packEnablementRefusal(enabled, floor) {
28621
+ if (floor === null || enabled) return null;
28622
+ return "disable";
28623
+ }
27832
28624
 
27833
28625
  // ../../packages/persistence/src/semver.ts
27834
28626
  function parse3(version2) {
@@ -27922,8 +28714,19 @@ function ruleIdsOf(rulesJson) {
27922
28714
  return ids;
27923
28715
  }
27924
28716
  var SqliteInstalledPacksRepository = class {
27925
- constructor(db) {
28717
+ /**
28718
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28719
+ * floor needs both halves of it (settings/ says whether this machine is
28720
+ * attached, data/ holds the cached bundle). It is optional because a caller
28721
+ * holding only a DatabaseSync — every test construction site, and any embedder
28722
+ * that opens the store itself — has no layout to point at, and such a caller
28723
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28724
+ * from `openLocalDatabase`, which is the single construction site that owns a
28725
+ * real `~/.aka`.
28726
+ */
28727
+ constructor(db, baseDir) {
27926
28728
  this.db = db;
28729
+ this.baseDir = baseDir;
27927
28730
  this.insertMissingStmt = db.prepare(
27928
28731
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27929
28732
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -27945,11 +28748,17 @@ var SqliteInstalledPacksRepository = class {
27945
28748
  this.signatureStmt = db.prepare(
27946
28749
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
27947
28750
  );
28751
+ this.packRulesStmt = db.prepare(
28752
+ `SELECT rules_json AS rulesJson FROM installed_packs
28753
+ WHERE namespace = ? AND pack_id = ?`
28754
+ );
27948
28755
  }
27949
28756
  db;
28757
+ baseDir;
27950
28758
  insertMissingStmt;
27951
28759
  upsertAvailableStmt;
27952
28760
  signatureStmt;
28761
+ packRulesStmt;
27953
28762
  /**
27954
28763
  * Record the running binary's detection inventory. Refreshes the
27955
28764
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -27991,7 +28800,7 @@ var SqliteInstalledPacksRepository = class {
27991
28800
  let behind = false;
27992
28801
  for (const row of rows) {
27993
28802
  const params = {
27994
- id: randomUUID3(),
28803
+ id: randomUUID4(),
27995
28804
  namespace: row.namespace,
27996
28805
  packId: row.packId,
27997
28806
  version: row.version,
@@ -28003,7 +28812,7 @@ var SqliteInstalledPacksRepository = class {
28003
28812
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28004
28813
  this.upsertAvailableStmt.run({
28005
28814
  ...params,
28006
- id: randomUUID3(),
28815
+ id: randomUUID4(),
28007
28816
  recordedBy: meta4?.recordedBy ?? null
28008
28817
  });
28009
28818
  } else {
@@ -28249,9 +29058,65 @@ var SqliteInstalledPacksRepository = class {
28249
29058
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28250
29059
  // caller rather than swallowing them. Each returns whether a row matched, so the
28251
29060
  // caller can tell an edit from a no-such-detection.
29061
+ /**
29062
+ * The rules one installed pack owns, reduced to what a floor computation
29063
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
29064
+ * unreadable contributes no rules to a scan either, so it is not a detection
29065
+ * the control plane can be governing, and an empty list correctly imposes no
29066
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
29067
+ * the user can re-enable, and its assignment stays governed meanwhile.
29068
+ */
29069
+ packFloorRules(namespace, packId) {
29070
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
29071
+ if (!row) return [];
29072
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
29073
+ }
29074
+ /**
29075
+ * What the connected control plane imposes on one installed pack, or null on a
29076
+ * machine that is its own authority (standalone, no cached bundle, or a
29077
+ * repository constructed without a layout base).
29078
+ *
29079
+ * Exposed as a READ so a surface can render the constraint — grey out the
29080
+ * choices below the floor, mark a locked detection as locked — rather than
29081
+ * offer the user a picker whose selections it will then be told it may not
29082
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
29083
+ */
29084
+ policyFloor(namespace, packId) {
29085
+ if (this.baseDir === void 0) return null;
29086
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
29087
+ }
29088
+ /**
29089
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
29090
+ * entry only for a pack the control plane actually governs.
29091
+ *
29092
+ * A surface listing every detection asks per pack, and asking through
29093
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
29094
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
29095
+ * answer, repeated for each row, on every render. This reads all of that once.
29096
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
29097
+ * exactly as the single-pack read returns null for them.
29098
+ */
29099
+ policyFloors(packs) {
29100
+ const floors = /* @__PURE__ */ new Map();
29101
+ if (this.baseDir === void 0) return floors;
29102
+ const source = openControlPlaneFloors(this.baseDir);
29103
+ if (source === null) return floors;
29104
+ for (const pack of packs) {
29105
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
29106
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
29107
+ }
29108
+ return floors;
29109
+ }
28252
29110
  /**
28253
29111
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28254
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
29112
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
29113
+ *
29114
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
29115
+ * write below, and a detection the organization has authored a policy for is
29116
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
29117
+ * a throw rather than a silently substituted value. This is the one device-local
29118
+ * write path for the assignment, so the check belongs here rather than on any
29119
+ * surface that offers the choice.
28255
29120
  */
28256
29121
  setPolicy(namespace, packId, policyId) {
28257
29122
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28259,14 +29124,38 @@ var SqliteInstalledPacksRepository = class {
28259
29124
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28260
29125
  );
28261
29126
  }
29127
+ const requested = policyId;
29128
+ const floor = this.policyFloor(namespace, packId);
29129
+ if (floor !== null) {
29130
+ const refusal = policyAssignmentRefusal(requested, floor);
29131
+ if (refusal !== null) {
29132
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
29133
+ }
29134
+ }
28262
29135
  const res = this.db.prepare(
28263
29136
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28264
29137
  WHERE namespace = :namespace AND pack_id = :packId`
28265
29138
  ).run({ policyId, now: Date.now(), namespace, packId });
28266
29139
  return Number(res.changes) > 0;
28267
29140
  }
28268
- /** Enable or disable one installed pack. */
29141
+ /**
29142
+ * Enable or disable one installed pack.
29143
+ *
29144
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29145
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29146
+ * merely another point below the floor, and why re-enabling stays open. Like
29147
+ * the assignment above, the check belongs at this write path rather than on a
29148
+ * surface: this is the one device-local writer of the column, and a refusal
29149
+ * that lived in a page would leave the CLI free.
29150
+ */
28269
29151
  setEnabled(namespace, packId, enabled) {
29152
+ const floor = this.policyFloor(namespace, packId);
29153
+ if (floor !== null) {
29154
+ const refusal = packEnablementRefusal(enabled, floor);
29155
+ if (refusal !== null) {
29156
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29157
+ }
29158
+ }
28270
29159
  const res = this.db.prepare(
28271
29160
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28272
29161
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28352,7 +29241,7 @@ var SqliteInventoryRepository = class {
28352
29241
  };
28353
29242
 
28354
29243
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28355
- import { randomUUID as randomUUID4 } from "crypto";
29244
+ import { randomUUID as randomUUID5 } from "crypto";
28356
29245
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28357
29246
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28358
29247
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28841,7 +29730,7 @@ var SqliteInventoryAssetsRepository = class {
28841
29730
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28842
29731
  VALUES (:id, :projectId, :path, :access, :now, :now)
28843
29732
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28844
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29733
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28845
29734
  }
28846
29735
  return true;
28847
29736
  }
@@ -28862,7 +29751,7 @@ var SqliteInventoryAssetsRepository = class {
28862
29751
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28863
29752
  VALUES (:id, :assetId, :trust, :now, :now)
28864
29753
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28865
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29754
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28866
29755
  }
28867
29756
  this.configRowsCache = void 0;
28868
29757
  return "ok";
@@ -29159,7 +30048,7 @@ var SqliteInventoryAssetsRepository = class {
29159
30048
  };
29160
30049
 
29161
30050
  // ../../packages/persistence/src/repositories/policies.ts
29162
- import { randomUUID as randomUUID5 } from "crypto";
30051
+ import { randomUUID as randomUUID6 } from "crypto";
29163
30052
  var SqlitePoliciesRepository = class {
29164
30053
  constructor(db) {
29165
30054
  this.db = db;
@@ -29194,7 +30083,7 @@ var SqlitePoliciesRepository = class {
29194
30083
  failOpenTransaction(this.db, () => {
29195
30084
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29196
30085
  stmt.run({
29197
- id: randomUUID5(),
30086
+ id: randomUUID6(),
29198
30087
  target: JSON.stringify({ category }),
29199
30088
  action,
29200
30089
  now: Date.now()
@@ -29214,7 +30103,7 @@ var SqlitePoliciesRepository = class {
29214
30103
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29215
30104
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29216
30105
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29217
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
30106
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29218
30107
  }
29219
30108
  // Caps every global per-category policy currently set to block/redact down
29220
30109
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29282,7 +30171,7 @@ var SqlitePolicyCatalogRepository = class {
29282
30171
  };
29283
30172
 
29284
30173
  // ../../packages/persistence/src/repositories/project-files.ts
29285
- import { randomUUID as randomUUID6 } from "crypto";
30174
+ import { randomUUID as randomUUID7 } from "crypto";
29286
30175
  var SqliteProjectFilesRepository = class {
29287
30176
  constructor(db) {
29288
30177
  this.db = db;
@@ -29314,7 +30203,7 @@ var SqliteProjectFilesRepository = class {
29314
30203
  const stamp = Math.max(now, maxStamp + 1);
29315
30204
  for (const file2 of scan2.files) {
29316
30205
  this.upsertStmt.run({
29317
- id: randomUUID6(),
30206
+ id: randomUUID7(),
29318
30207
  projectId,
29319
30208
  path: file2.path,
29320
30209
  name: file2.name,
@@ -29328,9 +30217,9 @@ var SqliteProjectFilesRepository = class {
29328
30217
  };
29329
30218
 
29330
30219
  // ../../packages/persistence/src/repositories/resolutions.ts
29331
- import { randomUUID as randomUUID7 } from "crypto";
30220
+ import { randomUUID as randomUUID8 } from "crypto";
29332
30221
  var SqliteResolutionsRepository = class {
29333
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30222
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29334
30223
  this.db = db;
29335
30224
  this.now = now;
29336
30225
  this.newId = newId;
@@ -29543,7 +30432,7 @@ var SqliteScanLedgerRepository = class {
29543
30432
  };
29544
30433
 
29545
30434
  // ../../packages/persistence/src/repositories/secret-vault.ts
29546
- import { randomUUID as randomUUID8 } from "crypto";
30435
+ import { randomUUID as randomUUID9 } from "crypto";
29547
30436
  function pageLimit(requested, fallback) {
29548
30437
  if (requested === void 0) return fallback;
29549
30438
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29589,12 +30478,14 @@ var SELECT_COLUMNS = `
29589
30478
  ciphertext,
29590
30479
  nonce,
29591
30480
  auth_tag AS authTag,
30481
+ user_authorized AS userAuthorized,
29592
30482
  occurrence_count AS occurrenceCount,
29593
30483
  first_seen AS firstSeen,
29594
30484
  last_seen AS lastSeen`;
29595
30485
  function toRow(raw) {
29596
- const { provider, ...rest } = raw;
29597
- return provider === null ? rest : { ...rest, provider };
30486
+ const { provider, userAuthorized, ...rest } = raw;
30487
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30488
+ return provider === null ? row : { ...row, provider };
29598
30489
  }
29599
30490
  var SqliteSecretVaultRepository = class {
29600
30491
  constructor(db) {
@@ -29604,17 +30495,18 @@ var SqliteSecretVaultRepository = class {
29604
30495
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29605
30496
  format_version, category, rule_id, masked_match, provider,
29606
30497
  ciphertext, nonce, auth_tag,
29607
- occurrence_count, first_seen, last_seen
30498
+ user_authorized, occurrence_count, first_seen, last_seen
29608
30499
  ) VALUES (
29609
30500
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29610
30501
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29611
30502
  :ciphertext, :nonce, :authTag,
29612
- 1, :now, :now
30503
+ :userAuthorized, 1, :now, :now
29613
30504
  )`
29614
30505
  );
29615
30506
  this.bumpStmt = db.prepare(
29616
30507
  `UPDATE secret_vault
29617
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30508
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30509
+ user_authorized = max(user_authorized, :userAuthorized)
29618
30510
  WHERE value_fingerprint = :valueFingerprint`
29619
30511
  );
29620
30512
  this.byPointerStmt = db.prepare(
@@ -29634,6 +30526,7 @@ var SqliteSecretVaultRepository = class {
29634
30526
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29635
30527
  WHERE pointer_id = :pointerId`
29636
30528
  );
30529
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29637
30530
  this.derefStmt = db.prepare(
29638
30531
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29639
30532
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29647,6 +30540,7 @@ var SqliteSecretVaultRepository = class {
29647
30540
  listStmt;
29648
30541
  replaceCiphertextStmt;
29649
30542
  refreshFingerprintStmt;
30543
+ deleteByPointerStmt;
29650
30544
  derefStmt;
29651
30545
  /**
29652
30546
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29655,6 +30549,11 @@ var SqliteSecretVaultRepository = class {
29655
30549
  * pointer, category and ciphertext, so the same secret always resolves to one
29656
30550
  * wire token. `minted` is true only when this call created the row.
29657
30551
  *
30552
+ * `userAuthorized` is the one field a repeat call may still change, and only
30553
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30554
+ * the row is shared with every automatic path that vaults the same value. See
30555
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30556
+ *
29658
30557
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29659
30558
  * writers cannot both decide they are minting.
29660
30559
  */
@@ -29681,13 +30580,18 @@ var SqliteSecretVaultRepository = class {
29681
30580
  ciphertext: input2.ciphertext,
29682
30581
  nonce: input2.nonce,
29683
30582
  authTag: input2.authTag,
30583
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29684
30584
  now
29685
30585
  })
29686
30586
  );
29687
30587
  minted = true;
29688
30588
  return;
29689
30589
  }
29690
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30590
+ this.bumpStmt.run({
30591
+ valueFingerprint: input2.valueFingerprint,
30592
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30593
+ now
30594
+ });
29691
30595
  },
29692
30596
  "IMMEDIATE"
29693
30597
  );
@@ -29747,6 +30651,42 @@ var SqliteSecretVaultRepository = class {
29747
30651
  );
29748
30652
  return destroyed;
29749
30653
  }
30654
+ /**
30655
+ * Destroy the named entries and report WHICH ones went — the scoped
30656
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30657
+ * values back where they came from. Ids the store does not hold are absent
30658
+ * from the answer rather than an error, so a set assembled from a stale read
30659
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30660
+ * it.
30661
+ *
30662
+ * The ids come back rather than a count because the caller's next act is to
30663
+ * write a purge row per destroyed entry, and a record of destruction has to
30664
+ * be a record of what was really destroyed: a selection is a claim about a
30665
+ * read that has since gone stale, and auditing from it invents a purge for an
30666
+ * entry still sitting in the vault.
30667
+ *
30668
+ * One transaction over the whole set rather than a statement per id: the
30669
+ * caller hands this the result of a restore pass it has completed, and a
30670
+ * fault partway through must leave the vault as it was found rather than
30671
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30672
+ * stands for, so half a delete is not a state anything can recover from.
30673
+ */
30674
+ deleteByPointerIds(pointerIds) {
30675
+ if (pointerIds.length === 0) return [];
30676
+ const deleted = [];
30677
+ withTransaction(
30678
+ this.db,
30679
+ () => {
30680
+ for (const pointerId of pointerIds) {
30681
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30682
+ deleted.push(pointerId);
30683
+ }
30684
+ }
30685
+ },
30686
+ "IMMEDIATE"
30687
+ );
30688
+ return deleted;
30689
+ }
29750
30690
  /**
29751
30691
  * Record (or re-stamp) one place a pointer has been written. One row per
29752
30692
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29759,7 +30699,7 @@ var SqliteSecretVaultRepository = class {
29759
30699
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29760
30700
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29761
30701
  ).run({
29762
- id: randomUUID8(),
30702
+ id: randomUUID9(),
29763
30703
  pointerId: entry.pointerId,
29764
30704
  location: entry.location,
29765
30705
  kind: entry.kind,
@@ -30272,15 +31212,15 @@ var SqliteSecurityRepository = class {
30272
31212
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30273
31213
  const rows = allRows(
30274
31214
  this.db.prepare(
30275
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31215
+ `SELECT e.repo AS repo, count(*) AS c
30276
31216
  FROM inspection_findings f
30277
31217
  JOIN audit_events e ON e.id = f.audit_event_id
30278
31218
  WHERE e.started_at >= :from AND e.started_at < :to
30279
31219
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30280
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30281
- AND json_extract(e.attributes, '$.repo') != ''
30282
- GROUP BY repo
30283
- ORDER BY c DESC, repo
31220
+ AND e.repo IS NOT NULL
31221
+ AND e.repo != ''
31222
+ GROUP BY e.repo
31223
+ ORDER BY c DESC, e.repo
30284
31224
  LIMIT :limit`
30285
31225
  ),
30286
31226
  { from, to: now, limit }
@@ -30342,7 +31282,7 @@ var SqliteSecurityRepository = class {
30342
31282
  `SELECT f.finding_key AS finding_key,
30343
31283
  d.rule_id AS rule_id,
30344
31284
  d.severity AS severity,
30345
- json_extract(e.attributes, '$.file_path') AS path,
31285
+ e.file_path AS path,
30346
31286
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30347
31287
  latest.resolved_at AS latest_resolved_at
30348
31288
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30395,7 +31335,7 @@ var SqliteSecurityRepository = class {
30395
31335
  };
30396
31336
 
30397
31337
  // ../../packages/persistence/src/repositories/shares.ts
30398
- import { randomUUID as randomUUID9 } from "crypto";
31338
+ import { randomUUID as randomUUID10 } from "crypto";
30399
31339
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30400
31340
  var IN_CHUNK = 500;
30401
31341
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30483,7 +31423,7 @@ function buildSummary(dest, endpoints) {
30483
31423
  callSiteCount,
30484
31424
  transports: distinctTransports(transports),
30485
31425
  dataClasses: distinctDataClasses(dataClasses),
30486
- review: buildReviewInfo(dest.trust, transports),
31426
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30487
31427
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30488
31428
  endpoints: endpoints.map(toEndpointSummary)
30489
31429
  };
@@ -30510,7 +31450,7 @@ function buildDetail(dest, endpoints, callSites) {
30510
31450
  lastSeen: new Date(lastSeenMs).toISOString(),
30511
31451
  transports: distinctTransports(transports),
30512
31452
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30513
- review: buildReviewInfo(dest.trust, transports),
31453
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30514
31454
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30515
31455
  note: dest.note,
30516
31456
  endpoints: endpoints.map((ep) => ({
@@ -30539,7 +31479,11 @@ var SqliteSharesRepository = class {
30539
31479
  FROM share_destination d
30540
31480
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30541
31481
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30542
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31482
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31483
+ AND NOT EXISTS (
31484
+ SELECT 1 FROM egress_decision_override o
31485
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31486
+ )`
30543
31487
  );
30544
31488
  const kindCounts = countBy(
30545
31489
  this.db,
@@ -30651,7 +31595,7 @@ var SqliteSharesRepository = class {
30651
31595
  (id, destination_id, host, decision, created_at, updated_at)
30652
31596
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30653
31597
  ).run({
30654
- id: randomUUID9(),
31598
+ id: randomUUID10(),
30655
31599
  destinationId,
30656
31600
  host: dest.host,
30657
31601
  decision,
@@ -30800,7 +31744,7 @@ var SqliteSharesRepository = class {
30800
31744
  let destinationId = destIds.get(hit.host);
30801
31745
  if (destinationId === void 0) {
30802
31746
  destStmt.run({
30803
- id: randomUUID9(),
31747
+ id: randomUUID10(),
30804
31748
  kind: hit.kind,
30805
31749
  name: hit.name,
30806
31750
  host: hit.host,
@@ -30816,7 +31760,7 @@ var SqliteSharesRepository = class {
30816
31760
  let endpointId = endpointIds.get(endpointKey);
30817
31761
  if (endpointId === void 0) {
30818
31762
  endpointStmt.run({
30819
- id: randomUUID9(),
31763
+ id: randomUUID10(),
30820
31764
  destinationId,
30821
31765
  method: hit.method,
30822
31766
  transport: hit.transport,
@@ -30829,7 +31773,7 @@ var SqliteSharesRepository = class {
30829
31773
  endpointIds.set(endpointKey, endpointId);
30830
31774
  }
30831
31775
  siteStmt.run({
30832
- id: randomUUID9(),
31776
+ id: randomUUID10(),
30833
31777
  endpointId,
30834
31778
  project: input2.project,
30835
31779
  projectKey: input2.projectKey,
@@ -31194,6 +32138,7 @@ function purgeSampleData(db) {
31194
32138
  }
31195
32139
 
31196
32140
  // ../../packages/persistence/src/database.ts
32141
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31197
32142
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31198
32143
  "aka.persistence.unsafeTestOnlyRawHandle"
31199
32144
  );
@@ -31241,7 +32186,7 @@ function backupLegacyStore(db, file2) {
31241
32186
  discardStore(file2, backup);
31242
32187
  return backup;
31243
32188
  }
31244
- function openAndInitialize(file2) {
32189
+ function openAndInitialize(file2, base) {
31245
32190
  let db = openWithPragmas(file2);
31246
32191
  try {
31247
32192
  if (isForeignSqliteLineage(db)) {
@@ -31254,7 +32199,7 @@ function openAndInitialize(file2) {
31254
32199
  applyMigrations(db, file2);
31255
32200
  tightenPerms(file2);
31256
32201
  const policies = new SqlitePoliciesRepository(db);
31257
- const installedPacks = new SqliteInstalledPacksRepository(db);
32202
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31258
32203
  const repositories = {
31259
32204
  events: new SqliteEventsRepository(db),
31260
32205
  findings: new SqliteFindingsRepository(db),
@@ -31290,7 +32235,7 @@ function openAndInitialize(file2) {
31290
32235
  }
31291
32236
  function openLocalDatabase(dir) {
31292
32237
  ensureDataDirSync(dir);
31293
- const file2 = join4(dir, DB_FILENAME);
32238
+ const file2 = join7(dir, DB_FILENAME);
31294
32239
  reapStalePartials(file2);
31295
32240
  const {
31296
32241
  db,
@@ -31318,7 +32263,13 @@ function openLocalDatabase(dir) {
31318
32263
  inspectionDefinitions,
31319
32264
  inspectionFindings,
31320
32265
  configInventory
31321
- } = openAndInitialize(file2);
32266
+ } = openAndInitialize(
32267
+ file2,
32268
+ // `dir` is always `<base>/data` — every caller resolves it through
32269
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32270
+ // settings/ and data/, and the pack-policy floor needs both halves.
32271
+ dirname2(dir)
32272
+ );
31322
32273
  function captureRowId(event) {
31323
32274
  return captureId(
31324
32275
  event.metadata?.sessionId ?? null,
@@ -31331,6 +32282,21 @@ function openLocalDatabase(dir) {
31331
32282
  historySync.markSynced([captureRowId(event)], atMs);
31332
32283
  });
31333
32284
  }
32285
+ function markCaptureOwed(event) {
32286
+ failOpenTransaction(db, () => {
32287
+ historySync.markCaptureOwed(captureRowId(event));
32288
+ });
32289
+ }
32290
+ function markAuditEventsDelivered(events2, atMs) {
32291
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32292
+ if (stampable.length === 0) return;
32293
+ failOpenTransaction(db, () => {
32294
+ historySync.markSynced(
32295
+ stampable.map((event) => event.id),
32296
+ atMs
32297
+ );
32298
+ });
32299
+ }
31334
32300
  function recordCapture(event, detected) {
31335
32301
  failOpenTransaction(db, () => {
31336
32302
  const sessionId = event.metadata?.sessionId;
@@ -31417,7 +32383,7 @@ function openLocalDatabase(dir) {
31417
32383
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31418
32384
  if (!definitionId) continue;
31419
32385
  inspectionFindings.insertFinding({
31420
- id: randomUUID10(),
32386
+ id: randomUUID11(),
31421
32387
  auditEventId: record2.scanEvent.id,
31422
32388
  inspectionDefinitionId: definitionId,
31423
32389
  span: finding.span,
@@ -31497,252 +32463,40 @@ function openLocalDatabase(dir) {
31497
32463
  historySync,
31498
32464
  secretVault,
31499
32465
  exceptions,
31500
- resolutions,
31501
- ruleProbeCache,
31502
- security,
31503
- detections,
31504
- shares,
31505
- policyCatalog,
31506
- inventory,
31507
- inventoryAssets,
31508
- activity,
31509
- sourceProject,
31510
- auditEvents,
31511
- classifiedData,
31512
- inspectionDefinitions,
31513
- inspectionFindings,
31514
- recordCapture,
31515
- markCaptureDelivered,
31516
- ensureInventory,
31517
- recordConfigScan,
31518
- recordProjectFiles,
31519
- reconcileWorktreeProjects,
31520
- configInventoryReport: () => configInventory.report(),
31521
- facets,
31522
- purgeSampleData: () => {
31523
- purgeSampleData(db);
31524
- },
31525
- transaction,
31526
- close: () => {
31527
- db.close();
31528
- },
31529
- // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
31530
- [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
31531
- };
31532
- }
31533
-
31534
- // ../../packages/persistence/src/file-lock.ts
31535
- import { randomUUID as randomUUID11 } from "crypto";
31536
- import {
31537
- closeSync,
31538
- existsSync as existsSync2,
31539
- openSync,
31540
- readFileSync as readFileSync2,
31541
- rmSync as rmSync5,
31542
- statSync as statSync3,
31543
- writeFileSync as writeFileSync2
31544
- } from "fs";
31545
- import { hostname as hostname3 } from "os";
31546
- var LOCK_SUFFIX = ".lock";
31547
- var DEFAULT_TIMEOUT_MS = 5e3;
31548
- var DEFAULT_STALE_MS = 2e3;
31549
- var RETRY_INTERVAL_MS = 5;
31550
- var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
31551
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31552
- function sleepSync(ms) {
31553
- Atomics.wait(PARK, 0, 0, ms);
31554
- }
31555
- var FileLockError = class extends Error {
31556
- reason;
31557
- file;
31558
- holderPid;
31559
- constructor(reason, file2, detail, holderPid, options) {
31560
- super(
31561
- `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
31562
- options
31563
- );
31564
- this.name = "FileLockError";
31565
- this.reason = reason;
31566
- this.file = file2;
31567
- this.holderPid = holderPid;
31568
- }
31569
- };
31570
- function lockPathFor(file2) {
31571
- return `${file2}${LOCK_SUFFIX}`;
31572
- }
31573
- function readLockBody(lock) {
31574
- let raw;
31575
- try {
31576
- raw = readFileSync2(lock, "utf8");
31577
- } catch {
31578
- return null;
31579
- }
31580
- try {
31581
- const parsed = JSON.parse(raw);
31582
- const { pid, token, at, host } = parsed;
31583
- if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
31584
- return { pid, token, at, host: typeof host === "string" ? host : "" };
31585
- } catch {
31586
- return null;
31587
- }
31588
- }
31589
- function holderIsAlive(pid) {
31590
- if (!Number.isInteger(pid) || pid <= 0) return false;
31591
- try {
31592
- process.kill(pid, 0);
31593
- return true;
31594
- } catch (err) {
31595
- return err.code !== "ESRCH";
31596
- }
31597
- }
31598
- function directoryAcceptsCreates(lock) {
31599
- const probe = `${lock}.probe-${randomUUID11()}`;
31600
- try {
31601
- closeSync(openSync(probe, "wx", DATA_FILE_MODE));
31602
- return true;
31603
- } catch {
31604
- return false;
31605
- } finally {
31606
- try {
31607
- rmSync5(probe, { force: true });
31608
- } catch {
31609
- }
31610
- }
31611
- }
31612
- function tryAcquire(lock, file2) {
31613
- const token = randomUUID11();
31614
- let fd;
31615
- try {
31616
- fd = openSync(lock, "wx", DATA_FILE_MODE);
31617
- } catch (err) {
31618
- const code = err.code ?? "";
31619
- if (code === "EEXIST") return null;
31620
- if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync2(lock) || directoryAcceptsCreates(lock))) {
31621
- return null;
31622
- }
31623
- throw new FileLockError(
31624
- "unavailable",
31625
- file2,
31626
- err instanceof Error ? err.message : String(err),
31627
- void 0,
31628
- { cause: err }
31629
- );
31630
- }
31631
- const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
31632
- try {
31633
- writeFileSync2(fd, `${JSON.stringify(body)}
31634
- `);
31635
- } catch {
31636
- try {
31637
- closeSync(fd);
31638
- } catch {
31639
- }
31640
- rmSync5(lock, { force: true });
31641
- return null;
31642
- }
31643
- try {
31644
- closeSync(fd);
31645
- } catch {
31646
- }
31647
- return token;
31648
- }
31649
- function isAbandoned(body, lock, staleMs) {
31650
- if (!body) {
31651
- try {
31652
- return Date.now() - statSync3(lock).mtimeMs >= staleMs;
31653
- } catch {
31654
- return false;
31655
- }
31656
- }
31657
- if (Date.now() - body.at < staleMs) return false;
31658
- if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
31659
- return Date.now() - body.at >= abandonWindow(staleMs);
31660
- }
31661
- function breakIfStale(lock, staleMs) {
31662
- const breaker2 = `${lock}.break`;
31663
- let fd;
31664
- try {
31665
- fd = openSync(breaker2, "wx", DATA_FILE_MODE);
31666
- } catch {
31667
- reapAbandonedBreaker(breaker2);
31668
- return false;
31669
- }
31670
- try {
31671
- closeSync(fd);
31672
- } catch {
31673
- }
31674
- try {
31675
- if (!existsSync2(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
31676
- rmSync5(lock, { force: true });
31677
- return true;
31678
- } catch {
31679
- return false;
31680
- } finally {
31681
- try {
31682
- rmSync5(breaker2, { force: true });
31683
- } catch {
31684
- }
31685
- }
31686
- }
31687
- var BREAKER_ABANDONED_MS = 1e4;
31688
- function reapAbandonedBreaker(breaker2) {
31689
- try {
31690
- if (Date.now() - statSync3(breaker2).mtimeMs >= BREAKER_ABANDONED_MS) {
31691
- rmSync5(breaker2, { force: true });
31692
- }
31693
- } catch {
31694
- }
31695
- }
31696
- function abandonWindow(staleMs) {
31697
- return Math.max(staleMs * 30, 6e4);
31698
- }
31699
- function release(lock, token) {
31700
- try {
31701
- if (readLockBody(lock)?.token !== token) return;
31702
- rmSync5(lock, { force: true });
31703
- } catch {
31704
- }
31705
- }
31706
- function withFileLock(file2, fn, options = {}) {
31707
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31708
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
31709
- const lock = lockPathFor(file2);
31710
- const deadline = Date.now() + timeoutMs;
31711
- let token = tryAcquire(lock, file2);
31712
- while (token === null) {
31713
- if (breakIfStale(lock, staleMs)) {
31714
- token = tryAcquire(lock, file2);
31715
- continue;
31716
- }
31717
- if (Date.now() >= deadline) {
31718
- throw new FileLockError(
31719
- "timeout",
31720
- file2,
31721
- `still held after ${String(timeoutMs)}ms`,
31722
- readLockBody(lock)?.pid
31723
- );
31724
- }
31725
- sleepSync(RETRY_INTERVAL_MS);
31726
- token = tryAcquire(lock, file2);
31727
- }
31728
- try {
31729
- const result = fn();
31730
- if (isThenable(result)) {
31731
- void result.then(
31732
- () => void 0,
31733
- () => void 0
31734
- );
31735
- throw new TypeError(
31736
- `withFileLock(${file2}) was given an async body; the lock is released as soon as it returns, so the awaited work would run unguarded. Pass a synchronous function.`
31737
- );
31738
- }
31739
- return result;
31740
- } finally {
31741
- release(lock, token);
31742
- }
31743
- }
31744
- function isThenable(value) {
31745
- return typeof value === "object" && value !== null && typeof value.then === "function";
32466
+ resolutions,
32467
+ ruleProbeCache,
32468
+ security,
32469
+ detections,
32470
+ shares,
32471
+ policyCatalog,
32472
+ inventory,
32473
+ inventoryAssets,
32474
+ activity,
32475
+ sourceProject,
32476
+ auditEvents,
32477
+ classifiedData,
32478
+ inspectionDefinitions,
32479
+ inspectionFindings,
32480
+ recordCapture,
32481
+ markCaptureDelivered,
32482
+ markCaptureOwed,
32483
+ markAuditEventsDelivered,
32484
+ ensureInventory,
32485
+ recordConfigScan,
32486
+ recordProjectFiles,
32487
+ reconcileWorktreeProjects,
32488
+ configInventoryReport: () => configInventory.report(),
32489
+ facets,
32490
+ purgeSampleData: () => {
32491
+ purgeSampleData(db);
32492
+ },
32493
+ transaction,
32494
+ close: () => {
32495
+ db.close();
32496
+ },
32497
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
32498
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
32499
+ };
31746
32500
  }
31747
32501
 
31748
32502
  // ../../packages/persistence/src/finding-key.ts
@@ -31750,240 +32504,18 @@ import { createHash as createHash3 } from "crypto";
31750
32504
 
31751
32505
  // ../../packages/persistence/src/fingerprint.ts
31752
32506
  import { createHmac, randomBytes } from "crypto";
31753
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31754
- import { join as join5 } from "path";
32507
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32508
+ import { join as join8 } from "path";
31755
32509
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31756
32510
 
31757
32511
  // ../../packages/persistence/src/history-preview.ts
31758
32512
  import { existsSync as existsSync4 } from "fs";
31759
- import { join as join6 } from "path";
32513
+ import { join as join9 } from "path";
31760
32514
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31761
32515
 
31762
- // ../../packages/persistence/src/local-layout.ts
31763
- import { renameSync as renameSync3 } from "fs";
31764
- import { mkdir } from "fs/promises";
31765
- import { homedir } from "os";
31766
- import { join as join7 } from "path";
31767
- function defaultDataDir() {
31768
- return join7(homedir(), ".aka");
31769
- }
31770
- function settingsDir(base = defaultDataDir()) {
31771
- return join7(base, "settings");
31772
- }
31773
- function dataDir(base = defaultDataDir()) {
31774
- return join7(base, "data");
31775
- }
31776
- function dbPath(base = defaultDataDir()) {
31777
- return join7(dataDir(base), "aka.db");
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 managedContextOf(managed) {
31829
- if (!managed) return NO_MANAGED_CONTEXT;
31830
- return {
31831
- present: true,
31832
- ...managed.organization === void 0 ? {} : { organization: managed.organization },
31833
- lockedFields: managed.lockedFields
31834
- };
31835
- }
31836
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31837
- if (!managed) return settings;
31838
- const { values } = managed;
31839
- const merged = { ...settings };
31840
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31841
- if (values.controlPlane !== void 0) {
31842
- merged.controlPlane = {
31843
- ...values.controlPlane,
31844
- // The administrator pinned WHICH deployment, not WHEN this machine
31845
- // joined it. Keep the user's own attach time when the endpoint is
31846
- // unchanged, so a managed machine does not appear to re-attach on every
31847
- // read; stamp a fresh one when the administrator moved it.
31848
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31849
- };
31850
- }
31851
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31852
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31853
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31854
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31855
- if (values.vaultConsent !== void 0) {
31856
- merged.vaultConsent = values.vaultConsent ? (
31857
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31858
- // at the current version otherwise.
31859
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31860
- ) : void 0;
31861
- }
31862
- if (values.modelJudgeConsent !== void 0) {
31863
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31864
- acknowledgedAt: now().toISOString(),
31865
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31866
- } : void 0;
31867
- }
31868
- return merged;
31869
- }
31870
- function lockedAmong(context, requested) {
31871
- if (!context.present) return [];
31872
- return requested.filter((key) => context.lockedFields.includes(key));
31873
- }
31874
-
31875
- // ../../packages/persistence/src/settings.ts
31876
- import { readFileSync as readFileSync5 } from "fs";
31877
- import { join as join8 } from "path";
31878
- var SETTINGS_FILENAME = "settings.json";
31879
- function readWorkspaceSettings(base = defaultDataDir()) {
31880
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31881
- }
31882
- function readUserSettings(base) {
31883
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31884
- if (!record2) return defaultWorkspaceSettings();
31885
- try {
31886
- return WorkspaceSettings.parse(record2);
31887
- } catch {
31888
- return defaultWorkspaceSettings();
31889
- }
31890
- }
31891
- var ManagedFieldError = class extends Error {
31892
- fields;
31893
- constructor(fields) {
31894
- super(`refusing to write administratively locked settings: ${fields.join(", ")}`);
31895
- this.name = "ManagedFieldError";
31896
- this.fields = fields;
31897
- }
31898
- };
31899
- function lockableKeysTouched(current, applied) {
31900
- const keys = [];
31901
- const changed = (key) => key in applied && applied[key] !== current[key];
31902
- const descriptorChanged = "controlPlane" in applied && (applied.controlPlane?.endpoint !== current.controlPlane?.endpoint || applied.controlPlane?.label !== current.controlPlane?.label);
31903
- if (changed("runMode") || descriptorChanged) keys.push("runMode");
31904
- if (changed("historicalAccess")) keys.push("historicalAccess");
31905
- if (changed("vaultKeyCustody")) keys.push("vaultKeyCustody");
31906
- if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
31907
- if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
31908
- if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
31909
- keys.push("vaultConsent");
31910
- }
31911
- if ("modelJudgeConsent" in applied && isModelJudgeConsentValid(applied.modelJudgeConsent) !== isModelJudgeConsentValid(current.modelJudgeConsent)) {
31912
- keys.push("modelJudgeConsent");
31913
- }
31914
- return keys;
31915
- }
31916
- function pinnedKeys(managed) {
31917
- if (!managed) return [];
31918
- const { values } = managed;
31919
- const keys = [];
31920
- if (values.runMode !== void 0 || values.controlPlane !== void 0) keys.push("runMode");
31921
- if (values.historicalAccess !== void 0) keys.push("historicalAccess");
31922
- if (values.vaultConsent !== void 0) keys.push("vaultConsent");
31923
- if (values.vaultKeyCustody !== void 0) keys.push("vaultKeyCustody");
31924
- if (values.vaultInlineReveal !== void 0) keys.push("vaultInlineReveal");
31925
- if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
31926
- if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
31927
- return keys;
31928
- }
31929
- function withoutManagedKeys(applied, managed, pinned, touched) {
31930
- if (!managed.present) return applied;
31931
- const strip = (key) => (managed.lockedFields.includes(key) || pinned.includes(key)) && !touched.includes(key);
31932
- const out = { ...applied };
31933
- if (strip("runMode")) {
31934
- delete out.runMode;
31935
- delete out.controlPlane;
31936
- }
31937
- if (strip("historicalAccess")) delete out.historicalAccess;
31938
- if (strip("vaultConsent")) delete out.vaultConsent;
31939
- if (strip("vaultKeyCustody")) delete out.vaultKeyCustody;
31940
- if (strip("vaultInlineReveal")) delete out.vaultInlineReveal;
31941
- if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
31942
- if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
31943
- return out;
31944
- }
31945
- function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
31946
- const dir = settingsDir(base);
31947
- ensureDataDirSync(dir);
31948
- const file2 = join8(dir, SETTINGS_FILENAME);
31949
- const managedSettings = managedOverride === void 0 ? readManagedSettings() : managedOverride;
31950
- const managed = managedContextOf(managedSettings);
31951
- return withFileLock(file2, () => {
31952
- const current = readUserSettings(base);
31953
- const applied = typeof answers2 === "function" ? answers2(current) : answers2;
31954
- const effective = overlayManagedSettings(current, managedSettings);
31955
- const touched = lockableKeysTouched(effective, applied);
31956
- const refused = lockedAmong(managed, touched);
31957
- if (refused.length > 0) throw new ManagedFieldError(refused);
31958
- const merged = WorkspaceSettings.parse({
31959
- ...current,
31960
- // Locked keys are stripped rather than merged. Everything still here is,
31961
- // by the refusal above, an unchanged ECHO of the administrator's value —
31962
- // so dropping it discards no answer of the user's, and writing it would
31963
- // persist the pin into their file, where it would outlive the managed
31964
- // file and read as their own choice once the lock was gone.
31965
- ...withoutManagedKeys(applied, managed, pinnedKeys(managedSettings), touched),
31966
- // First setup stamps the time; later edits keep the original completion mark.
31967
- onboardedAt: applied.onboardedAt ?? current.onboardedAt ?? (/* @__PURE__ */ new Date()).toISOString()
31968
- });
31969
- writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
31970
- `);
31971
- return merged;
31972
- });
31973
- }
31974
- function readJson(file2) {
31975
- let text;
31976
- try {
31977
- text = readFileSync5(file2, "utf8");
31978
- } catch {
31979
- return null;
31980
- }
31981
- return parseJsonObject(text) ?? null;
31982
- }
31983
-
31984
32516
  // ../../packages/persistence/src/store-symlinks.ts
31985
32517
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31986
- import { dirname as dirname2, join as join9, resolve } from "path";
32518
+ import { dirname as dirname3, join as join10, resolve } from "path";
31987
32519
 
31988
32520
  // ../../packages/persistence/src/vault/crypto.ts
31989
32521
  import {
@@ -31997,19 +32529,19 @@ import {
31997
32529
  // ../../packages/persistence/src/vault/key-provider.ts
31998
32530
  import { execFileSync } from "child_process";
31999
32531
  import { randomBytes as randomBytes2 } from "crypto";
32000
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32001
- import { join as join10 } from "path";
32532
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32533
+ import { join as join11 } from "path";
32002
32534
 
32003
32535
  // ../../packages/persistence/src/vault/vault.ts
32004
32536
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32005
32537
 
32006
32538
  // ../../packages/persistence/src/warn-era-cap.ts
32007
32539
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32008
- import { join as join11 } from "path";
32540
+ import { join as join12 } from "path";
32009
32541
  var MARKER = "warn-era-capped";
32010
32542
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32011
32543
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32012
- const marker = join11(dataDir2, MARKER);
32544
+ const marker = join12(dataDir2, MARKER);
32013
32545
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32014
32546
  const capped = db.policies.capCategoryActions();
32015
32547
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32019,7 +32551,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32019
32551
 
32020
32552
  // ../../packages/plugin-sdk/src/config.ts
32021
32553
  import { existsSync as existsSync7 } from "fs";
32022
- import { join as join12 } from "path";
32554
+ import { join as join13 } from "path";
32023
32555
 
32024
32556
  // ../../packages/plugin-sdk/src/provider-env.ts
32025
32557
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32073,7 +32605,7 @@ function resolveProvider() {
32073
32605
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32074
32606
  try {
32075
32607
  ensureLayoutDirSync(base);
32076
- const settingsFile = join12(settingsDir(base), "settings.json");
32608
+ const settingsFile = join13(settingsDir(base), "settings.json");
32077
32609
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
32078
32610
  } catch {
32079
32611
  }
@@ -32097,9 +32629,9 @@ function resolveProviderSafe(resolveProviderFn) {
32097
32629
  }
32098
32630
 
32099
32631
  // ../../packages/plugin-sdk/src/config-inventory.ts
32100
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32632
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32101
32633
  import { homedir as homedir2 } from "os";
32102
- import { basename as basename3, join as join14 } from "path";
32634
+ import { basename as basename3, join as join15 } from "path";
32103
32635
 
32104
32636
  // ../../packages/detections/src/egress/registry.ts
32105
32637
  var EXTRACTOR_VERSION = "1";
@@ -32857,8 +33389,8 @@ var CPU_CORROBORATION_SHARE = 0.2;
32857
33389
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
32858
33390
 
32859
33391
  // ../../packages/plugin-sdk/src/repo.ts
32860
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
32861
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
33392
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
33393
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
32862
33394
 
32863
33395
  // ../../packages/plugin-sdk/src/events.ts
32864
33396
  import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
@@ -32870,8 +33402,8 @@ import { Worker } from "worker_threads";
32870
33402
 
32871
33403
  // ../../packages/plugin-sdk/src/ignore-layers.ts
32872
33404
  var import_ignore = __toESM(require_ignore(), 1);
32873
- import { readFileSync as readFileSync9 } from "fs";
32874
- import { join as join15 } from "path";
33405
+ import { readFileSync as readFileSync10 } from "fs";
33406
+ import { join as join16 } from "path";
32875
33407
 
32876
33408
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
32877
33409
  import { arch, hostname as hostname4, platform, release as release2 } from "os";
@@ -32882,20 +33414,20 @@ import {
32882
33414
  fstatSync,
32883
33415
  mkdirSync as mkdirSync2,
32884
33416
  openSync as openSync2,
32885
- readFileSync as readFileSync10,
33417
+ readFileSync as readFileSync11,
32886
33418
  readSync,
32887
33419
  writeFileSync as writeFileSync5
32888
33420
  } from "fs";
32889
- import { join as join16 } from "path";
33421
+ import { join as join17 } from "path";
32890
33422
  var TAIL_BYTES = 256 * 1024;
32891
33423
 
32892
33424
  // ../../packages/plugin-sdk/src/nudge.ts
32893
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
32894
- import { join as join17 } from "path";
33425
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
33426
+ import { join as join18 } from "path";
32895
33427
 
32896
33428
  // ../../packages/plugin-sdk/src/paths.ts
32897
33429
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
32898
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
33430
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
32899
33431
 
32900
33432
  // ../../packages/plugin-sdk/src/posture.ts
32901
33433
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
@@ -32909,7 +33441,7 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
32909
33441
 
32910
33442
  // ../../packages/plugin-sdk/src/project-files.ts
32911
33443
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
32912
- import { basename as basename5, join as join18 } from "path";
33444
+ import { basename as basename5, join as join19 } from "path";
32913
33445
 
32914
33446
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
32915
33447
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -32945,7 +33477,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
32945
33477
 
32946
33478
  // ../../packages/plugin-sdk/src/throttle.ts
32947
33479
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
32948
- import { join as join19 } from "path";
33480
+ import { join as join20 } from "path";
32949
33481
 
32950
33482
  // ../../packages/setup-wizard/src/onboard-posture.ts
32951
33483
  function parsePosture(json2) {
@@ -32968,7 +33500,7 @@ function parsePosture(json2) {
32968
33500
 
32969
33501
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
32970
33502
  import { writeFileSync as writeFileSync8 } from "fs";
32971
- import { join as join20 } from "path";
33503
+ import { join as join21 } from "path";
32972
33504
 
32973
33505
  // ../../packages/setup-wizard/src/triage/merge.ts
32974
33506
  var RANK = Object.fromEntries(
@@ -32976,9 +33508,9 @@ var RANK = Object.fromEntries(
32976
33508
  );
32977
33509
 
32978
33510
  // ../../packages/setup-wizard/src/triage/plan-file.ts
32979
- import { mkdtempSync, readFileSync as readFileSync12, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
33511
+ import { mkdtempSync, readFileSync as readFileSync13, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
32980
33512
  import { tmpdir } from "os";
32981
- import { basename as basename6, dirname as dirname5, join as join21 } from "path";
33513
+ import { basename as basename6, dirname as dirname6, join as join22 } from "path";
32982
33514
  var SuppressionEntrySchema = external_exports.object({
32983
33515
  ruleId: external_exports.string(),
32984
33516
  category: DetectionCategory,