@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.
@@ -492,14 +492,15 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // src/apply-suppressions.ts
495
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
495
+ import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
496
496
  import { userInfo } from "os";
497
- import { dirname as dirname7, join as join23 } from "path";
497
+ import { dirname as dirname8, join as join24 } from "path";
498
498
  import { fileURLToPath as fileURLToPath4 } from "url";
499
499
 
500
500
  // ../../packages/persistence/src/attached-derived.ts
501
501
  import { rmSync } from "fs";
502
502
  import { join } from "path";
503
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
503
504
 
504
505
  // ../../packages/persistence/src/control-plane-credential.ts
505
506
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
@@ -598,6 +599,30 @@ var SQLITE_MIGRATIONS = [
598
599
  {
599
600
  tag: "0022_audit_inspection_ms",
600
601
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
602
+ },
603
+ {
604
+ tag: "0023_secret_vault_user_authorized",
605
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
606
+ },
607
+ {
608
+ tag: "0024_finding_resolution_key_created_index",
609
+ 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`);"
610
+ },
611
+ {
612
+ tag: "0025_audit_capture_attribute_columns",
613
+ 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;"
614
+ },
615
+ {
616
+ tag: "0026_audit_llm_call_usage_columns",
617
+ 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;"
618
+ },
619
+ {
620
+ tag: "0027_audit_llm_usage_index",
621
+ 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;"
622
+ },
623
+ {
624
+ tag: "0028_activity_session_probe_indexes",
625
+ 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"
601
626
  }
602
627
  ];
603
628
 
@@ -22136,6 +22161,26 @@ var AttachTokenResponse = external_exports.union([
22136
22161
  AttachTokenExpired,
22137
22162
  external_exports.object({ status: printable(64) })
22138
22163
  ]);
22164
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22165
+ var DeviceCommand = external_exports.object({
22166
+ id: printable(128).min(1),
22167
+ kind: DeviceCommandKind,
22168
+ issuedAt: printable(64).min(1),
22169
+ expiresAt: printable(64).min(1)
22170
+ }).strict();
22171
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22172
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22173
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22174
+ external_exports.object({
22175
+ outcome: external_exports.literal("reported"),
22176
+ projectsScanned: external_exports.number().int().nonnegative()
22177
+ }).strict(),
22178
+ external_exports.object({
22179
+ outcome: external_exports.literal("failed"),
22180
+ reason: DeviceCommandFailureReason,
22181
+ projectsScanned: external_exports.number().int().nonnegative()
22182
+ }).strict()
22183
+ ]);
22139
22184
 
22140
22185
  // ../../packages/schema/src/zod/registry.ts
22141
22186
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22302,7 +22347,7 @@ var PackManifest = external_exports.object({
22302
22347
  }).meta({ id: "PackManifest" });
22303
22348
 
22304
22349
  // ../../packages/schema/src/zod/detection.ts
22305
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22350
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22306
22351
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22307
22352
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22308
22353
  var DetectionCounts = external_exports.object({
@@ -22439,14 +22484,17 @@ function optional2(key, parsed, raw) {
22439
22484
  function isStringArray(value) {
22440
22485
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22441
22486
  }
22487
+ var ORIGIN_VALUES = { library: true, custom: true };
22488
+ function resolveOrigin(origin) {
22489
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22490
+ }
22442
22491
  function summaryToDetectionListItem(s) {
22443
22492
  return {
22444
22493
  id: `${s.namespace}/${s.packId}`,
22445
22494
  name: s.name,
22446
22495
  version: s.version,
22447
22496
  enabled: s.enabled,
22448
- origin: "library",
22449
- // v1: every installed pack is library origin
22497
+ origin: resolveOrigin(s.origin),
22450
22498
  namespace: s.namespace,
22451
22499
  packId: s.packId,
22452
22500
  ruleCount: s.ruleCount,
@@ -22498,7 +22546,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22498
22546
  name: row.name,
22499
22547
  version: row.version,
22500
22548
  enabled: row.enabled,
22501
- origin: "library",
22549
+ origin: resolveOrigin(row.origin),
22502
22550
  namespace: row.namespace,
22503
22551
  packId: row.packId,
22504
22552
  ruleCount: row.rules.length,
@@ -22518,16 +22566,20 @@ function splitDetectionId(id) {
22518
22566
  }
22519
22567
  function buildDetectionsList(summaries, query) {
22520
22568
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22569
+ const originOf = (s) => resolveOrigin(s.origin);
22521
22570
  const counts = {
22522
22571
  all: summaries.length,
22523
- library: summaries.length,
22524
- // all origin=library in v1
22525
- custom: 0,
22572
+ library: summaries.filter((s) => originOf(s) === "library").length,
22573
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22574
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22575
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22576
+ // place, and that state does not exist — editing a library pack forks it. See
22577
+ // OriginEnum.
22526
22578
  customized: 0,
22527
22579
  updates: withUpdate.length
22528
22580
  };
22529
22581
  const filter = query.filter;
22530
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22582
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22531
22583
  if (query.q) {
22532
22584
  const q = query.q.toLowerCase();
22533
22585
  filtered = filtered.filter(
@@ -22607,8 +22659,9 @@ var Event = external_exports.object({
22607
22659
  metadata: EventMetadata.optional()
22608
22660
  }).meta({ id: "Event" });
22609
22661
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22662
+ var INGEST_BATCH_MAX = 100;
22610
22663
  var IngestBatch = external_exports.object({
22611
- events: external_exports.array(IngestEvent).min(1).max(100),
22664
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22612
22665
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22613
22666
  // additionally rejects any event whose contentHash the store has already
22614
22667
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23164,6 +23217,257 @@ var PatchInstalledPackRequest = external_exports.object({
23164
23217
  message: "At least one field must be provided"
23165
23218
  }).meta({ id: "PatchInstalledPackRequest" });
23166
23219
 
23220
+ // ../../packages/schema/src/zod/policy.ts
23221
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23222
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23223
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23224
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23225
+ var Policy = external_exports.object({
23226
+ id: external_exports.guid(),
23227
+ scope: PolicyScope,
23228
+ target: PolicyTarget,
23229
+ action: ActionTaken,
23230
+ enabled: external_exports.boolean().default(true),
23231
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23232
+ // Display name — optional so older policy rows without name still parse.
23233
+ // Added for the findings API (policy.name column migration).
23234
+ name: external_exports.string().optional(),
23235
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23236
+ // which row this is. A producer that collapses several rows onto one target
23237
+ // must carry the marker onto whichever row survives, or the collapse decides
23238
+ // the answer; a survivor may therefore be a built-in expansion still marked
23239
+ // 'authored' because an authored sibling targeted the same thing.
23240
+ // Optional so an older producer — and an older on-disk cache — still parses;
23241
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23242
+ //
23243
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23244
+ // built-in archetype catalog entry a policy is, which every catalog surface
23245
+ // reads and which a caller may state. This one is a statement the PRODUCER
23246
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23247
+ // — the CRUD routes neither accept nor set it.
23248
+ //
23249
+ // A device consumes this in exactly one direction: an 'authored' policy
23250
+ // arriving from a control plane marks the rules it targets as not
23251
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23252
+ // which is what makes it safe to honour from an unsigned cache — the same
23253
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23254
+ provenance: PolicyProvenance.optional()
23255
+ }).meta({ id: "Policy" });
23256
+ var PolicyBundle = external_exports.object({
23257
+ version: external_exports.string(),
23258
+ policies: external_exports.array(Policy),
23259
+ // Rules from the installed marketplace packs (snapshotted by the
23260
+ // control plane). The plugin registers these in addition to its bundled
23261
+ // packs. Optional so older backends — and older on-disk caches — that omit
23262
+ // the field still parse; consumers read `bundle.rules ?? []`.
23263
+ rules: external_exports.array(Rule).optional(),
23264
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23265
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23266
+ // after reading the user's installed snapshot (installed_packs, enabled
23267
+ // packs only), which is how detection updates stay manual: new bundled
23268
+ // rules run only after the user applies the pack update. Absent/false keeps
23269
+ // the historical composition (bundled packs + rules) — older caches.
23270
+ rulesComplete: external_exports.boolean().optional(),
23271
+ // Active detection exceptions, evaluation subset only (see
23272
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23273
+ // on-disk caches — that omit the field still parse; consumers read
23274
+ // `bundle.exceptions ?? []`.
23275
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23276
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23277
+ // A second axis over the same `redact` action, carried beside the policies
23278
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23279
+ // widening Policy itself would change a persisted shape to express something
23280
+ // only the in-memory bundle needs. Optional so an older producer — or an
23281
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23282
+ // pre-existing one-way behaviour, which is the safe direction to default.
23283
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23284
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23285
+ // from a versioned installed pack. Optional so older backends — and older
23286
+ // on-disk caches — that omit the field still parse; consumers fall back to
23287
+ // the rule's own spec version. NOT the bundle version above — see
23288
+ // installedRuleset's ruleVersions for the source of truth.
23289
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23290
+ // Model ids (the raw `model` string a harness reports, e.g.
23291
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23292
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23293
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23294
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23295
+ // unenforced behaviour that predates this field and the safe direction to
23296
+ // default.
23297
+ //
23298
+ // Ids, not display names: the governance decision is keyed on the exact
23299
+ // string the harness reports (`model_status_override.versionId` in the
23300
+ // control plane), so no name resolution stands between the decision and the
23301
+ // comparison.
23302
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23303
+ customKeywords: external_exports.array(external_exports.string()),
23304
+ fetchedAt: external_exports.iso.datetime()
23305
+ }).meta({ id: "PolicyBundle" });
23306
+ var POLICY_BUNDLE_SHAPE_ID = [
23307
+ ...Object.keys(PolicyBundle.shape),
23308
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23309
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23310
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23311
+ ].sort().join(",");
23312
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23313
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23314
+ var CATEGORY_PEAK_SEVERITY = {
23315
+ secret: "critical",
23316
+ financial: "critical",
23317
+ // core-financial/credit-card
23318
+ code_flaw: "critical",
23319
+ pii: "high",
23320
+ phi: "high",
23321
+ custom: "high",
23322
+ // user-defined; conservative
23323
+ code_context: "low",
23324
+ config: "low"
23325
+ // observe-only; floors to monitor regardless
23326
+ };
23327
+ function severityFloorPolicy(category) {
23328
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23329
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23330
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23331
+ }
23332
+ function severityFloorPosture() {
23333
+ const out = {};
23334
+ for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23335
+ return out;
23336
+ }
23337
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23338
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23339
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23340
+ id: "RedactFallback"
23341
+ });
23342
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23343
+ var BUILTIN_POLICY_SPECS = {
23344
+ monitor: {
23345
+ name: "Monitor",
23346
+ action: "log",
23347
+ reversible: false,
23348
+ description: "Log every match for audit. The request is allowed through untouched."
23349
+ },
23350
+ warn: {
23351
+ name: "Warn",
23352
+ action: "warn",
23353
+ reversible: false,
23354
+ description: "Allow the request, but warn the user inline before it is sent."
23355
+ },
23356
+ redact: {
23357
+ name: "Redact",
23358
+ action: "redact",
23359
+ reversible: false,
23360
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23361
+ },
23362
+ vault: {
23363
+ name: "Redact & Vault",
23364
+ action: "redact",
23365
+ reversible: true,
23366
+ 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."
23367
+ },
23368
+ block: {
23369
+ name: "Block",
23370
+ action: "block",
23371
+ reversible: false,
23372
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23373
+ }
23374
+ };
23375
+ function builtinPolicyToAction(id) {
23376
+ return BUILTIN_POLICY_SPECS[id].action;
23377
+ }
23378
+ var PALETTE_WEAKEST_FIRST = [
23379
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23380
+ ];
23381
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23382
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23383
+ );
23384
+ var ACTION_STRENGTH_ORDER = [
23385
+ ...BELOW_PALETTE,
23386
+ ...PALETTE_WEAKEST_FIRST
23387
+ ];
23388
+ function actionRank(action) {
23389
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23390
+ }
23391
+ function isActionAtLeast(action, floor) {
23392
+ return actionRank(action) >= actionRank(floor);
23393
+ }
23394
+ function strongerAction(a, b) {
23395
+ return actionRank(a) >= actionRank(b) ? a : b;
23396
+ }
23397
+ function weakestBuiltinAtLeast(floor) {
23398
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23399
+ }
23400
+ var PackPolicyFloor = external_exports.object({
23401
+ /**
23402
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23403
+ * rather than a raw ActionTaken because that is the vocabulary the user
23404
+ * picks from — a floor a UI cannot name is one it cannot explain.
23405
+ */
23406
+ floor: BuiltinPolicyId,
23407
+ /**
23408
+ * True when the organization AUTHORED a policy governing this pack rather
23409
+ * than stating a minimum: it gave the answer, so the pack is not
23410
+ * re-assignable locally in either direction.
23411
+ */
23412
+ locked: external_exports.boolean()
23413
+ }).describe("PackPolicyFloor");
23414
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23415
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23416
+ );
23417
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23418
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23419
+ );
23420
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23421
+ function builtinPolicyIsReversible(id) {
23422
+ return BUILTIN_POLICY_SPECS[id].reversible;
23423
+ }
23424
+ function policyIdIsReversible(policyId) {
23425
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23426
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23427
+ return builtinPolicyIsReversible(id);
23428
+ }
23429
+ var DEFAULT_ACTIONS = Object.fromEntries(
23430
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23431
+ );
23432
+ var BUILTIN_POLICIES = Object.fromEntries(
23433
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23434
+ );
23435
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23436
+ function policyIdToAction(policyId) {
23437
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23438
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23439
+ return BUILTIN_POLICIES[id].action;
23440
+ }
23441
+ var UsedByItem = external_exports.object({
23442
+ id: external_exports.string(),
23443
+ name: external_exports.string(),
23444
+ ruleCount: external_exports.number().int().nonnegative(),
23445
+ enabled: external_exports.boolean()
23446
+ }).meta({ id: "UsedByItem" });
23447
+ var PolicyListItem = external_exports.object({
23448
+ id: external_exports.string(),
23449
+ kind: PolicyKind,
23450
+ name: external_exports.string(),
23451
+ enabled: external_exports.boolean(),
23452
+ usedByCount: external_exports.number().int().nonnegative()
23453
+ }).meta({ id: "PolicyListItem" });
23454
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23455
+ var PolicyDetail = external_exports.object({
23456
+ specVersion: external_exports.literal(1),
23457
+ id: external_exports.string(),
23458
+ kind: PolicyKind,
23459
+ name: external_exports.string(),
23460
+ enabled: external_exports.boolean(),
23461
+ description: external_exports.string(),
23462
+ usedBy: external_exports.array(UsedByItem)
23463
+ }).meta({ id: "PolicyDetail" });
23464
+ var PolicyStatsResponse = external_exports.object({
23465
+ policies: external_exports.number().int().nonnegative(),
23466
+ builtin: external_exports.number().int().nonnegative(),
23467
+ custom: external_exports.number().int().nonnegative(),
23468
+ detectionsGoverned: external_exports.number().int().nonnegative()
23469
+ }).meta({ id: "PolicyStatsResponse" });
23470
+
23167
23471
  // ../../packages/schema/src/zod/vault.ts
23168
23472
  var POINTER_FORMAT_VERSION = 2;
23169
23473
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23204,6 +23508,14 @@ var VaultEntry = external_exports.object({
23204
23508
  // How many times this value has been detected on this machine — the reuse
23205
23509
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23206
23510
  occurrenceCount: external_exports.number().int().nonnegative(),
23511
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23512
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23513
+ // one row however many paths vault it, so this is what tells a policy sweep
23514
+ // that the row carries somebody's own instruction and not just an assignment
23515
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23516
+ // vaulting of the same value must never clear it — what the user said about
23517
+ // the value does not expire.
23518
+ userAuthorized: external_exports.boolean(),
23207
23519
  firstSeen: external_exports.string(),
23208
23520
  lastSeen: external_exports.string()
23209
23521
  });
@@ -23322,7 +23634,7 @@ var VaultConsent = external_exports.object({
23322
23634
  });
23323
23635
 
23324
23636
  // ../../packages/schema/src/zod/local.ts
23325
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23637
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23326
23638
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23327
23639
  var RunMode = external_exports.enum(["standalone", "attached"]);
23328
23640
  var ControlPlaneConnection = external_exports.object({
@@ -23367,6 +23679,19 @@ var WorkspaceSettings = external_exports.object({
23367
23679
  vaultKeyCustody: VaultKeyCustody.default("file"),
23368
23680
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23369
23681
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23682
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23683
+ // place. Not a handling policy: the policy has already resolved to redact,
23684
+ // and this only says what happens when the host offers no channel to carry it
23685
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23686
+ // Claude Code decline to mask a field that EXECUTES because masking would
23687
+ // change what runs. Per FIELD rather than per host, so a host that can
23688
+ // rewrite some inputs keeps true redaction on those.
23689
+ //
23690
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23691
+ // an attached machine's merge is `strongerAction` over the one action ladder
23692
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23693
+ // word and stays out of the stored value.
23694
+ redactFallback: RedactFallback.default("warn"),
23370
23695
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23371
23696
  onboardedAt: external_exports.iso.datetime().optional(),
23372
23697
  // Records that the user consented to sending findings to the model API for
@@ -23374,15 +23699,20 @@ var WorkspaceSettings = external_exports.object({
23374
23699
  // Absent until granted; a stale payloadVersion means the consent no longer
23375
23700
  // covers the current payload and must be re-granted.
23376
23701
  modelJudgeConsent: ModelJudgeConsent.optional(),
23377
- // Records that the user consented to sending the activity already recorded on
23378
- // this machine to the deployment it is attached to, along with the payload
23379
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23380
- // a different endpoint or an older payload no longer counts.
23702
+ // Records that the user consented to the DEFERRED send — the outbox along
23703
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23704
+ // that covers both the pre-attach backlog and undelivered captures (which
23705
+ // carry prompt/reply text in `content`); the key name predates the widening.
23706
+ // Absent until granted, and a grant for a different endpoint or an older
23707
+ // payload no longer counts.
23381
23708
  historySyncConsent: HistorySyncConsent.optional()
23382
23709
  });
23383
23710
  function defaultWorkspaceSettings() {
23384
23711
  return WorkspaceSettings.parse({});
23385
23712
  }
23713
+ function isAttached(settings) {
23714
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23715
+ }
23386
23716
  function toInventoryRow(input2, id, now) {
23387
23717
  return {
23388
23718
  id,
@@ -23502,7 +23832,8 @@ var ManagedSettingKey = external_exports.enum([
23502
23832
  "vaultKeyCustody",
23503
23833
  "vaultInlineReveal",
23504
23834
  "modelJudgeConsent",
23505
- "dataSharesInPlace"
23835
+ "dataSharesInPlace",
23836
+ "redactFallback"
23506
23837
  ]).meta({ id: "ManagedSettingKey" });
23507
23838
  var ManagedSettingsValues = external_exports.object({
23508
23839
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
@@ -23515,7 +23846,8 @@ var ManagedSettingsValues = external_exports.object({
23515
23846
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23516
23847
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23517
23848
  modelJudgeConsent: external_exports.boolean().optional(),
23518
- dataSharesInPlace: external_exports.boolean().optional()
23849
+ dataSharesInPlace: external_exports.boolean().optional(),
23850
+ redactFallback: RedactFallback.optional()
23519
23851
  }).meta({ id: "ManagedSettingsValues" });
23520
23852
  var ManagedSettings = external_exports.object({
23521
23853
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23530,191 +23862,6 @@ var ManagedSettings = external_exports.object({
23530
23862
  lockedFields: external_exports.array(ManagedSettingKey).default([])
23531
23863
  }).meta({ id: "ManagedSettings" });
23532
23864
 
23533
- // ../../packages/schema/src/zod/policy.ts
23534
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23535
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23536
- var Policy = external_exports.object({
23537
- id: external_exports.guid(),
23538
- scope: PolicyScope,
23539
- target: PolicyTarget,
23540
- action: ActionTaken,
23541
- enabled: external_exports.boolean().default(true),
23542
- customKeywords: external_exports.array(external_exports.string()).optional(),
23543
- // Display name — optional so older policy rows without name still parse.
23544
- // Added for the findings API (policy.name column migration).
23545
- name: external_exports.string().optional()
23546
- }).meta({ id: "Policy" });
23547
- var PolicyBundle = external_exports.object({
23548
- version: external_exports.string(),
23549
- policies: external_exports.array(Policy),
23550
- // Rules from the installed marketplace packs (snapshotted by the
23551
- // control plane). The plugin registers these in addition to its bundled
23552
- // packs. Optional so older backends — and older on-disk caches — that omit
23553
- // the field still parse; consumers read `bundle.rules ?? []`.
23554
- rules: external_exports.array(Rule).optional(),
23555
- // When true, `rules` IS the complete effective ruleset and the runtime must
23556
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23557
- // after reading the user's installed snapshot (installed_packs, enabled
23558
- // packs only), which is how detection updates stay manual: new bundled
23559
- // rules run only after the user applies the pack update. Absent/false keeps
23560
- // the historical composition (bundled packs + rules) — older caches.
23561
- rulesComplete: external_exports.boolean().optional(),
23562
- // Active detection exceptions, evaluation subset only (see
23563
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23564
- // on-disk caches — that omit the field still parse; consumers read
23565
- // `bundle.exceptions ?? []`.
23566
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23567
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23568
- // A second axis over the same `redact` action, carried beside the policies
23569
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23570
- // widening Policy itself would change a persisted shape to express something
23571
- // only the in-memory bundle needs. Optional so an older producer — or an
23572
- // older on-disk cache — still parses; consumers read `?? []` and get the
23573
- // pre-existing one-way behaviour, which is the safe direction to default.
23574
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23575
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23576
- // from a versioned installed pack. Optional so older backends — and older
23577
- // on-disk caches — that omit the field still parse; consumers fall back to
23578
- // the rule's own spec version. NOT the bundle version above — see
23579
- // installedRuleset's ruleVersions for the source of truth.
23580
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23581
- // Model ids (the raw `model` string a harness reports, e.g.
23582
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23583
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23584
- // one (UserPromptSubmit). Optional so an older backend — and an older
23585
- // on-disk cache — still parses; consumers read `?? []`, which is the
23586
- // unenforced behaviour that predates this field and the safe direction to
23587
- // default.
23588
- //
23589
- // Ids, not display names: the governance decision is keyed on the exact
23590
- // string the harness reports (`model_status_override.versionId` in the
23591
- // control plane), so no name resolution stands between the decision and the
23592
- // comparison.
23593
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23594
- customKeywords: external_exports.array(external_exports.string()),
23595
- fetchedAt: external_exports.iso.datetime()
23596
- }).meta({ id: "PolicyBundle" });
23597
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23598
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23599
- var CATEGORY_PEAK_SEVERITY = {
23600
- secret: "critical",
23601
- financial: "critical",
23602
- // core-financial/credit-card
23603
- code_flaw: "critical",
23604
- pii: "high",
23605
- phi: "high",
23606
- custom: "high",
23607
- // user-defined; conservative
23608
- code_context: "low",
23609
- config: "low"
23610
- // observe-only; floors to monitor regardless
23611
- };
23612
- function severityFloorPolicy(category) {
23613
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23614
- const peak = CATEGORY_PEAK_SEVERITY[category];
23615
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23616
- }
23617
- function severityFloorPosture() {
23618
- const out = {};
23619
- for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23620
- return out;
23621
- }
23622
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23623
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23624
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23625
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23626
- var BUILTIN_POLICY_SPECS = {
23627
- monitor: {
23628
- name: "Monitor",
23629
- action: "log",
23630
- reversible: false,
23631
- description: "Log every match for audit. The request is allowed through untouched."
23632
- },
23633
- warn: {
23634
- name: "Warn",
23635
- action: "warn",
23636
- reversible: false,
23637
- description: "Allow the request, but warn the user inline before it is sent."
23638
- },
23639
- redact: {
23640
- name: "Redact",
23641
- action: "redact",
23642
- reversible: false,
23643
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23644
- },
23645
- vault: {
23646
- name: "Redact & Vault",
23647
- action: "redact",
23648
- reversible: true,
23649
- 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."
23650
- },
23651
- block: {
23652
- name: "Block",
23653
- action: "block",
23654
- reversible: false,
23655
- description: "Refuse the request entirely whenever any rule in this detection matches."
23656
- }
23657
- };
23658
- function builtinPolicyToAction(id) {
23659
- return BUILTIN_POLICY_SPECS[id].action;
23660
- }
23661
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23662
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23663
- );
23664
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23665
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23666
- );
23667
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23668
- function builtinPolicyIsReversible(id) {
23669
- return BUILTIN_POLICY_SPECS[id].reversible;
23670
- }
23671
- function policyIdIsReversible(policyId) {
23672
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23673
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23674
- return builtinPolicyIsReversible(id);
23675
- }
23676
- var DEFAULT_ACTIONS = Object.fromEntries(
23677
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23678
- );
23679
- var BUILTIN_POLICIES = Object.fromEntries(
23680
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23681
- );
23682
- var DEFAULT_PACK_POLICY_ID = "monitor";
23683
- function policyIdToAction(policyId) {
23684
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23685
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23686
- return BUILTIN_POLICIES[id].action;
23687
- }
23688
- var UsedByItem = external_exports.object({
23689
- id: external_exports.string(),
23690
- name: external_exports.string(),
23691
- ruleCount: external_exports.number().int().nonnegative(),
23692
- enabled: external_exports.boolean()
23693
- }).meta({ id: "UsedByItem" });
23694
- var PolicyListItem = external_exports.object({
23695
- id: external_exports.string(),
23696
- kind: PolicyKind,
23697
- name: external_exports.string(),
23698
- enabled: external_exports.boolean(),
23699
- usedByCount: external_exports.number().int().nonnegative()
23700
- }).meta({ id: "PolicyListItem" });
23701
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23702
- var PolicyDetail = external_exports.object({
23703
- specVersion: external_exports.literal(1),
23704
- id: external_exports.string(),
23705
- kind: PolicyKind,
23706
- name: external_exports.string(),
23707
- enabled: external_exports.boolean(),
23708
- description: external_exports.string(),
23709
- usedBy: external_exports.array(UsedByItem)
23710
- }).meta({ id: "PolicyDetail" });
23711
- var PolicyStatsResponse = external_exports.object({
23712
- policies: external_exports.number().int().nonnegative(),
23713
- builtin: external_exports.number().int().nonnegative(),
23714
- custom: external_exports.number().int().nonnegative(),
23715
- detectionsGoverned: external_exports.number().int().nonnegative()
23716
- }).meta({ id: "PolicyStatsResponse" });
23717
-
23718
23865
  // ../../packages/schema/src/zod/project-files.ts
23719
23866
  var ProjectFileInput = external_exports.object({
23720
23867
  path: external_exports.string().min(1),
@@ -23960,10 +24107,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23960
24107
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23961
24108
 
23962
24109
  // ../../packages/schema/src/zod/settings-action.ts
24110
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24111
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23963
24112
  var SaveSettingsInput = external_exports.object({
23964
24113
  historicalAccess: external_exports.string(),
23965
- modelJudgeConsent: external_exports.boolean(),
23966
- historySyncConsent: external_exports.boolean(),
24114
+ modelJudgeConsent: ModelJudgeConsentChoice,
24115
+ historySyncConsent: HistorySyncConsentChoice,
23967
24116
  vaultConsent: external_exports.string(),
23968
24117
  vaultInlineReveal: external_exports.string()
23969
24118
  });
@@ -24113,9 +24262,9 @@ function deriveReviewReasons(trust, transports) {
24113
24262
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24114
24263
  return reasons;
24115
24264
  }
24116
- function buildReviewInfo(trust, transports) {
24265
+ function buildReviewInfo(trust, transports, decided) {
24117
24266
  const reasons = deriveReviewReasons(trust, transports);
24118
- return { needsReview: reasons.length > 0, reasons };
24267
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24119
24268
  }
24120
24269
  function distinctTransports(transports) {
24121
24270
  return Array.from(new Set(transports));
@@ -24186,8 +24335,8 @@ function tightenPerms(file2) {
24186
24335
  }
24187
24336
 
24188
24337
  // ../../packages/persistence/src/database.ts
24189
- import { randomUUID as randomUUID10 } from "crypto";
24190
- import { join as join4, sep } from "path";
24338
+ import { randomUUID as randomUUID11 } from "crypto";
24339
+ import { dirname as dirname2, join as join7, sep } from "path";
24191
24340
  import { DatabaseSync } from "node:sqlite";
24192
24341
 
24193
24342
  // ../../packages/persistence/src/ids.ts
@@ -24431,6 +24580,10 @@ function allRows(stmt, params) {
24431
24580
  if (Array.isArray(params)) return stmt.all(...params);
24432
24581
  return stmt.all(params);
24433
24582
  }
24583
+ function* iterateRows(stmt, params) {
24584
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24585
+ for (const row of rows) yield row;
24586
+ }
24434
24587
  function getRow(stmt, params) {
24435
24588
  if (params === void 0) return stmt.get();
24436
24589
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24899,10 +25052,17 @@ function ensureSyncedAtColumn(db, table2) {
24899
25052
  if (!columns.includes("sync_claimed_at")) {
24900
25053
  db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
24901
25054
  }
25055
+ if (!columns.includes("outbox_owed")) {
25056
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25057
+ }
24902
25058
  db.exec(
24903
25059
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24904
25060
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24905
25061
  );
25062
+ db.exec(
25063
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25064
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25065
+ );
24906
25066
  db.exec(
24907
25067
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24908
25068
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25007,7 +25167,6 @@ function decodeKeysetCursor(cursor) {
25007
25167
  // ../../packages/persistence/src/repositories/activity.ts
25008
25168
  var DAY_MS = 864e5;
25009
25169
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25010
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25011
25170
  function defaultTimeZone() {
25012
25171
  try {
25013
25172
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25062,6 +25221,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25062
25221
  error: "error",
25063
25222
  active: "active"
25064
25223
  };
25224
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25065
25225
  function safeParseStringArray(raw) {
25066
25226
  if (!raw) return [];
25067
25227
  const parsed = safeJson(raw, null);
@@ -25135,6 +25295,37 @@ var TIMELINE_COLUMNS = `
25135
25295
  json_extract(attributes, '$.targetId') AS target_id,
25136
25296
  json_extract(attributes, '$.internal') AS internal,
25137
25297
  json_extract(attributes, '$.flagged') AS flagged`;
25298
+ var LLM_USAGE_SELECT = `
25299
+ SELECT root_session_id AS sessionId,
25300
+ provider,
25301
+ model,
25302
+ service_tier AS serviceTier,
25303
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25304
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25305
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25306
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25307
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25308
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25309
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25310
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25311
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25312
+ function usageLeaves(rows) {
25313
+ return rows.map((row) => {
25314
+ const attributes = {
25315
+ input_tokens: row.inputTokens,
25316
+ output_tokens: row.outputTokens,
25317
+ cache_creation_input_tokens: row.cacheCreationTokens,
25318
+ cache_read_input_tokens: row.cacheReadTokens,
25319
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25320
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25321
+ web_search_requests: row.webSearchRequests
25322
+ };
25323
+ if (row.provider !== null) attributes.provider = row.provider;
25324
+ if (row.model !== null) attributes.model = row.model;
25325
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25326
+ return { sessionId: row.sessionId, attributes };
25327
+ });
25328
+ }
25138
25329
  var SESSION_ROOT = `event_type = 'session'`;
25139
25330
  var HAS_ACTIVITY = `EXISTS (
25140
25331
  SELECT 1 FROM audit_events c
@@ -25160,16 +25351,17 @@ var SqliteActivityRepository = class {
25160
25351
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25161
25352
  const liveNow = countScalar(
25162
25353
  this.db,
25163
- `SELECT count(*) AS n FROM audit_events s
25354
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25164
25355
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25165
- AND max(
25166
- s.started_at,
25167
- coalesce(
25168
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25169
- s.started_at
25170
- )
25171
- ) >= ?`,
25172
- [liveThreshold]
25356
+ AND s.id IN (
25357
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25358
+ UNION
25359
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25360
+ WHERE started_at >= ?
25361
+ UNION
25362
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25363
+ WHERE ended_at >= ?)`,
25364
+ [liveThreshold, liveThreshold, liveThreshold]
25173
25365
  );
25174
25366
  const toolCallsToday = countScalar(
25175
25367
  this.db,
@@ -25299,7 +25491,7 @@ var SqliteActivityRepository = class {
25299
25491
  this.db.prepare(
25300
25492
  `SELECT ${TIMELINE_COLUMNS}
25301
25493
  FROM audit_events
25302
- WHERE id = ? OR root_session_id = ?
25494
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25303
25495
  ORDER BY started_at ASC, id ASC`
25304
25496
  ),
25305
25497
  [sessionId, sessionId]
@@ -25312,14 +25504,14 @@ var SqliteActivityRepository = class {
25312
25504
  coalesce(sum(output_tokens), 0) AS output,
25313
25505
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25314
25506
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25315
- FROM audit_events
25507
+ FROM audit_events INDEXED BY idx_audit_session_type
25316
25508
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25317
25509
  ),
25318
25510
  [sessionId]
25319
25511
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25320
25512
  const primaryModel = getRow(
25321
25513
  this.db.prepare(
25322
- `SELECT model, provider FROM audit_events
25514
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25323
25515
  WHERE root_session_id = ? AND event_type = 'llm_call'
25324
25516
  ORDER BY started_at ASC, id ASC
25325
25517
  LIMIT 1`
@@ -25330,7 +25522,7 @@ var SqliteActivityRepository = class {
25330
25522
  this.db.prepare(
25331
25523
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25332
25524
  count(*) AS n
25333
- FROM audit_events
25525
+ FROM audit_events INDEXED BY idx_audit_session
25334
25526
  WHERE root_session_id = ? AND event_type = 'tool_call'
25335
25527
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25336
25528
  ),
@@ -25338,7 +25530,7 @@ var SqliteActivityRepository = class {
25338
25530
  );
25339
25531
  const modelRows = allRows(
25340
25532
  this.db.prepare(
25341
- `SELECT DISTINCT model FROM audit_events
25533
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25342
25534
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25343
25535
  ORDER BY model`
25344
25536
  ),
@@ -25347,7 +25539,7 @@ var SqliteActivityRepository = class {
25347
25539
  const derivedModels = modelRows.map((r) => r.model);
25348
25540
  const commits = countScalar(
25349
25541
  this.db,
25350
- `SELECT count(*) AS n FROM audit_events
25542
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25351
25543
  WHERE root_session_id = ? AND event_type = 'commit'`,
25352
25544
  [sessionId]
25353
25545
  );
@@ -25383,25 +25575,57 @@ var SqliteActivityRepository = class {
25383
25575
  return Promise.resolve(session);
25384
25576
  }
25385
25577
  /**
25386
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25387
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25388
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25389
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25390
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25391
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25578
+ * Cross-session token report — every `llm_call` in the store (or in a
25579
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25580
+ * session, with USD cost DERIVED at read time via the shared
25581
+ * `defaultCostModel` (never stored). The caller collapses these onto
25582
+ * per-model rows with `aggregateTokenUsage`.
25583
+ *
25584
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25585
+ * the members the rollup sums — and priced once per group, which is exact
25586
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25587
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25588
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25589
+ * the index stores the values once, at write, and answers the same window in
25590
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25591
+ * planner prefers the general event-type index and fetches every row to
25592
+ * recompute the columns it could have read. The index is one every open
25593
+ * store carries, since opening runs the migrations, so the hard requirement
25594
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25595
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25596
+ * per call, no bag parsed.
25392
25597
  */
25393
25598
  tokenReports(fromMs) {
25394
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25395
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25599
+ const rows = allRows(
25600
+ this.db.prepare(
25601
+ `${LLM_USAGE_SELECT}
25602
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25603
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25604
+ ${LLM_USAGE_GROUP}`
25605
+ ),
25606
+ fromMs === void 0 ? void 0 : [fromMs]
25607
+ );
25608
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25396
25609
  }
25397
25610
  /**
25398
- * One session's token report — its `llm_call` leaves grouped per (provider,
25399
- * model) with derived cost, or `null` when the session made no `llm_call`s
25400
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25401
- * breakdown + estimated cost.
25611
+ * One session's token report — its `llm_call`s grouped per (provider,
25612
+ * model, tier) with derived cost, or `null` when the session made no
25613
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25614
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25615
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25616
+ * it replaces walked every `llm_call` in the store to find one session's.
25402
25617
  */
25403
25618
  tokenReportForSession(sessionId) {
25404
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25619
+ const rows = allRows(
25620
+ this.db.prepare(
25621
+ `${LLM_USAGE_SELECT}
25622
+ FROM audit_events
25623
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25624
+ ${LLM_USAGE_GROUP}`
25625
+ ),
25626
+ [sessionId]
25627
+ );
25628
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25405
25629
  return Promise.resolve(reports[0] ?? null);
25406
25630
  }
25407
25631
  /**
@@ -25425,42 +25649,6 @@ var SqliteActivityRepository = class {
25425
25649
  for (const row of rows) seen.add(toHarness(row.harness));
25426
25650
  return Promise.resolve([...seen]);
25427
25651
  }
25428
- /**
25429
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25430
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25431
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25432
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25433
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25434
- */
25435
- readLlmCallLeaves(opts = {}) {
25436
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25437
- const params = [];
25438
- if (opts.sessionId !== void 0) {
25439
- conditions.push("root_session_id = ?");
25440
- params.push(opts.sessionId);
25441
- }
25442
- if (opts.fromMs !== void 0) {
25443
- conditions.push("started_at >= ?");
25444
- params.push(opts.fromMs);
25445
- }
25446
- const rows = allRows(
25447
- this.db.prepare(
25448
- `SELECT root_session_id AS sessionId, attributes
25449
- FROM audit_events
25450
- WHERE ${conditions.join(" AND ")}`
25451
- ),
25452
- params
25453
- );
25454
- return mapRowsTolerant(
25455
- rows.filter(
25456
- (row) => row.sessionId !== null
25457
- ),
25458
- (row) => ({
25459
- sessionId: row.sessionId,
25460
- attributes: JSON.parse(row.attributes)
25461
- })
25462
- );
25463
- }
25464
25652
  /**
25465
25653
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25466
25654
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25475,20 +25663,23 @@ var SqliteActivityRepository = class {
25475
25663
  const inClause = placeholders(sessionIds.length);
25476
25664
  const lastActivityRows = allRows(
25477
25665
  this.db.prepare(
25478
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25479
- WHERE root_session_id IN (${inClause})
25480
- GROUP BY root_session_id`
25666
+ `SELECT ids.value AS id,
25667
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25668
+ (SELECT max(ended_at) FROM audit_events e
25669
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25670
+ FROM json_each(?) AS ids`
25481
25671
  ),
25482
- sessionIds
25672
+ [JSON.stringify(sessionIds)]
25483
25673
  );
25484
25674
  for (const row of lastActivityRows) {
25485
- if (row.id === null) continue;
25486
25675
  const entry = result.get(row.id);
25487
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25676
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25677
+ if (entry && last > 0) entry.lastActivityMs = last;
25488
25678
  }
25489
25679
  const turnsRows = allRows(
25490
25680
  this.db.prepare(
25491
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25681
+ `SELECT root_session_id AS id, count(*) AS n
25682
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25492
25683
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25493
25684
  GROUP BY root_session_id`
25494
25685
  ),
@@ -25503,7 +25694,7 @@ var SqliteActivityRepository = class {
25503
25694
  this.db.prepare(
25504
25695
  `SELECT root_session_id AS id,
25505
25696
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25506
- FROM audit_events
25697
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25507
25698
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25508
25699
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25509
25700
  GROUP BY root_session_id`
@@ -25533,7 +25724,7 @@ var SqliteActivityRepository = class {
25533
25724
  this.db.prepare(
25534
25725
  `SELECT root_session_id AS id,
25535
25726
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25536
- FROM audit_events
25727
+ FROM audit_events INDEXED BY idx_audit_session_share
25537
25728
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25538
25729
  GROUP BY root_session_id`
25539
25730
  ),
@@ -26562,7 +26753,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26562
26753
 
26563
26754
  // ../../packages/persistence/src/repositories/findings.ts
26564
26755
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26565
- var SCAN_BATCH_ROWS = 1e3;
26566
26756
  var DEFAULT_LOCATIONS_LIMIT = 100;
26567
26757
  var LOCATION_RULE_IDS_CAP = 20;
26568
26758
  function compareLocationOrder(a, b) {
@@ -26591,6 +26781,25 @@ function deriveInstanceStatus(row) {
26591
26781
  latestResolutionStatus: row.latest_status
26592
26782
  });
26593
26783
  }
26784
+ function toFlatFindingRow(r) {
26785
+ return {
26786
+ id: r.id,
26787
+ ruleId: r.rule_id,
26788
+ category: r.category,
26789
+ severity: r.severity,
26790
+ maskedMatch: r.masked_match,
26791
+ actionTaken: r.action_taken,
26792
+ confidence: r.confidence,
26793
+ occurredAt: epochMillisToIso(r.occurred_at),
26794
+ sourceTool: r.source_tool,
26795
+ repo: r.repo ?? "",
26796
+ file: r.file ?? "",
26797
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26798
+ eventId: r.event_id,
26799
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26800
+ status: deriveInstanceStatus(r)
26801
+ };
26802
+ }
26594
26803
  function encodeGroupCursor(group) {
26595
26804
  const payload = {
26596
26805
  sev: group.severity,
@@ -26666,7 +26875,7 @@ var SqliteFindingsRepository = class {
26666
26875
  this.db.prepare(
26667
26876
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26668
26877
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26669
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26878
+ e.source_tool AS source_tool,
26670
26879
  e.event_type AS kind
26671
26880
  FROM audit_events e
26672
26881
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26774,56 +26983,11 @@ var SqliteFindingsRepository = class {
26774
26983
  predicate,
26775
26984
  params: sessionParams
26776
26985
  });
26777
- const rows = allRows(
26778
- this.db.prepare(
26779
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26780
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26781
- kind, finding_key, latest_status
26782
- FROM (
26783
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26784
- d.severity AS severity, f.masked_match AS masked_match,
26785
- f.action_taken AS action_taken, f.confidence AS confidence,
26786
- e.started_at AS occurred_at,
26787
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26788
- json_extract(e.attributes, '$.repo') AS repo,
26789
- json_extract(e.attributes, '$.file_path') AS file,
26790
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26791
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26792
- e.event_type AS kind, f.finding_key AS finding_key,
26793
- latest.status AS latest_status,
26794
- ROW_NUMBER() OVER (
26795
- PARTITION BY d.rule_id
26796
- ORDER BY e.started_at DESC, f.id DESC
26797
- ) AS rn
26798
- FROM inspection_findings f
26799
- JOIN audit_events e ON e.id = f.audit_event_id
26800
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26801
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26802
- ON latest.finding_key = f.finding_key
26803
- ${predicate}
26804
- )
26805
- WHERE rn <= :cap
26806
- ORDER BY occurred_at DESC, id DESC`
26807
- ),
26808
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26809
- );
26810
- const groupable = rows.map((r) => ({
26811
- id: r.id,
26812
- ruleId: r.rule_id,
26813
- category: r.category,
26814
- severity: r.severity,
26815
- maskedMatch: r.masked_match,
26816
- actionTaken: r.action_taken,
26817
- confidence: r.confidence,
26818
- occurredAt: epochMillisToIso(r.occurred_at),
26819
- sourceTool: r.source_tool,
26820
- repo: r.repo ?? "",
26821
- file: r.file ?? "",
26822
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26823
- eventId: r.event_id,
26824
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26825
- status: deriveInstanceStatus(r)
26826
- }));
26986
+ const rows = this.previewRows(aggregates, {
26987
+ sessionId: query.sessionId,
26988
+ from: query.from
26989
+ });
26990
+ const groupable = rows.map(toFlatFindingRow);
26827
26991
  const allGroups = buildFindingGroups(groupable, { aggregates });
26828
26992
  const filterOpts = {
26829
26993
  severity: query.severity,
@@ -26909,8 +27073,10 @@ var SqliteFindingsRepository = class {
26909
27073
  *
26910
27074
  * The scan runs from the top of the scope on every request, not from the
26911
27075
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26912
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26913
- * while the counting runs, and only the page itself is retained.
27076
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27077
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27078
+ * counting runs — a generator streaming the index order, not a sequence of
27079
+ * fetched batches; only the page itself is retained.
26914
27080
  */
26915
27081
  listFindingInstances(query) {
26916
27082
  const opts = {
@@ -26926,6 +27092,10 @@ var SqliteFindingsRepository = class {
26926
27092
  };
26927
27093
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
26928
27094
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27095
+ const isPastCursor = cursor === null ? () => true : (row) => {
27096
+ const rowMs = isoToEpochMillis(row.occurredAt);
27097
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27098
+ };
26929
27099
  const accumulator = createInstanceFacetAccumulator(opts);
26930
27100
  const items = [];
26931
27101
  let total = 0;
@@ -26938,6 +27108,7 @@ var SqliteFindingsRepository = class {
26938
27108
  accumulator.add(row);
26939
27109
  if (!matchesInstanceFilters(row, opts)) continue;
26940
27110
  total += 1;
27111
+ if (!isPastCursor(row)) continue;
26941
27112
  if (items.length < limit) {
26942
27113
  items.push(toInstanceDetail(row));
26943
27114
  last = row;
@@ -26946,15 +27117,6 @@ var SqliteFindingsRepository = class {
26946
27117
  }
26947
27118
  }
26948
27119
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
26949
- if (cursor !== null) {
26950
- const resumed = this.pageAfter(cursor, opts, limit, query);
26951
- return Promise.resolve({
26952
- totals: { findings: total },
26953
- facets: accumulator.facets(),
26954
- items: resumed.items,
26955
- nextCursor: resumed.nextCursor
26956
- });
26957
- }
26958
27120
  return Promise.resolve({
26959
27121
  totals: { findings: total },
26960
27122
  facets: accumulator.facets(),
@@ -26962,35 +27124,6 @@ var SqliteFindingsRepository = class {
26962
27124
  nextCursor
26963
27125
  });
26964
27126
  }
26965
- /**
26966
- * The page of matching rows strictly after `cursor`. Separate from the
26967
- * counting pass because that one starts at the top of the scope by design;
26968
- * this one narrows the scan with the same keyset predicate the activity list
26969
- * uses, so a later page costs less than the first rather than more.
26970
- */
26971
- pageAfter(cursor, opts, limit, query) {
26972
- const items = [];
26973
- let last;
26974
- let hasMore = false;
26975
- for (const row of this.scanFindingRows({
26976
- sessionId: query.sessionId,
26977
- from: query.from,
26978
- after: cursor
26979
- })) {
26980
- if (!matchesInstanceFilters(row, opts)) continue;
26981
- if (items.length < limit) {
26982
- items.push(toInstanceDetail(row));
26983
- last = row;
26984
- } else {
26985
- hasMore = true;
26986
- break;
26987
- }
26988
- }
26989
- return {
26990
- items,
26991
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
26992
- };
26993
- }
26994
27127
  /**
26995
27128
  * The same findings folded by location: repository, then file within it.
26996
27129
  *
@@ -27073,25 +27206,111 @@ var SqliteFindingsRepository = class {
27073
27206
  });
27074
27207
  }
27075
27208
  /**
27076
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27209
+ * Each group's newest instances, for the table's expanded rows.
27210
+ *
27211
+ * ONE index-ordered scan with early termination, and the shape is the point.
27212
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27213
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27214
+ * through a temp B-tree to keep a bounded preview of each group, and then
27215
+ * sorts the survivors again for the page order. Both sorts grow with the
27216
+ * store while the answer does not.
27217
+ *
27218
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27219
+ * (or the session or window index the scope names — see `findingScanSql`),
27220
+ * which is already the order the page wants, and keeps rows per rule until
27221
+ * each rule has as many as it can show. The aggregate the caller already holds
27222
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27223
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27224
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27225
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27226
+ * store with many firing rules widens it. The bound that DOES hold
27227
+ * unconditionally is the sorted form's floor: this scan visits at most as
27228
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27229
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27230
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27231
+ * wanted instances sitting at the tail of the scope — is one pass over
27232
+ * everything in scope with a block sort of the id tie-break only, never a
27233
+ * sort of the scope, which is still that floor.
27234
+ *
27235
+ * A row whose rule the aggregate did not see is skipped: the two statements
27236
+ * run without a shared snapshot, so a capture landing between them can add a
27237
+ * rule here that has no counts there, and the counts are what the group is
27238
+ * built from.
27239
+ */
27240
+ previewRows(aggregates, scope) {
27241
+ const wanted = /* @__PURE__ */ new Map();
27242
+ let remaining = 0;
27243
+ for (const [ruleId, agg] of aggregates) {
27244
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27245
+ wanted.set(ruleId, n);
27246
+ remaining += n;
27247
+ }
27248
+ const rows = [];
27249
+ if (remaining === 0) return rows;
27250
+ const { sql, params } = this.findingScanSql(scope);
27251
+ const taken = /* @__PURE__ */ new Map();
27252
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27253
+ const want = wanted.get(r.rule_id);
27254
+ if (want === void 0) continue;
27255
+ const have = taken.get(r.rule_id) ?? 0;
27256
+ if (have >= want) continue;
27257
+ taken.set(r.rule_id, have + 1);
27258
+ rows.push(r);
27259
+ remaining -= 1;
27260
+ if (remaining === 0) break;
27261
+ }
27262
+ return rows;
27263
+ }
27264
+ /**
27265
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27077
27266
  *
27078
27267
  * A generator so a caller streams the scope without it ever being an array:
27079
27268
  * the flat list counts and facets the whole filtered scope, which on a large
27080
- * store is far more rows than any page. Each batch advances the same keyset
27081
- * predicate the page read uses, so the scan is a sequence of bounded reads
27082
- * rather than one unbounded result set.
27269
+ * store is far more rows than any page. The rows come off ONE statement,
27270
+ * iterated rather than materialized, in the index order `findingScanSql`
27271
+ * arranges so the scan is a single pass with a block sort of the id
27272
+ * tie-break only, never a sort of the scope, where a sequence of
27273
+ * keyset-bounded batches re-sorted everything below the cursor on every
27274
+ * batch and cost the square of the scope.
27083
27275
  *
27084
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27085
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27086
- * makes it a point lookup per row, and the derived table would re-materialize
27087
- * a window over the whole resolution table once per batch.
27088
- *
27089
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27090
- * would be missing from its own facet, which is computed by excluding that
27091
- * dimension — see listFindingInstances.
27276
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27277
+ * dimension narrowed here would be missing from its own facet, which is
27278
+ * computed by excluding that dimension (see listFindingInstances). There is
27279
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27280
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27281
+ * narrower statement, since the counting pass already visits every row a
27282
+ * page-2+ request would otherwise re-seek for.
27092
27283
  */
27093
27284
  *scanFindingRows(scope) {
27094
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27285
+ const { sql, params } = this.findingScanSql(scope);
27286
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27287
+ yield toFlatFindingRow(r);
27288
+ }
27289
+ }
27290
+ /**
27291
+ * The one statement both instance-level scans run: every finding in scope,
27292
+ * joined to its event and definition, newest first.
27293
+ *
27294
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27295
+ * the same two `recentFindings` documents at length, for the same reason:
27296
+ *
27297
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27298
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27299
+ * yields `started_at` order per event type, not across the four, so
27300
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27301
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27302
+ * `idx_audit_session` for a session scope, which is also `started_at`
27303
+ * ordered within the session — and the order falls out of the index.
27304
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27305
+ * JOINs the planner drives from the findings and sorts everything.
27306
+ *
27307
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27308
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27309
+ * index probe per keyed row, and a derived table over the whole resolution
27310
+ * table would be materialized before the first row streamed.
27311
+ */
27312
+ findingScanSql(scope) {
27313
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27095
27314
  const params = [];
27096
27315
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27097
27316
  conditions.push("e.root_session_id = ?");
@@ -27105,58 +27324,24 @@ var SqliteFindingsRepository = class {
27105
27324
  d.severity AS severity, f.masked_match AS masked_match,
27106
27325
  f.action_taken AS action_taken, f.confidence AS confidence,
27107
27326
  e.started_at AS occurred_at,
27108
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27109
- json_extract(e.attributes, '$.repo') AS repo,
27110
- json_extract(e.attributes, '$.file_path') AS file,
27111
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27327
+ e.source_tool AS source_tool,
27328
+ e.repo AS repo,
27329
+ e.file_path AS file,
27330
+ e.tool_name AS tool_name,
27112
27331
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27113
27332
  e.event_type AS kind, f.finding_key AS finding_key,
27114
27333
  ${latestResolutionStatusSql("f")} AS latest_status
27115
- FROM inspection_findings f
27116
- JOIN audit_events e ON e.id = f.audit_event_id
27117
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27334
+ FROM audit_events e
27335
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27336
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27118
27337
  WHERE ${conditions.join(" AND ")}
27119
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27120
- ORDER BY e.started_at DESC, f.id DESC
27121
- LIMIT ?`;
27122
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27123
- for (; ; ) {
27124
- const rows = allRows(this.db.prepare(sql), [
27125
- ...params,
27126
- after.startedAtMs,
27127
- after.startedAtMs,
27128
- after.id,
27129
- SCAN_BATCH_ROWS
27130
- ]);
27131
- for (const r of rows) {
27132
- yield {
27133
- id: r.id,
27134
- ruleId: r.rule_id,
27135
- category: r.category,
27136
- severity: r.severity,
27137
- maskedMatch: r.masked_match,
27138
- actionTaken: r.action_taken,
27139
- confidence: r.confidence,
27140
- occurredAt: epochMillisToIso(r.occurred_at),
27141
- sourceTool: r.source_tool,
27142
- repo: r.repo ?? "",
27143
- file: r.file ?? "",
27144
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27145
- eventId: r.event_id,
27146
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27147
- status: deriveInstanceStatus(r)
27148
- };
27149
- }
27150
- if (rows.length < SCAN_BATCH_ROWS) return;
27151
- const lastRow = rows[rows.length - 1];
27152
- if (lastRow === void 0) return;
27153
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27154
- }
27338
+ ORDER BY e.started_at DESC, f.id DESC`;
27339
+ return { sql, params };
27155
27340
  }
27156
27341
  groupAggregates(withSearchText, scope) {
27157
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27158
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27159
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27342
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27343
+ group_concat(DISTINCT e.file_path) AS files,
27344
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27160
27345
  const rows = this.db.prepare(
27161
27346
  `SELECT rule_id,
27162
27347
  sum(tuple_count) AS instance_count,
@@ -27174,7 +27359,7 @@ var SqliteFindingsRepository = class {
27174
27359
  coalesce(latest.status, '') AS status_tuple,
27175
27360
  count(*) AS tuple_count,
27176
27361
  max(e.started_at) AS latest_at,
27177
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27362
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27178
27363
  group_concat(DISTINCT f.action_taken) AS actions_taken
27179
27364
  ${innerSearchColumns}
27180
27365
  FROM inspection_findings f
@@ -27305,6 +27490,8 @@ function isoDay(ms) {
27305
27490
  // ../../packages/persistence/src/repositories/history-sync.ts
27306
27491
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27307
27492
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27493
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27494
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27308
27495
  var SKIPPED = -1;
27309
27496
  var ROW_COLUMNS = `id,
27310
27497
  parent_id AS parentId,
@@ -27344,6 +27531,20 @@ var SqliteHistorySyncRepository = class {
27344
27531
  ORDER BY (event_type = 'session') DESC, started_at
27345
27532
  LIMIT :limit`
27346
27533
  );
27534
+ this.captureRowsStmt = db.prepare(
27535
+ `SELECT ${ROW_COLUMNS}
27536
+ FROM audit_events
27537
+ WHERE synced_at IS NULL
27538
+ AND sync_claimed_at IS NULL
27539
+ AND outbox_owed = 1
27540
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27541
+ AND started_at < :before
27542
+ ORDER BY started_at
27543
+ LIMIT :limit`
27544
+ );
27545
+ this.markOwedStmt = db.prepare(
27546
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27547
+ );
27347
27548
  this.stampStmt = db.prepare(
27348
27549
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27349
27550
  );
@@ -27375,6 +27576,12 @@ var SqliteHistorySyncRepository = class {
27375
27576
  FROM audit_events
27376
27577
  WHERE event_type IN (${TYPE_LIST})`
27377
27578
  );
27579
+ this.captureSkipCountStmt = db.prepare(
27580
+ `SELECT COUNT(*) AS skipped
27581
+ FROM audit_events
27582
+ WHERE synced_at = ${String(SKIPPED)}
27583
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27584
+ );
27378
27585
  this.fingerprintStmt = db.prepare(
27379
27586
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27380
27587
  FROM history_sync WHERE id = 1`
@@ -27384,6 +27591,10 @@ var SqliteHistorySyncRepository = class {
27384
27591
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27385
27592
  WHERE id = 1`
27386
27593
  );
27594
+ this.disownCapturesStmt = db.prepare(
27595
+ `UPDATE audit_events SET outbox_owed = NULL
27596
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27597
+ );
27387
27598
  this.rearmStmt = db.prepare(
27388
27599
  `UPDATE audit_events SET synced_at = NULL
27389
27600
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27456,6 +27667,10 @@ var SqliteHistorySyncRepository = class {
27456
27667
  closeWindowStmt;
27457
27668
  releaseBoundaryStmt;
27458
27669
  freezeBoundaryStmt;
27670
+ captureRowsStmt;
27671
+ markOwedStmt;
27672
+ captureSkipCountStmt;
27673
+ disownCapturesStmt;
27459
27674
  partitionStmt;
27460
27675
  claimRowStmt;
27461
27676
  releaseRowStmt;
@@ -27489,6 +27704,34 @@ var SqliteHistorySyncRepository = class {
27489
27704
  pendingRows(sessionId, limit, before) {
27490
27705
  return allRows(this.rowsStmt, { sessionId, limit, before });
27491
27706
  }
27707
+ /**
27708
+ * Captures this machine still owes the deployment, oldest first.
27709
+ *
27710
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27711
+ * by a time window — see captureRowsStmt for why a window could not express
27712
+ * this. `before` is the grace window that leaves a just-recorded capture to
27713
+ * the live path.
27714
+ */
27715
+ pendingCaptureRows(limit, before) {
27716
+ return allRows(this.captureRowsStmt, { limit, before });
27717
+ }
27718
+ /**
27719
+ * Record that a capture is OWED to the deployment.
27720
+ *
27721
+ * Written by the attached forward path when a live send did not confirm
27722
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27723
+ * a fact rather than an inference: the machine was attached, the send did not
27724
+ * land, so the row is owed — which no time window can state, because the same
27725
+ * window that holds the rows a past attachment left owed also holds every
27726
+ * capture recorded while the machine was DETACHED, and those were never
27727
+ * offered to anyone.
27728
+ *
27729
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27730
+ * out of the drain's read.
27731
+ */
27732
+ markCaptureOwed(id) {
27733
+ this.markOwedStmt.run({ id });
27734
+ }
27492
27735
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27493
27736
  markSynced(ids, atMs) {
27494
27737
  this.stampAll(ids, atMs);
@@ -27572,10 +27815,12 @@ var SqliteHistorySyncRepository = class {
27572
27815
  this.countsStmt,
27573
27816
  { before }
27574
27817
  );
27818
+ const captures = getRow(this.captureSkipCountStmt);
27575
27819
  return {
27576
27820
  pending: row?.pending ?? 0,
27577
27821
  sent: row?.sent ?? 0,
27578
- skipped: row?.skipped ?? 0
27822
+ skipped: row?.skipped ?? 0,
27823
+ capturesSkipped: captures?.skipped ?? 0
27579
27824
  };
27580
27825
  }
27581
27826
  /**
@@ -27616,7 +27861,11 @@ var SqliteHistorySyncRepository = class {
27616
27861
  withTransaction(
27617
27862
  this.db,
27618
27863
  () => {
27864
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27619
27865
  this.rearmStmt.run();
27866
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27867
+ this.disownCapturesStmt.run();
27868
+ }
27620
27869
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27621
27870
  },
27622
27871
  "IMMEDIATE"
@@ -27813,7 +28062,252 @@ var SqliteInspectionFindingsRepository = class {
27813
28062
  };
27814
28063
 
27815
28064
  // ../../packages/persistence/src/repositories/installed-packs.ts
27816
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28065
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28066
+
28067
+ // ../../packages/persistence/src/policy-floor.ts
28068
+ import { readFileSync as readFileSync5 } from "fs";
28069
+ import { join as join6 } from "path";
28070
+
28071
+ // ../../packages/persistence/src/local-layout.ts
28072
+ import { renameSync as renameSync3 } from "fs";
28073
+ import { mkdir } from "fs/promises";
28074
+ import { homedir } from "os";
28075
+ import { join as join4 } from "path";
28076
+ function defaultDataDir() {
28077
+ return join4(homedir(), ".aka");
28078
+ }
28079
+ function settingsDir(base = defaultDataDir()) {
28080
+ return join4(base, "settings");
28081
+ }
28082
+ function dataDir(base = defaultDataDir()) {
28083
+ return join4(base, "data");
28084
+ }
28085
+ function dbPath(base = defaultDataDir()) {
28086
+ return join4(dataDir(base), "aka.db");
28087
+ }
28088
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28089
+ ensureDataDirSync(dir);
28090
+ }
28091
+ function migrateLegacyLayout(base = defaultDataDir()) {
28092
+ const moves = [
28093
+ { name: "config.json", dest: settingsDir(base) },
28094
+ { name: "policy-cache.json", dest: dataDir(base) }
28095
+ ];
28096
+ for (const { name, dest } of moves) {
28097
+ try {
28098
+ ensureDataDirSync(dest);
28099
+ const moved = join4(dest, name);
28100
+ renameSync3(join4(base, name), moved);
28101
+ tightenFile(moved);
28102
+ } catch {
28103
+ }
28104
+ }
28105
+ }
28106
+
28107
+ // ../../packages/persistence/src/settings.ts
28108
+ import { readFileSync as readFileSync4 } from "fs";
28109
+ import { join as join5 } from "path";
28110
+
28111
+ // ../../packages/persistence/src/file-lock.ts
28112
+ import { randomUUID as randomUUID3 } from "crypto";
28113
+ import {
28114
+ closeSync,
28115
+ existsSync as existsSync2,
28116
+ openSync,
28117
+ readFileSync as readFileSync2,
28118
+ rmSync as rmSync5,
28119
+ statSync as statSync3,
28120
+ writeFileSync as writeFileSync2
28121
+ } from "fs";
28122
+ import { hostname as hostname3 } from "os";
28123
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28124
+
28125
+ // ../../packages/persistence/src/managed-settings.ts
28126
+ import { readFileSync as readFileSync3 } from "fs";
28127
+ import { posix, win32 } from "path";
28128
+ function managedSettingsPaths(platform2 = process.platform) {
28129
+ if (platform2 === "darwin") {
28130
+ return [
28131
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28132
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28133
+ ];
28134
+ }
28135
+ if (platform2 === "win32") {
28136
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28137
+ }
28138
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28139
+ }
28140
+ function readManagedSettings(paths = managedSettingsPaths()) {
28141
+ for (const path of paths) {
28142
+ let text;
28143
+ try {
28144
+ text = readFileSync3(path, "utf8");
28145
+ } catch {
28146
+ continue;
28147
+ }
28148
+ const record2 = parseJsonObject(text);
28149
+ if (!record2) continue;
28150
+ const parsed = ManagedSettings.safeParse(record2);
28151
+ if (parsed.success) return parsed.data;
28152
+ }
28153
+ return null;
28154
+ }
28155
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28156
+ if (!managed) return settings;
28157
+ const { values } = managed;
28158
+ const merged = { ...settings };
28159
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28160
+ if (values.controlPlane !== void 0) {
28161
+ merged.controlPlane = {
28162
+ ...values.controlPlane,
28163
+ // The administrator pinned WHICH deployment, not WHEN this machine
28164
+ // joined it. Keep the user's own attach time when the endpoint is
28165
+ // unchanged, so a managed machine does not appear to re-attach on every
28166
+ // read; stamp a fresh one when the administrator moved it.
28167
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28168
+ };
28169
+ }
28170
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28171
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28172
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28173
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28174
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28175
+ if (values.vaultConsent !== void 0) {
28176
+ merged.vaultConsent = values.vaultConsent ? (
28177
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28178
+ // at the current version otherwise.
28179
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28180
+ ) : void 0;
28181
+ }
28182
+ if (values.modelJudgeConsent !== void 0) {
28183
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28184
+ acknowledgedAt: now().toISOString(),
28185
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28186
+ } : void 0;
28187
+ }
28188
+ return merged;
28189
+ }
28190
+
28191
+ // ../../packages/persistence/src/settings.ts
28192
+ var SETTINGS_FILENAME = "settings.json";
28193
+ function readWorkspaceSettings(base = defaultDataDir()) {
28194
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28195
+ }
28196
+ function readUserSettings(base) {
28197
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28198
+ if (!record2) return defaultWorkspaceSettings();
28199
+ try {
28200
+ return WorkspaceSettings.parse(record2);
28201
+ } catch {
28202
+ return defaultWorkspaceSettings();
28203
+ }
28204
+ }
28205
+ function readJson(file2) {
28206
+ let text;
28207
+ try {
28208
+ text = readFileSync4(file2, "utf8");
28209
+ } catch {
28210
+ return null;
28211
+ }
28212
+ return parseJsonObject(text) ?? null;
28213
+ }
28214
+
28215
+ // ../../packages/persistence/src/policy-floor.ts
28216
+ function refusalMessage(pack, attempted, floor, refusal) {
28217
+ switch (refusal) {
28218
+ case "lock":
28219
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28220
+ case "disable":
28221
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28222
+ case "floor":
28223
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28224
+ }
28225
+ }
28226
+ var PolicyFloorError = class extends Error {
28227
+ /** `namespace/packId` of the detection whose write was refused. */
28228
+ pack;
28229
+ /**
28230
+ * The archetype the caller asked for, or null when the write named none —
28231
+ * clearing the assignment, or switching the detection off.
28232
+ */
28233
+ attempted;
28234
+ /** The weakest archetype the control plane permits for this pack. */
28235
+ floor;
28236
+ refusal;
28237
+ constructor(pack, attempted, floor, refusal) {
28238
+ super(refusalMessage(pack, attempted, floor, refusal));
28239
+ this.name = "PolicyFloorError";
28240
+ this.pack = pack;
28241
+ this.attempted = attempted;
28242
+ this.floor = floor;
28243
+ this.refusal = refusal;
28244
+ }
28245
+ };
28246
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28247
+ try {
28248
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28249
+ const parsed = JSON.parse(raw);
28250
+ if (typeof parsed !== "object" || parsed === null) return null;
28251
+ return PolicyBundle.parse(parsed.bundle);
28252
+ } catch {
28253
+ return null;
28254
+ }
28255
+ }
28256
+ function indexEnabled(policies) {
28257
+ const byRuleId = /* @__PURE__ */ new Map();
28258
+ const byCategory = /* @__PURE__ */ new Map();
28259
+ for (const policy of policies) {
28260
+ if (!policy.enabled) continue;
28261
+ if ("ruleId" in policy.target) {
28262
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28263
+ } else if (!byCategory.has(policy.target.category)) {
28264
+ byCategory.set(policy.target.category, policy.action);
28265
+ }
28266
+ }
28267
+ return { byRuleId, byCategory };
28268
+ }
28269
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28270
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28271
+ const categories = new Set(rules.map((rule) => rule.category));
28272
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28273
+ return policies.some((policy) => {
28274
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28275
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28276
+ });
28277
+ }
28278
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28279
+ const floors = openControlPlaneFloors(base);
28280
+ return floors === null ? null : floors.floorFor(rules);
28281
+ }
28282
+ function openControlPlaneFloors(base = defaultDataDir()) {
28283
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28284
+ const bundle = readCachedPolicyBundle(base);
28285
+ if (bundle === null) return null;
28286
+ const indexes = indexEnabled(bundle.policies);
28287
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28288
+ }
28289
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28290
+ let action = null;
28291
+ for (const rule of rules) {
28292
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28293
+ if (resolved === void 0) continue;
28294
+ action = action === null ? resolved : strongerAction(action, resolved);
28295
+ }
28296
+ if (action === null) return null;
28297
+ return {
28298
+ floor: weakestBuiltinAtLeast(action),
28299
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28300
+ };
28301
+ }
28302
+ function policyAssignmentRefusal(policyId, floor) {
28303
+ if (floor.locked) return "lock";
28304
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28305
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28306
+ }
28307
+ function packEnablementRefusal(enabled, floor) {
28308
+ if (floor === null || enabled) return null;
28309
+ return "disable";
28310
+ }
27817
28311
 
27818
28312
  // ../../packages/persistence/src/semver.ts
27819
28313
  function parse3(version2) {
@@ -27907,8 +28401,19 @@ function ruleIdsOf(rulesJson) {
27907
28401
  return ids;
27908
28402
  }
27909
28403
  var SqliteInstalledPacksRepository = class {
27910
- constructor(db) {
28404
+ /**
28405
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28406
+ * floor needs both halves of it (settings/ says whether this machine is
28407
+ * attached, data/ holds the cached bundle). It is optional because a caller
28408
+ * holding only a DatabaseSync — every test construction site, and any embedder
28409
+ * that opens the store itself — has no layout to point at, and such a caller
28410
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28411
+ * from `openLocalDatabase`, which is the single construction site that owns a
28412
+ * real `~/.aka`.
28413
+ */
28414
+ constructor(db, baseDir) {
27911
28415
  this.db = db;
28416
+ this.baseDir = baseDir;
27912
28417
  this.insertMissingStmt = db.prepare(
27913
28418
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27914
28419
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -27930,11 +28435,17 @@ var SqliteInstalledPacksRepository = class {
27930
28435
  this.signatureStmt = db.prepare(
27931
28436
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
27932
28437
  );
28438
+ this.packRulesStmt = db.prepare(
28439
+ `SELECT rules_json AS rulesJson FROM installed_packs
28440
+ WHERE namespace = ? AND pack_id = ?`
28441
+ );
27933
28442
  }
27934
28443
  db;
28444
+ baseDir;
27935
28445
  insertMissingStmt;
27936
28446
  upsertAvailableStmt;
27937
28447
  signatureStmt;
28448
+ packRulesStmt;
27938
28449
  /**
27939
28450
  * Record the running binary's detection inventory. Refreshes the
27940
28451
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -27976,7 +28487,7 @@ var SqliteInstalledPacksRepository = class {
27976
28487
  let behind = false;
27977
28488
  for (const row of rows) {
27978
28489
  const params = {
27979
- id: randomUUID3(),
28490
+ id: randomUUID4(),
27980
28491
  namespace: row.namespace,
27981
28492
  packId: row.packId,
27982
28493
  version: row.version,
@@ -27988,7 +28499,7 @@ var SqliteInstalledPacksRepository = class {
27988
28499
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
27989
28500
  this.upsertAvailableStmt.run({
27990
28501
  ...params,
27991
- id: randomUUID3(),
28502
+ id: randomUUID4(),
27992
28503
  recordedBy: meta4?.recordedBy ?? null
27993
28504
  });
27994
28505
  } else {
@@ -28234,9 +28745,65 @@ var SqliteInstalledPacksRepository = class {
28234
28745
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28235
28746
  // caller rather than swallowing them. Each returns whether a row matched, so the
28236
28747
  // caller can tell an edit from a no-such-detection.
28748
+ /**
28749
+ * The rules one installed pack owns, reduced to what a floor computation
28750
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28751
+ * unreadable contributes no rules to a scan either, so it is not a detection
28752
+ * the control plane can be governing, and an empty list correctly imposes no
28753
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28754
+ * the user can re-enable, and its assignment stays governed meanwhile.
28755
+ */
28756
+ packFloorRules(namespace, packId) {
28757
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28758
+ if (!row) return [];
28759
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28760
+ }
28761
+ /**
28762
+ * What the connected control plane imposes on one installed pack, or null on a
28763
+ * machine that is its own authority (standalone, no cached bundle, or a
28764
+ * repository constructed without a layout base).
28765
+ *
28766
+ * Exposed as a READ so a surface can render the constraint — grey out the
28767
+ * choices below the floor, mark a locked detection as locked — rather than
28768
+ * offer the user a picker whose selections it will then be told it may not
28769
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28770
+ */
28771
+ policyFloor(namespace, packId) {
28772
+ if (this.baseDir === void 0) return null;
28773
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28774
+ }
28775
+ /**
28776
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28777
+ * entry only for a pack the control plane actually governs.
28778
+ *
28779
+ * A surface listing every detection asks per pack, and asking through
28780
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28781
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28782
+ * answer, repeated for each row, on every render. This reads all of that once.
28783
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28784
+ * exactly as the single-pack read returns null for them.
28785
+ */
28786
+ policyFloors(packs2) {
28787
+ const floors = /* @__PURE__ */ new Map();
28788
+ if (this.baseDir === void 0) return floors;
28789
+ const source = openControlPlaneFloors(this.baseDir);
28790
+ if (source === null) return floors;
28791
+ for (const pack of packs2) {
28792
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28793
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28794
+ }
28795
+ return floors;
28796
+ }
28237
28797
  /**
28238
28798
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28239
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28799
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28800
+ *
28801
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28802
+ * write below, and a detection the organization has authored a policy for is
28803
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28804
+ * a throw rather than a silently substituted value. This is the one device-local
28805
+ * write path for the assignment, so the check belongs here rather than on any
28806
+ * surface that offers the choice.
28240
28807
  */
28241
28808
  setPolicy(namespace, packId, policyId) {
28242
28809
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28244,14 +28811,38 @@ var SqliteInstalledPacksRepository = class {
28244
28811
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28245
28812
  );
28246
28813
  }
28814
+ const requested = policyId;
28815
+ const floor = this.policyFloor(namespace, packId);
28816
+ if (floor !== null) {
28817
+ const refusal = policyAssignmentRefusal(requested, floor);
28818
+ if (refusal !== null) {
28819
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28820
+ }
28821
+ }
28247
28822
  const res = this.db.prepare(
28248
28823
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28249
28824
  WHERE namespace = :namespace AND pack_id = :packId`
28250
28825
  ).run({ policyId, now: Date.now(), namespace, packId });
28251
28826
  return Number(res.changes) > 0;
28252
28827
  }
28253
- /** Enable or disable one installed pack. */
28828
+ /**
28829
+ * Enable or disable one installed pack.
28830
+ *
28831
+ * On an ATTACHED machine a detection the organization's bundle governs at all
28832
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
28833
+ * merely another point below the floor, and why re-enabling stays open. Like
28834
+ * the assignment above, the check belongs at this write path rather than on a
28835
+ * surface: this is the one device-local writer of the column, and a refusal
28836
+ * that lived in a page would leave the CLI free.
28837
+ */
28254
28838
  setEnabled(namespace, packId, enabled) {
28839
+ const floor = this.policyFloor(namespace, packId);
28840
+ if (floor !== null) {
28841
+ const refusal = packEnablementRefusal(enabled, floor);
28842
+ if (refusal !== null) {
28843
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
28844
+ }
28845
+ }
28255
28846
  const res = this.db.prepare(
28256
28847
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28257
28848
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28337,7 +28928,7 @@ var SqliteInventoryRepository = class {
28337
28928
  };
28338
28929
 
28339
28930
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28340
- import { randomUUID as randomUUID4 } from "crypto";
28931
+ import { randomUUID as randomUUID5 } from "crypto";
28341
28932
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28342
28933
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28343
28934
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28826,7 +29417,7 @@ var SqliteInventoryAssetsRepository = class {
28826
29417
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28827
29418
  VALUES (:id, :projectId, :path, :access, :now, :now)
28828
29419
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28829
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29420
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28830
29421
  }
28831
29422
  return true;
28832
29423
  }
@@ -28847,7 +29438,7 @@ var SqliteInventoryAssetsRepository = class {
28847
29438
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28848
29439
  VALUES (:id, :assetId, :trust, :now, :now)
28849
29440
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28850
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29441
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28851
29442
  }
28852
29443
  this.configRowsCache = void 0;
28853
29444
  return "ok";
@@ -29144,7 +29735,7 @@ var SqliteInventoryAssetsRepository = class {
29144
29735
  };
29145
29736
 
29146
29737
  // ../../packages/persistence/src/repositories/policies.ts
29147
- import { randomUUID as randomUUID5 } from "crypto";
29738
+ import { randomUUID as randomUUID6 } from "crypto";
29148
29739
  var SqlitePoliciesRepository = class {
29149
29740
  constructor(db) {
29150
29741
  this.db = db;
@@ -29179,7 +29770,7 @@ var SqlitePoliciesRepository = class {
29179
29770
  failOpenTransaction(this.db, () => {
29180
29771
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29181
29772
  stmt.run({
29182
- id: randomUUID5(),
29773
+ id: randomUUID6(),
29183
29774
  target: JSON.stringify({ category }),
29184
29775
  action,
29185
29776
  now: Date.now()
@@ -29199,7 +29790,7 @@ var SqlitePoliciesRepository = class {
29199
29790
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29200
29791
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29201
29792
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29202
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29793
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29203
29794
  }
29204
29795
  // Caps every global per-category policy currently set to block/redact down
29205
29796
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29267,7 +29858,7 @@ var SqlitePolicyCatalogRepository = class {
29267
29858
  };
29268
29859
 
29269
29860
  // ../../packages/persistence/src/repositories/project-files.ts
29270
- import { randomUUID as randomUUID6 } from "crypto";
29861
+ import { randomUUID as randomUUID7 } from "crypto";
29271
29862
  var SqliteProjectFilesRepository = class {
29272
29863
  constructor(db) {
29273
29864
  this.db = db;
@@ -29299,7 +29890,7 @@ var SqliteProjectFilesRepository = class {
29299
29890
  const stamp = Math.max(now, maxStamp + 1);
29300
29891
  for (const file2 of scan2.files) {
29301
29892
  this.upsertStmt.run({
29302
- id: randomUUID6(),
29893
+ id: randomUUID7(),
29303
29894
  projectId,
29304
29895
  path: file2.path,
29305
29896
  name: file2.name,
@@ -29313,9 +29904,9 @@ var SqliteProjectFilesRepository = class {
29313
29904
  };
29314
29905
 
29315
29906
  // ../../packages/persistence/src/repositories/resolutions.ts
29316
- import { randomUUID as randomUUID7 } from "crypto";
29907
+ import { randomUUID as randomUUID8 } from "crypto";
29317
29908
  var SqliteResolutionsRepository = class {
29318
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
29909
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29319
29910
  this.db = db;
29320
29911
  this.now = now;
29321
29912
  this.newId = newId;
@@ -29528,7 +30119,7 @@ var SqliteScanLedgerRepository = class {
29528
30119
  };
29529
30120
 
29530
30121
  // ../../packages/persistence/src/repositories/secret-vault.ts
29531
- import { randomUUID as randomUUID8 } from "crypto";
30122
+ import { randomUUID as randomUUID9 } from "crypto";
29532
30123
  function pageLimit(requested, fallback) {
29533
30124
  if (requested === void 0) return fallback;
29534
30125
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29574,12 +30165,14 @@ var SELECT_COLUMNS = `
29574
30165
  ciphertext,
29575
30166
  nonce,
29576
30167
  auth_tag AS authTag,
30168
+ user_authorized AS userAuthorized,
29577
30169
  occurrence_count AS occurrenceCount,
29578
30170
  first_seen AS firstSeen,
29579
30171
  last_seen AS lastSeen`;
29580
30172
  function toRow(raw) {
29581
- const { provider, ...rest } = raw;
29582
- return provider === null ? rest : { ...rest, provider };
30173
+ const { provider, userAuthorized, ...rest } = raw;
30174
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30175
+ return provider === null ? row : { ...row, provider };
29583
30176
  }
29584
30177
  var SqliteSecretVaultRepository = class {
29585
30178
  constructor(db) {
@@ -29589,17 +30182,18 @@ var SqliteSecretVaultRepository = class {
29589
30182
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29590
30183
  format_version, category, rule_id, masked_match, provider,
29591
30184
  ciphertext, nonce, auth_tag,
29592
- occurrence_count, first_seen, last_seen
30185
+ user_authorized, occurrence_count, first_seen, last_seen
29593
30186
  ) VALUES (
29594
30187
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29595
30188
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29596
30189
  :ciphertext, :nonce, :authTag,
29597
- 1, :now, :now
30190
+ :userAuthorized, 1, :now, :now
29598
30191
  )`
29599
30192
  );
29600
30193
  this.bumpStmt = db.prepare(
29601
30194
  `UPDATE secret_vault
29602
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30195
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30196
+ user_authorized = max(user_authorized, :userAuthorized)
29603
30197
  WHERE value_fingerprint = :valueFingerprint`
29604
30198
  );
29605
30199
  this.byPointerStmt = db.prepare(
@@ -29619,6 +30213,7 @@ var SqliteSecretVaultRepository = class {
29619
30213
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29620
30214
  WHERE pointer_id = :pointerId`
29621
30215
  );
30216
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29622
30217
  this.derefStmt = db.prepare(
29623
30218
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29624
30219
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29632,6 +30227,7 @@ var SqliteSecretVaultRepository = class {
29632
30227
  listStmt;
29633
30228
  replaceCiphertextStmt;
29634
30229
  refreshFingerprintStmt;
30230
+ deleteByPointerStmt;
29635
30231
  derefStmt;
29636
30232
  /**
29637
30233
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29640,6 +30236,11 @@ var SqliteSecretVaultRepository = class {
29640
30236
  * pointer, category and ciphertext, so the same secret always resolves to one
29641
30237
  * wire token. `minted` is true only when this call created the row.
29642
30238
  *
30239
+ * `userAuthorized` is the one field a repeat call may still change, and only
30240
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30241
+ * the row is shared with every automatic path that vaults the same value. See
30242
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30243
+ *
29643
30244
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29644
30245
  * writers cannot both decide they are minting.
29645
30246
  */
@@ -29666,13 +30267,18 @@ var SqliteSecretVaultRepository = class {
29666
30267
  ciphertext: input2.ciphertext,
29667
30268
  nonce: input2.nonce,
29668
30269
  authTag: input2.authTag,
30270
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29669
30271
  now
29670
30272
  })
29671
30273
  );
29672
30274
  minted = true;
29673
30275
  return;
29674
30276
  }
29675
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30277
+ this.bumpStmt.run({
30278
+ valueFingerprint: input2.valueFingerprint,
30279
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30280
+ now
30281
+ });
29676
30282
  },
29677
30283
  "IMMEDIATE"
29678
30284
  );
@@ -29732,6 +30338,42 @@ var SqliteSecretVaultRepository = class {
29732
30338
  );
29733
30339
  return destroyed;
29734
30340
  }
30341
+ /**
30342
+ * Destroy the named entries and report WHICH ones went — the scoped
30343
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30344
+ * values back where they came from. Ids the store does not hold are absent
30345
+ * from the answer rather than an error, so a set assembled from a stale read
30346
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30347
+ * it.
30348
+ *
30349
+ * The ids come back rather than a count because the caller's next act is to
30350
+ * write a purge row per destroyed entry, and a record of destruction has to
30351
+ * be a record of what was really destroyed: a selection is a claim about a
30352
+ * read that has since gone stale, and auditing from it invents a purge for an
30353
+ * entry still sitting in the vault.
30354
+ *
30355
+ * One transaction over the whole set rather than a statement per id: the
30356
+ * caller hands this the result of a restore pass it has completed, and a
30357
+ * fault partway through must leave the vault as it was found rather than
30358
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30359
+ * stands for, so half a delete is not a state anything can recover from.
30360
+ */
30361
+ deleteByPointerIds(pointerIds) {
30362
+ if (pointerIds.length === 0) return [];
30363
+ const deleted = [];
30364
+ withTransaction(
30365
+ this.db,
30366
+ () => {
30367
+ for (const pointerId of pointerIds) {
30368
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30369
+ deleted.push(pointerId);
30370
+ }
30371
+ }
30372
+ },
30373
+ "IMMEDIATE"
30374
+ );
30375
+ return deleted;
30376
+ }
29735
30377
  /**
29736
30378
  * Record (or re-stamp) one place a pointer has been written. One row per
29737
30379
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29744,7 +30386,7 @@ var SqliteSecretVaultRepository = class {
29744
30386
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29745
30387
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29746
30388
  ).run({
29747
- id: randomUUID8(),
30389
+ id: randomUUID9(),
29748
30390
  pointerId: entry.pointerId,
29749
30391
  location: entry.location,
29750
30392
  kind: entry.kind,
@@ -30257,15 +30899,15 @@ var SqliteSecurityRepository = class {
30257
30899
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30258
30900
  const rows = allRows(
30259
30901
  this.db.prepare(
30260
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
30902
+ `SELECT e.repo AS repo, count(*) AS c
30261
30903
  FROM inspection_findings f
30262
30904
  JOIN audit_events e ON e.id = f.audit_event_id
30263
30905
  WHERE e.started_at >= :from AND e.started_at < :to
30264
30906
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30265
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30266
- AND json_extract(e.attributes, '$.repo') != ''
30267
- GROUP BY repo
30268
- ORDER BY c DESC, repo
30907
+ AND e.repo IS NOT NULL
30908
+ AND e.repo != ''
30909
+ GROUP BY e.repo
30910
+ ORDER BY c DESC, e.repo
30269
30911
  LIMIT :limit`
30270
30912
  ),
30271
30913
  { from, to: now, limit }
@@ -30327,7 +30969,7 @@ var SqliteSecurityRepository = class {
30327
30969
  `SELECT f.finding_key AS finding_key,
30328
30970
  d.rule_id AS rule_id,
30329
30971
  d.severity AS severity,
30330
- json_extract(e.attributes, '$.file_path') AS path,
30972
+ e.file_path AS path,
30331
30973
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30332
30974
  latest.resolved_at AS latest_resolved_at
30333
30975
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30380,7 +31022,7 @@ var SqliteSecurityRepository = class {
30380
31022
  };
30381
31023
 
30382
31024
  // ../../packages/persistence/src/repositories/shares.ts
30383
- import { randomUUID as randomUUID9 } from "crypto";
31025
+ import { randomUUID as randomUUID10 } from "crypto";
30384
31026
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30385
31027
  var IN_CHUNK = 500;
30386
31028
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30468,7 +31110,7 @@ function buildSummary(dest, endpoints) {
30468
31110
  callSiteCount,
30469
31111
  transports: distinctTransports(transports),
30470
31112
  dataClasses: distinctDataClasses(dataClasses),
30471
- review: buildReviewInfo(dest.trust, transports),
31113
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30472
31114
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30473
31115
  endpoints: endpoints.map(toEndpointSummary)
30474
31116
  };
@@ -30495,7 +31137,7 @@ function buildDetail(dest, endpoints, callSites) {
30495
31137
  lastSeen: new Date(lastSeenMs).toISOString(),
30496
31138
  transports: distinctTransports(transports),
30497
31139
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30498
- review: buildReviewInfo(dest.trust, transports),
31140
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30499
31141
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30500
31142
  note: dest.note,
30501
31143
  endpoints: endpoints.map((ep) => ({
@@ -30524,7 +31166,11 @@ var SqliteSharesRepository = class {
30524
31166
  FROM share_destination d
30525
31167
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30526
31168
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30527
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31169
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31170
+ AND NOT EXISTS (
31171
+ SELECT 1 FROM egress_decision_override o
31172
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31173
+ )`
30528
31174
  );
30529
31175
  const kindCounts = countBy(
30530
31176
  this.db,
@@ -30636,7 +31282,7 @@ var SqliteSharesRepository = class {
30636
31282
  (id, destination_id, host, decision, created_at, updated_at)
30637
31283
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30638
31284
  ).run({
30639
- id: randomUUID9(),
31285
+ id: randomUUID10(),
30640
31286
  destinationId,
30641
31287
  host: dest.host,
30642
31288
  decision,
@@ -30785,7 +31431,7 @@ var SqliteSharesRepository = class {
30785
31431
  let destinationId = destIds.get(hit.host);
30786
31432
  if (destinationId === void 0) {
30787
31433
  destStmt.run({
30788
- id: randomUUID9(),
31434
+ id: randomUUID10(),
30789
31435
  kind: hit.kind,
30790
31436
  name: hit.name,
30791
31437
  host: hit.host,
@@ -30801,7 +31447,7 @@ var SqliteSharesRepository = class {
30801
31447
  let endpointId = endpointIds.get(endpointKey);
30802
31448
  if (endpointId === void 0) {
30803
31449
  endpointStmt.run({
30804
- id: randomUUID9(),
31450
+ id: randomUUID10(),
30805
31451
  destinationId,
30806
31452
  method: hit.method,
30807
31453
  transport: hit.transport,
@@ -30814,7 +31460,7 @@ var SqliteSharesRepository = class {
30814
31460
  endpointIds.set(endpointKey, endpointId);
30815
31461
  }
30816
31462
  siteStmt.run({
30817
- id: randomUUID9(),
31463
+ id: randomUUID10(),
30818
31464
  endpointId,
30819
31465
  project: input2.project,
30820
31466
  projectKey: input2.projectKey,
@@ -31179,6 +31825,7 @@ function purgeSampleData(db) {
31179
31825
  }
31180
31826
 
31181
31827
  // ../../packages/persistence/src/database.ts
31828
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31182
31829
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31183
31830
  "aka.persistence.unsafeTestOnlyRawHandle"
31184
31831
  );
@@ -31226,7 +31873,7 @@ function backupLegacyStore(db, file2) {
31226
31873
  discardStore(file2, backup);
31227
31874
  return backup;
31228
31875
  }
31229
- function openAndInitialize(file2) {
31876
+ function openAndInitialize(file2, base) {
31230
31877
  let db = openWithPragmas(file2);
31231
31878
  try {
31232
31879
  if (isForeignSqliteLineage(db)) {
@@ -31239,7 +31886,7 @@ function openAndInitialize(file2) {
31239
31886
  applyMigrations(db, file2);
31240
31887
  tightenPerms(file2);
31241
31888
  const policies = new SqlitePoliciesRepository(db);
31242
- const installedPacks = new SqliteInstalledPacksRepository(db);
31889
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31243
31890
  const repositories = {
31244
31891
  events: new SqliteEventsRepository(db),
31245
31892
  findings: new SqliteFindingsRepository(db),
@@ -31275,7 +31922,7 @@ function openAndInitialize(file2) {
31275
31922
  }
31276
31923
  function openLocalDatabase(dir) {
31277
31924
  ensureDataDirSync(dir);
31278
- const file2 = join4(dir, DB_FILENAME);
31925
+ const file2 = join7(dir, DB_FILENAME);
31279
31926
  reapStalePartials(file2);
31280
31927
  const {
31281
31928
  db,
@@ -31303,7 +31950,13 @@ function openLocalDatabase(dir) {
31303
31950
  inspectionDefinitions,
31304
31951
  inspectionFindings,
31305
31952
  configInventory
31306
- } = openAndInitialize(file2);
31953
+ } = openAndInitialize(
31954
+ file2,
31955
+ // `dir` is always `<base>/data` — every caller resolves it through
31956
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
31957
+ // settings/ and data/, and the pack-policy floor needs both halves.
31958
+ dirname2(dir)
31959
+ );
31307
31960
  function captureRowId(event) {
31308
31961
  return captureId(
31309
31962
  event.metadata?.sessionId ?? null,
@@ -31316,6 +31969,21 @@ function openLocalDatabase(dir) {
31316
31969
  historySync.markSynced([captureRowId(event)], atMs);
31317
31970
  });
31318
31971
  }
31972
+ function markCaptureOwed(event) {
31973
+ failOpenTransaction(db, () => {
31974
+ historySync.markCaptureOwed(captureRowId(event));
31975
+ });
31976
+ }
31977
+ function markAuditEventsDelivered(events2, atMs) {
31978
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
31979
+ if (stampable.length === 0) return;
31980
+ failOpenTransaction(db, () => {
31981
+ historySync.markSynced(
31982
+ stampable.map((event) => event.id),
31983
+ atMs
31984
+ );
31985
+ });
31986
+ }
31319
31987
  function recordCapture(event, detected) {
31320
31988
  failOpenTransaction(db, () => {
31321
31989
  const sessionId = event.metadata?.sessionId;
@@ -31402,7 +32070,7 @@ function openLocalDatabase(dir) {
31402
32070
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31403
32071
  if (!definitionId) continue;
31404
32072
  inspectionFindings.insertFinding({
31405
- id: randomUUID10(),
32073
+ id: randomUUID11(),
31406
32074
  auditEventId: record2.scanEvent.id,
31407
32075
  inspectionDefinitionId: definitionId,
31408
32076
  span: finding.span,
@@ -31498,6 +32166,8 @@ function openLocalDatabase(dir) {
31498
32166
  inspectionFindings,
31499
32167
  recordCapture,
31500
32168
  markCaptureDelivered,
32169
+ markCaptureOwed,
32170
+ markAuditEventsDelivered,
31501
32171
  ensureInventory,
31502
32172
  recordConfigScan,
31503
32173
  recordProjectFiles,
@@ -31516,164 +32186,23 @@ function openLocalDatabase(dir) {
31516
32186
  };
31517
32187
  }
31518
32188
 
31519
- // ../../packages/persistence/src/file-lock.ts
31520
- import { randomUUID as randomUUID11 } from "crypto";
31521
- import {
31522
- closeSync,
31523
- existsSync as existsSync2,
31524
- openSync,
31525
- readFileSync as readFileSync2,
31526
- rmSync as rmSync5,
31527
- statSync as statSync3,
31528
- writeFileSync as writeFileSync2
31529
- } from "fs";
31530
- import { hostname as hostname3 } from "os";
31531
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31532
-
31533
32189
  // ../../packages/persistence/src/finding-key.ts
31534
32190
  import { createHash as createHash3 } from "crypto";
31535
32191
 
31536
32192
  // ../../packages/persistence/src/fingerprint.ts
31537
32193
  import { createHmac, randomBytes } from "crypto";
31538
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31539
- import { join as join5 } from "path";
32194
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32195
+ import { join as join8 } from "path";
31540
32196
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31541
32197
 
31542
32198
  // ../../packages/persistence/src/history-preview.ts
31543
32199
  import { existsSync as existsSync4 } from "fs";
31544
- import { join as join6 } from "path";
32200
+ import { join as join9 } from "path";
31545
32201
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31546
32202
 
31547
- // ../../packages/persistence/src/local-layout.ts
31548
- import { renameSync as renameSync3 } from "fs";
31549
- import { mkdir } from "fs/promises";
31550
- import { homedir } from "os";
31551
- import { join as join7 } from "path";
31552
- function defaultDataDir() {
31553
- return join7(homedir(), ".aka");
31554
- }
31555
- function settingsDir(base = defaultDataDir()) {
31556
- return join7(base, "settings");
31557
- }
31558
- function dataDir(base = defaultDataDir()) {
31559
- return join7(base, "data");
31560
- }
31561
- function dbPath(base = defaultDataDir()) {
31562
- return join7(dataDir(base), "aka.db");
31563
- }
31564
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31565
- ensureDataDirSync(dir);
31566
- }
31567
- function migrateLegacyLayout(base = defaultDataDir()) {
31568
- const moves = [
31569
- { name: "config.json", dest: settingsDir(base) },
31570
- { name: "policy-cache.json", dest: dataDir(base) }
31571
- ];
31572
- for (const { name, dest } of moves) {
31573
- try {
31574
- ensureDataDirSync(dest);
31575
- const moved = join7(dest, name);
31576
- renameSync3(join7(base, name), moved);
31577
- tightenFile(moved);
31578
- } catch {
31579
- }
31580
- }
31581
- }
31582
-
31583
- // ../../packages/persistence/src/managed-settings.ts
31584
- import { readFileSync as readFileSync4 } from "fs";
31585
- import { posix, win32 } from "path";
31586
- function managedSettingsPaths(platform2 = process.platform) {
31587
- if (platform2 === "darwin") {
31588
- return [
31589
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31590
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31591
- ];
31592
- }
31593
- if (platform2 === "win32") {
31594
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31595
- }
31596
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31597
- }
31598
- function readManagedSettings(paths = managedSettingsPaths()) {
31599
- for (const path of paths) {
31600
- let text;
31601
- try {
31602
- text = readFileSync4(path, "utf8");
31603
- } catch {
31604
- continue;
31605
- }
31606
- const record2 = parseJsonObject(text);
31607
- if (!record2) continue;
31608
- const parsed = ManagedSettings.safeParse(record2);
31609
- if (parsed.success) return parsed.data;
31610
- }
31611
- return null;
31612
- }
31613
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31614
- if (!managed) return settings;
31615
- const { values } = managed;
31616
- const merged = { ...settings };
31617
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31618
- if (values.controlPlane !== void 0) {
31619
- merged.controlPlane = {
31620
- ...values.controlPlane,
31621
- // The administrator pinned WHICH deployment, not WHEN this machine
31622
- // joined it. Keep the user's own attach time when the endpoint is
31623
- // unchanged, so a managed machine does not appear to re-attach on every
31624
- // read; stamp a fresh one when the administrator moved it.
31625
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31626
- };
31627
- }
31628
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31629
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31630
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31631
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31632
- if (values.vaultConsent !== void 0) {
31633
- merged.vaultConsent = values.vaultConsent ? (
31634
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31635
- // at the current version otherwise.
31636
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31637
- ) : void 0;
31638
- }
31639
- if (values.modelJudgeConsent !== void 0) {
31640
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31641
- acknowledgedAt: now().toISOString(),
31642
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31643
- } : void 0;
31644
- }
31645
- return merged;
31646
- }
31647
-
31648
- // ../../packages/persistence/src/settings.ts
31649
- import { readFileSync as readFileSync5 } from "fs";
31650
- import { join as join8 } from "path";
31651
- var SETTINGS_FILENAME = "settings.json";
31652
- function readWorkspaceSettings(base = defaultDataDir()) {
31653
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31654
- }
31655
- function readUserSettings(base) {
31656
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31657
- if (!record2) return defaultWorkspaceSettings();
31658
- try {
31659
- return WorkspaceSettings.parse(record2);
31660
- } catch {
31661
- return defaultWorkspaceSettings();
31662
- }
31663
- }
31664
- function readJson(file2) {
31665
- let text;
31666
- try {
31667
- text = readFileSync5(file2, "utf8");
31668
- } catch {
31669
- return null;
31670
- }
31671
- return parseJsonObject(text) ?? null;
31672
- }
31673
-
31674
32203
  // ../../packages/persistence/src/store-symlinks.ts
31675
32204
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31676
- import { dirname as dirname2, join as join9, resolve } from "path";
32205
+ import { dirname as dirname3, join as join10, resolve } from "path";
31677
32206
 
31678
32207
  // ../../packages/persistence/src/vault/crypto.ts
31679
32208
  import {
@@ -31687,19 +32216,19 @@ import {
31687
32216
  // ../../packages/persistence/src/vault/key-provider.ts
31688
32217
  import { execFileSync } from "child_process";
31689
32218
  import { randomBytes as randomBytes2 } from "crypto";
31690
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
31691
- import { join as join10 } from "path";
32219
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32220
+ import { join as join11 } from "path";
31692
32221
 
31693
32222
  // ../../packages/persistence/src/vault/vault.ts
31694
32223
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
31695
32224
 
31696
32225
  // ../../packages/persistence/src/warn-era-cap.ts
31697
32226
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
31698
- import { join as join11 } from "path";
32227
+ import { join as join12 } from "path";
31699
32228
 
31700
32229
  // ../../packages/plugin-sdk/src/config.ts
31701
32230
  import { existsSync as existsSync7 } from "fs";
31702
- import { join as join12 } from "path";
32231
+ import { join as join13 } from "path";
31703
32232
 
31704
32233
  // ../../packages/plugin-sdk/src/provider-env.ts
31705
32234
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -31753,7 +32282,7 @@ function resolveProvider() {
31753
32282
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
31754
32283
  try {
31755
32284
  ensureLayoutDirSync(base);
31756
- const settingsFile = join12(settingsDir(base), "settings.json");
32285
+ const settingsFile = join13(settingsDir(base), "settings.json");
31757
32286
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
31758
32287
  } catch {
31759
32288
  }
@@ -31777,9 +32306,9 @@ function resolveProviderSafe(resolveProviderFn) {
31777
32306
  }
31778
32307
 
31779
32308
  // ../../packages/plugin-sdk/src/config-inventory.ts
31780
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32309
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
31781
32310
  import { homedir as homedir2 } from "os";
31782
- import { basename as basename3, join as join14 } from "path";
32311
+ import { basename as basename3, join as join15 } from "path";
31783
32312
 
31784
32313
  // ../../packages/detections/src/egress/registry.ts
31785
32314
  var EXTRACTOR_VERSION = "1";
@@ -34814,8 +35343,8 @@ function maskText(text) {
34814
35343
  }
34815
35344
 
34816
35345
  // ../../packages/plugin-sdk/src/repo.ts
34817
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
34818
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
35346
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
35347
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
34819
35348
 
34820
35349
  // ../../packages/plugin-sdk/src/events.ts
34821
35350
  import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
@@ -34827,8 +35356,8 @@ import { Worker } from "worker_threads";
34827
35356
 
34828
35357
  // ../../packages/plugin-sdk/src/ignore-layers.ts
34829
35358
  var import_ignore = __toESM(require_ignore(), 1);
34830
- import { readFileSync as readFileSync9 } from "fs";
34831
- import { join as join15 } from "path";
35359
+ import { readFileSync as readFileSync10 } from "fs";
35360
+ import { join as join16 } from "path";
34832
35361
 
34833
35362
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
34834
35363
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -34839,20 +35368,20 @@ import {
34839
35368
  fstatSync,
34840
35369
  mkdirSync as mkdirSync2,
34841
35370
  openSync as openSync2,
34842
- readFileSync as readFileSync10,
35371
+ readFileSync as readFileSync11,
34843
35372
  readSync,
34844
35373
  writeFileSync as writeFileSync5
34845
35374
  } from "fs";
34846
- import { join as join16 } from "path";
35375
+ import { join as join17 } from "path";
34847
35376
  var TAIL_BYTES = 256 * 1024;
34848
35377
 
34849
35378
  // ../../packages/plugin-sdk/src/nudge.ts
34850
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
34851
- import { join as join17 } from "path";
35379
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35380
+ import { join as join18 } from "path";
34852
35381
 
34853
35382
  // ../../packages/plugin-sdk/src/paths.ts
34854
35383
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
34855
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
35384
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
34856
35385
 
34857
35386
  // ../../packages/plugin-sdk/src/posture.ts
34858
35387
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
@@ -34866,7 +35395,7 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
34866
35395
 
34867
35396
  // ../../packages/plugin-sdk/src/project-files.ts
34868
35397
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
34869
- import { basename as basename5, join as join18 } from "path";
35398
+ import { basename as basename5, join as join19 } from "path";
34870
35399
 
34871
35400
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
34872
35401
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -34984,11 +35513,11 @@ async function applySetupTriageSuppressions(entries, writer, opts) {
34984
35513
 
34985
35514
  // ../../packages/plugin-sdk/src/throttle.ts
34986
35515
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
34987
- import { join as join19 } from "path";
35516
+ import { join as join20 } from "path";
34988
35517
 
34989
35518
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
34990
35519
  import { writeFileSync as writeFileSync8 } from "fs";
34991
- import { join as join20 } from "path";
35520
+ import { join as join21 } from "path";
34992
35521
 
34993
35522
  // ../../packages/setup-wizard/src/triage/dedupe.ts
34994
35523
  function dedupeKey(hit) {
@@ -35043,12 +35572,12 @@ function deriveFalsePositivePatterns(hits, rec, plan) {
35043
35572
  }
35044
35573
 
35045
35574
  // ../../packages/setup-wizard/src/triage/gate-display.ts
35046
- function findContext(entry, join24) {
35047
- const byFingerprint = join24.find(
35575
+ function findContext(entry, join25) {
35576
+ const byFingerprint = join25.find(
35048
35577
  (j) => j.valueFingerprint !== void 0 && j.valueFingerprint === entry.valueFingerprint
35049
35578
  );
35050
35579
  if (byFingerprint) return byFingerprint.maskedContext;
35051
- const byRuleAndMask = join24.find(
35580
+ const byRuleAndMask = join25.find(
35052
35581
  (j) => j.ruleId === entry.ruleId && j.maskedMatch === entry.maskedValue
35053
35582
  );
35054
35583
  return byRuleAndMask?.maskedContext;
@@ -35119,13 +35648,13 @@ function renderShowcase(showcase) {
35119
35648
 
35120
35649
  ${blocks.join("\n\n")}`;
35121
35650
  }
35122
- function renderSuppressionGate(entries, join24) {
35651
+ function renderSuppressionGate(entries, join25) {
35123
35652
  if (entries.length === 0) {
35124
35653
  return "No false-positive suppressions to confirm \u2014 nothing will be written.";
35125
35654
  }
35126
35655
  const header = entries.length === 1 ? "This looks like a false positive \u2014 take a look before I suppress it:" : `These ${String(entries.length)} look like false positives \u2014 take a look before I suppress them:`;
35127
35656
  const blocks = entries.map((entry, i) => {
35128
- const context = findContext(entry, join24);
35657
+ const context = findContext(entry, join25);
35129
35658
  const lines = [
35130
35659
  `${String(i + 1)}. ${entry.ruleId} [${entry.category}]`,
35131
35660
  ` value: ${entry.maskedValue}`,
@@ -35211,9 +35740,9 @@ function mergeRecommendations(verdicts) {
35211
35740
  }
35212
35741
 
35213
35742
  // ../../packages/setup-wizard/src/triage/plan-file.ts
35214
- import { mkdtempSync, readFileSync as readFileSync12, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
35743
+ import { mkdtempSync, readFileSync as readFileSync13, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
35215
35744
  import { tmpdir } from "os";
35216
- import { basename as basename6, dirname as dirname5, join as join21 } from "path";
35745
+ import { basename as basename6, dirname as dirname6, join as join22 } from "path";
35217
35746
  var SuppressionEntrySchema = external_exports.object({
35218
35747
  ruleId: external_exports.string(),
35219
35748
  category: DetectionCategory,
@@ -35268,19 +35797,19 @@ function serializePlan(plan, current) {
35268
35797
  function writePlanFile(plan, current, rawValues, deps = {}) {
35269
35798
  const serialized = serializePlan(plan, current);
35270
35799
  assertRawFree(serialized, rawValues);
35271
- const dir = (deps.mkTempDir ?? (() => mkdtempSync(join21(tmpdir(), "aka-plan-"))))();
35272
- const path = join21(dir, "setup-plan.json");
35800
+ const dir = (deps.mkTempDir ?? (() => mkdtempSync(join22(tmpdir(), "aka-plan-"))))();
35801
+ const path = join22(dir, "setup-plan.json");
35273
35802
  writeFileSync9(path, serialized, { encoding: "utf8", mode: 384 });
35274
35803
  return path;
35275
35804
  }
35276
35805
  function readPlanFile(path) {
35277
- const text = readFileSync12(path, "utf8");
35806
+ const text = readFileSync13(path, "utf8");
35278
35807
  const json2 = JSON.parse(text);
35279
35808
  return PersistedPlanSchema.parse(json2);
35280
35809
  }
35281
35810
  function deletePlanFile(path) {
35282
35811
  rmSync7(path, { force: true });
35283
- const dir = dirname5(path);
35812
+ const dir = dirname6(path);
35284
35813
  if (!basename6(dir).startsWith("aka-plan-")) return;
35285
35814
  try {
35286
35815
  rmdirSync(dir);
@@ -35348,8 +35877,8 @@ function buildJoinEntries(hits) {
35348
35877
  }
35349
35878
 
35350
35879
  // ../../packages/setup-wizard/src/triage/resolve.ts
35351
- function resolveSuppressions(rec, join24) {
35352
- const byId = new Map(join24.map((e) => [e.id, e]));
35880
+ function resolveSuppressions(rec, join25) {
35881
+ const byId = new Map(join25.map((e) => [e.id, e]));
35353
35882
  const entries = [];
35354
35883
  const skipped = [];
35355
35884
  for (const cat of rec.perCategory) {
@@ -35451,7 +35980,7 @@ function parseTriageStream(text) {
35451
35980
  return { hits, status: "complete" };
35452
35981
  }
35453
35982
  function planTriageWriteback(hits, rec) {
35454
- const join24 = buildJoinEntries(hits);
35983
+ const join25 = buildJoinEntries(hits);
35455
35984
  const rawValues = hits.map((h) => h.rawMatch);
35456
35985
  const skipped = [];
35457
35986
  const posture = {};
@@ -35491,7 +36020,7 @@ function planTriageWriteback(hits, rec) {
35491
36020
  }
35492
36021
  const { entries, skipped: resolveSkips } = resolveSuppressions(
35493
36022
  { perCategory: safeCategories, notes: rec.notes },
35494
- join24
36023
+ join25
35495
36024
  );
35496
36025
  skipped.push(...resolveSkips);
35497
36026
  let notes = rec.notes;
@@ -35501,7 +36030,7 @@ function planTriageWriteback(hits, rec) {
35501
36030
  if (err instanceof RawEgressError) notes = SCRUBBED_NOTES;
35502
36031
  else throw err;
35503
36032
  }
35504
- return { entries, posture, showcase, join: join24, notes, skipped };
36033
+ return { entries, posture, showcase, join: join25, notes, skipped };
35505
36034
  }
35506
36035
  function recommendedPosture(evidence) {
35507
36036
  return { ...severityFloorPosture(), ...evidence };
@@ -35770,9 +36299,9 @@ function parseRecommendation(text) {
35770
36299
 
35771
36300
  // src/triage/judge.ts
35772
36301
  import { execFileSync as execFileSync2 } from "child_process";
35773
- import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, rmSync as rmSync8 } from "fs";
36302
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync8 } from "fs";
35774
36303
  import { tmpdir as tmpdir2 } from "os";
35775
- import { dirname as dirname6, join as join22 } from "path";
36304
+ import { dirname as dirname7, join as join23 } from "path";
35776
36305
  import { fileURLToPath as fileURLToPath2 } from "url";
35777
36306
 
35778
36307
  // ../../packages/plugin-sdk/src/bare-command.ts
@@ -35883,8 +36412,8 @@ function planBareCommand(command, args, deps = {}) {
35883
36412
  }
35884
36413
 
35885
36414
  // src/triage/judge.ts
35886
- var TRIAGE_DIR = dirname6(fileURLToPath2(import.meta.url));
35887
- var DEFAULT_RUBRIC_PATH = join22(
36415
+ var TRIAGE_DIR = dirname7(fileURLToPath2(import.meta.url));
36416
+ var DEFAULT_RUBRIC_PATH = join23(
35888
36417
  TRIAGE_DIR,
35889
36418
  "..",
35890
36419
  "..",
@@ -35921,7 +36450,7 @@ function judgeEnv(platform2 = process.platform) {
35921
36450
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
35922
36451
  };
35923
36452
  if (platform2 === "darwin") {
35924
- env.CLAUDE_CONFIG_DIR = mkdtempSync2(join22(tmpdir2(), "aka-judge-cfg-"));
36453
+ env.CLAUDE_CONFIG_DIR = mkdtempSync2(join23(tmpdir2(), "aka-judge-cfg-"));
35925
36454
  }
35926
36455
  return env;
35927
36456
  }
@@ -35956,7 +36485,7 @@ function runJudge(hits, deps) {
35956
36485
  if (typeof deps.spawn !== "function") {
35957
36486
  throw new TypeError("runJudge requires deps.spawn \u2014 there is no live-spawn fallback");
35958
36487
  }
35959
- const rubric = deps.loadRubric?.() ?? readFileSync13(DEFAULT_RUBRIC_PATH, "utf8");
36488
+ const rubric = deps.loadRubric?.() ?? readFileSync14(DEFAULT_RUBRIC_PATH, "utf8");
35960
36489
  const hitsJsonl = hits.map((h) => JSON.stringify(toJudgePayload(h))).join("\n");
35961
36490
  const fullPrompt = `${rubric}
35962
36491
 
@@ -36225,11 +36754,11 @@ function resolveCreatedBy() {
36225
36754
  }
36226
36755
  }
36227
36756
  function loadRubric() {
36228
- const here = dirname7(fileURLToPath4(import.meta.url));
36229
- const shipped = join23(here, "triage-rubric.md");
36230
- if (existsSync11(shipped)) return readFileSync14(shipped, "utf8");
36231
- return readFileSync14(
36232
- join23(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
36757
+ const here = dirname8(fileURLToPath4(import.meta.url));
36758
+ const shipped = join24(here, "triage-rubric.md");
36759
+ if (existsSync11(shipped)) return readFileSync15(shipped, "utf8");
36760
+ return readFileSync15(
36761
+ join24(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
36233
36762
  "utf8"
36234
36763
  );
36235
36764
  }
@@ -36239,7 +36768,7 @@ async function main() {
36239
36768
  argv,
36240
36769
  // fd 0 = stdin; the wizard pipes `backfill.js --triage` into this on preview.
36241
36770
  // Called only on the preview path — the confirm path never reads a stream.
36242
- readStream: (streamPath) => streamPath !== void 0 ? readFileSync14(streamPath, "utf8") : readFileSync14(0, "utf8"),
36771
+ readStream: (streamPath) => streamPath !== void 0 ? readFileSync15(streamPath, "utf8") : readFileSync15(0, "utf8"),
36243
36772
  runJudge: (hits) => runJudge(hits, { spawn: spawnClaude, loadRubric }),
36244
36773
  // The distinct model-judge egress consent, read from settings.json. When it
36245
36774
  // is absent or stale the preview skips the judge instead of sending findings