@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.
@@ -497,6 +497,7 @@ import { createHash as createHash4 } from "crypto";
497
497
  // ../../packages/persistence/src/attached-derived.ts
498
498
  import { rmSync } from "fs";
499
499
  import { join } from "path";
500
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
500
501
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
501
502
  var ATTACHED_HISTORY_SYNC_STATE_FILENAME = "attached-history-sync.json";
502
503
 
@@ -597,6 +598,30 @@ var SQLITE_MIGRATIONS = [
597
598
  {
598
599
  tag: "0022_audit_inspection_ms",
599
600
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
601
+ },
602
+ {
603
+ tag: "0023_secret_vault_user_authorized",
604
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
605
+ },
606
+ {
607
+ tag: "0024_finding_resolution_key_created_index",
608
+ 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`);"
609
+ },
610
+ {
611
+ tag: "0025_audit_capture_attribute_columns",
612
+ 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;"
613
+ },
614
+ {
615
+ tag: "0026_audit_llm_call_usage_columns",
616
+ 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;"
617
+ },
618
+ {
619
+ tag: "0027_audit_llm_usage_index",
620
+ 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;"
621
+ },
622
+ {
623
+ tag: "0028_activity_session_probe_indexes",
624
+ 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"
600
625
  }
601
626
  ];
602
627
 
@@ -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, parsed2, 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,252 @@ 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
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23333
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23334
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23335
+ id: "RedactFallback"
23336
+ });
23337
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23338
+ var BUILTIN_POLICY_SPECS = {
23339
+ monitor: {
23340
+ name: "Monitor",
23341
+ action: "log",
23342
+ reversible: false,
23343
+ description: "Log every match for audit. The request is allowed through untouched."
23344
+ },
23345
+ warn: {
23346
+ name: "Warn",
23347
+ action: "warn",
23348
+ reversible: false,
23349
+ description: "Allow the request, but warn the user inline before it is sent."
23350
+ },
23351
+ redact: {
23352
+ name: "Redact",
23353
+ action: "redact",
23354
+ reversible: false,
23355
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23356
+ },
23357
+ vault: {
23358
+ name: "Redact & Vault",
23359
+ action: "redact",
23360
+ reversible: true,
23361
+ 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."
23362
+ },
23363
+ block: {
23364
+ name: "Block",
23365
+ action: "block",
23366
+ reversible: false,
23367
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23368
+ }
23369
+ };
23370
+ function builtinPolicyToAction(id) {
23371
+ return BUILTIN_POLICY_SPECS[id].action;
23372
+ }
23373
+ var PALETTE_WEAKEST_FIRST = [
23374
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23375
+ ];
23376
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23377
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23378
+ );
23379
+ var ACTION_STRENGTH_ORDER = [
23380
+ ...BELOW_PALETTE,
23381
+ ...PALETTE_WEAKEST_FIRST
23382
+ ];
23383
+ function actionRank(action) {
23384
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23385
+ }
23386
+ function isActionAtLeast(action, floor) {
23387
+ return actionRank(action) >= actionRank(floor);
23388
+ }
23389
+ function strongerAction(a, b) {
23390
+ return actionRank(a) >= actionRank(b) ? a : b;
23391
+ }
23392
+ function weakestBuiltinAtLeast(floor) {
23393
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23394
+ }
23395
+ var PackPolicyFloor = external_exports.object({
23396
+ /**
23397
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23398
+ * rather than a raw ActionTaken because that is the vocabulary the user
23399
+ * picks from — a floor a UI cannot name is one it cannot explain.
23400
+ */
23401
+ floor: BuiltinPolicyId,
23402
+ /**
23403
+ * True when the organization AUTHORED a policy governing this pack rather
23404
+ * than stating a minimum: it gave the answer, so the pack is not
23405
+ * re-assignable locally in either direction.
23406
+ */
23407
+ locked: external_exports.boolean()
23408
+ }).describe("PackPolicyFloor");
23409
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23410
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23411
+ );
23412
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23413
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23414
+ );
23415
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23416
+ function builtinPolicyIsReversible(id) {
23417
+ return BUILTIN_POLICY_SPECS[id].reversible;
23418
+ }
23419
+ function policyIdIsReversible(policyId) {
23420
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23421
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23422
+ return builtinPolicyIsReversible(id);
23423
+ }
23424
+ var DEFAULT_ACTIONS = Object.fromEntries(
23425
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23426
+ );
23427
+ var BUILTIN_POLICIES = Object.fromEntries(
23428
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23429
+ );
23430
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23431
+ function policyIdToAction(policyId) {
23432
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23433
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23434
+ return BUILTIN_POLICIES[id].action;
23435
+ }
23436
+ var UsedByItem = external_exports.object({
23437
+ id: external_exports.string(),
23438
+ name: external_exports.string(),
23439
+ ruleCount: external_exports.number().int().nonnegative(),
23440
+ enabled: external_exports.boolean()
23441
+ }).meta({ id: "UsedByItem" });
23442
+ var PolicyListItem = external_exports.object({
23443
+ id: external_exports.string(),
23444
+ kind: PolicyKind,
23445
+ name: external_exports.string(),
23446
+ enabled: external_exports.boolean(),
23447
+ usedByCount: external_exports.number().int().nonnegative()
23448
+ }).meta({ id: "PolicyListItem" });
23449
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23450
+ var PolicyDetail = external_exports.object({
23451
+ specVersion: external_exports.literal(1),
23452
+ id: external_exports.string(),
23453
+ kind: PolicyKind,
23454
+ name: external_exports.string(),
23455
+ enabled: external_exports.boolean(),
23456
+ description: external_exports.string(),
23457
+ usedBy: external_exports.array(UsedByItem)
23458
+ }).meta({ id: "PolicyDetail" });
23459
+ var PolicyStatsResponse = external_exports.object({
23460
+ policies: external_exports.number().int().nonnegative(),
23461
+ builtin: external_exports.number().int().nonnegative(),
23462
+ custom: external_exports.number().int().nonnegative(),
23463
+ detectionsGoverned: external_exports.number().int().nonnegative()
23464
+ }).meta({ id: "PolicyStatsResponse" });
23465
+
23167
23466
  // ../../packages/schema/src/zod/vault.ts
23168
23467
  var POINTER_FORMAT_VERSION = 2;
23169
23468
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23201,6 +23500,14 @@ var VaultEntry = external_exports.object({
23201
23500
  // How many times this value has been detected on this machine — the reuse
23202
23501
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23203
23502
  occurrenceCount: external_exports.number().int().nonnegative(),
23503
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23504
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23505
+ // one row however many paths vault it, so this is what tells a policy sweep
23506
+ // that the row carries somebody's own instruction and not just an assignment
23507
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23508
+ // vaulting of the same value must never clear it — what the user said about
23509
+ // the value does not expire.
23510
+ userAuthorized: external_exports.boolean(),
23204
23511
  firstSeen: external_exports.string(),
23205
23512
  lastSeen: external_exports.string()
23206
23513
  });
@@ -23319,9 +23626,9 @@ var VaultConsent = external_exports.object({
23319
23626
  });
23320
23627
 
23321
23628
  // ../../packages/schema/src/zod/local.ts
23322
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23629
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23323
23630
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23324
- var HISTORY_SYNC_PAYLOAD_VERSION = 1;
23631
+ var HISTORY_SYNC_PAYLOAD_VERSION = 2;
23325
23632
  var RunMode = external_exports.enum(["standalone", "attached"]);
23326
23633
  var ControlPlaneConnection = external_exports.object({
23327
23634
  endpoint: external_exports.string().min(1),
@@ -23366,6 +23673,19 @@ var WorkspaceSettings = external_exports.object({
23366
23673
  vaultKeyCustody: VaultKeyCustody.default("file"),
23367
23674
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23368
23675
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23676
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23677
+ // place. Not a handling policy: the policy has already resolved to redact,
23678
+ // and this only says what happens when the host offers no channel to carry it
23679
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23680
+ // Claude Code decline to mask a field that EXECUTES because masking would
23681
+ // change what runs. Per FIELD rather than per host, so a host that can
23682
+ // rewrite some inputs keeps true redaction on those.
23683
+ //
23684
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23685
+ // an attached machine's merge is `strongerAction` over the one action ladder
23686
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23687
+ // word and stays out of the stored value.
23688
+ redactFallback: RedactFallback.default("warn"),
23369
23689
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23370
23690
  onboardedAt: external_exports.iso.datetime().optional(),
23371
23691
  // Records that the user consented to sending findings to the model API for
@@ -23373,10 +23693,12 @@ var WorkspaceSettings = external_exports.object({
23373
23693
  // Absent until granted; a stale payloadVersion means the consent no longer
23374
23694
  // covers the current payload and must be re-granted.
23375
23695
  modelJudgeConsent: ModelJudgeConsent.optional(),
23376
- // Records that the user consented to sending the activity already recorded on
23377
- // this machine to the deployment it is attached to, along with the payload
23378
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23379
- // a different endpoint or an older payload no longer counts.
23696
+ // Records that the user consented to the DEFERRED send — the outbox along
23697
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23698
+ // that covers both the pre-attach backlog and undelivered captures (which
23699
+ // carry prompt/reply text in `content`); the key name predates the widening.
23700
+ // Absent until granted, and a grant for a different endpoint or an older
23701
+ // payload no longer counts.
23380
23702
  historySyncConsent: HistorySyncConsent.optional()
23381
23703
  });
23382
23704
  function defaultWorkspaceSettings() {
@@ -23504,7 +23826,8 @@ var ManagedSettingKey = external_exports.enum([
23504
23826
  "vaultKeyCustody",
23505
23827
  "vaultInlineReveal",
23506
23828
  "modelJudgeConsent",
23507
- "dataSharesInPlace"
23829
+ "dataSharesInPlace",
23830
+ "redactFallback"
23508
23831
  ]).meta({ id: "ManagedSettingKey" });
23509
23832
  var ManagedSettingsValues = external_exports.object({
23510
23833
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
@@ -23517,7 +23840,8 @@ var ManagedSettingsValues = external_exports.object({
23517
23840
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23518
23841
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23519
23842
  modelJudgeConsent: external_exports.boolean().optional(),
23520
- dataSharesInPlace: external_exports.boolean().optional()
23843
+ dataSharesInPlace: external_exports.boolean().optional(),
23844
+ redactFallback: RedactFallback.optional()
23521
23845
  }).meta({ id: "ManagedSettingsValues" });
23522
23846
  var ManagedSettings = external_exports.object({
23523
23847
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23532,186 +23856,6 @@ var ManagedSettings = external_exports.object({
23532
23856
  lockedFields: external_exports.array(ManagedSettingKey).default([])
23533
23857
  }).meta({ id: "ManagedSettings" });
23534
23858
 
23535
- // ../../packages/schema/src/zod/policy.ts
23536
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23537
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23538
- var Policy = external_exports.object({
23539
- id: external_exports.guid(),
23540
- scope: PolicyScope,
23541
- target: PolicyTarget,
23542
- action: ActionTaken,
23543
- enabled: external_exports.boolean().default(true),
23544
- customKeywords: external_exports.array(external_exports.string()).optional(),
23545
- // Display name — optional so older policy rows without name still parse.
23546
- // Added for the findings API (policy.name column migration).
23547
- name: external_exports.string().optional()
23548
- }).meta({ id: "Policy" });
23549
- var PolicyBundle = external_exports.object({
23550
- version: external_exports.string(),
23551
- policies: external_exports.array(Policy),
23552
- // Rules from the installed marketplace packs (snapshotted by the
23553
- // control plane). The plugin registers these in addition to its bundled
23554
- // packs. Optional so older backends — and older on-disk caches — that omit
23555
- // the field still parse; consumers read `bundle.rules ?? []`.
23556
- rules: external_exports.array(Rule).optional(),
23557
- // When true, `rules` IS the complete effective ruleset and the runtime must
23558
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23559
- // after reading the user's installed snapshot (installed_packs, enabled
23560
- // packs only), which is how detection updates stay manual: new bundled
23561
- // rules run only after the user applies the pack update. Absent/false keeps
23562
- // the historical composition (bundled packs + rules) — older caches.
23563
- rulesComplete: external_exports.boolean().optional(),
23564
- // Active detection exceptions, evaluation subset only (see
23565
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23566
- // on-disk caches — that omit the field still parse; consumers read
23567
- // `bundle.exceptions ?? []`.
23568
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23569
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23570
- // A second axis over the same `redact` action, carried beside the policies
23571
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23572
- // widening Policy itself would change a persisted shape to express something
23573
- // only the in-memory bundle needs. Optional so an older producer — or an
23574
- // older on-disk cache — still parses; consumers read `?? []` and get the
23575
- // pre-existing one-way behaviour, which is the safe direction to default.
23576
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23577
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23578
- // from a versioned installed pack. Optional so older backends — and older
23579
- // on-disk caches — that omit the field still parse; consumers fall back to
23580
- // the rule's own spec version. NOT the bundle version above — see
23581
- // installedRuleset's ruleVersions for the source of truth.
23582
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23583
- // Model ids (the raw `model` string a harness reports, e.g.
23584
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23585
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23586
- // one (UserPromptSubmit). Optional so an older backend — and an older
23587
- // on-disk cache — still parses; consumers read `?? []`, which is the
23588
- // unenforced behaviour that predates this field and the safe direction to
23589
- // default.
23590
- //
23591
- // Ids, not display names: the governance decision is keyed on the exact
23592
- // string the harness reports (`model_status_override.versionId` in the
23593
- // control plane), so no name resolution stands between the decision and the
23594
- // comparison.
23595
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23596
- customKeywords: external_exports.array(external_exports.string()),
23597
- fetchedAt: external_exports.iso.datetime()
23598
- }).meta({ id: "PolicyBundle" });
23599
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23600
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23601
- var CATEGORY_PEAK_SEVERITY = {
23602
- secret: "critical",
23603
- financial: "critical",
23604
- // core-financial/credit-card
23605
- code_flaw: "critical",
23606
- pii: "high",
23607
- phi: "high",
23608
- custom: "high",
23609
- // user-defined; conservative
23610
- code_context: "low",
23611
- config: "low"
23612
- // observe-only; floors to monitor regardless
23613
- };
23614
- function severityFloorPolicy(category) {
23615
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23616
- const peak = CATEGORY_PEAK_SEVERITY[category];
23617
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23618
- }
23619
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23620
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23621
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23622
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23623
- var BUILTIN_POLICY_SPECS = {
23624
- monitor: {
23625
- name: "Monitor",
23626
- action: "log",
23627
- reversible: false,
23628
- description: "Log every match for audit. The request is allowed through untouched."
23629
- },
23630
- warn: {
23631
- name: "Warn",
23632
- action: "warn",
23633
- reversible: false,
23634
- description: "Allow the request, but warn the user inline before it is sent."
23635
- },
23636
- redact: {
23637
- name: "Redact",
23638
- action: "redact",
23639
- reversible: false,
23640
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23641
- },
23642
- vault: {
23643
- name: "Redact & Vault",
23644
- action: "redact",
23645
- reversible: true,
23646
- 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."
23647
- },
23648
- block: {
23649
- name: "Block",
23650
- action: "block",
23651
- reversible: false,
23652
- description: "Refuse the request entirely whenever any rule in this detection matches."
23653
- }
23654
- };
23655
- function builtinPolicyToAction(id) {
23656
- return BUILTIN_POLICY_SPECS[id].action;
23657
- }
23658
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23659
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23660
- );
23661
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23662
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23663
- );
23664
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23665
- function builtinPolicyIsReversible(id) {
23666
- return BUILTIN_POLICY_SPECS[id].reversible;
23667
- }
23668
- function policyIdIsReversible(policyId) {
23669
- const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23670
- const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23671
- return builtinPolicyIsReversible(id);
23672
- }
23673
- var DEFAULT_ACTIONS = Object.fromEntries(
23674
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23675
- );
23676
- var BUILTIN_POLICIES = Object.fromEntries(
23677
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23678
- );
23679
- var DEFAULT_PACK_POLICY_ID = "monitor";
23680
- function policyIdToAction(policyId) {
23681
- const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23682
- const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23683
- return BUILTIN_POLICIES[id].action;
23684
- }
23685
- var UsedByItem = external_exports.object({
23686
- id: external_exports.string(),
23687
- name: external_exports.string(),
23688
- ruleCount: external_exports.number().int().nonnegative(),
23689
- enabled: external_exports.boolean()
23690
- }).meta({ id: "UsedByItem" });
23691
- var PolicyListItem = external_exports.object({
23692
- id: external_exports.string(),
23693
- kind: PolicyKind,
23694
- name: external_exports.string(),
23695
- enabled: external_exports.boolean(),
23696
- usedByCount: external_exports.number().int().nonnegative()
23697
- }).meta({ id: "PolicyListItem" });
23698
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23699
- var PolicyDetail = external_exports.object({
23700
- specVersion: external_exports.literal(1),
23701
- id: external_exports.string(),
23702
- kind: PolicyKind,
23703
- name: external_exports.string(),
23704
- enabled: external_exports.boolean(),
23705
- description: external_exports.string(),
23706
- usedBy: external_exports.array(UsedByItem)
23707
- }).meta({ id: "PolicyDetail" });
23708
- var PolicyStatsResponse = external_exports.object({
23709
- policies: external_exports.number().int().nonnegative(),
23710
- builtin: external_exports.number().int().nonnegative(),
23711
- custom: external_exports.number().int().nonnegative(),
23712
- detectionsGoverned: external_exports.number().int().nonnegative()
23713
- }).meta({ id: "PolicyStatsResponse" });
23714
-
23715
23859
  // ../../packages/schema/src/zod/project-files.ts
23716
23860
  var ProjectFileInput = external_exports.object({
23717
23861
  path: external_exports.string().min(1),
@@ -23957,10 +24101,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23957
24101
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23958
24102
 
23959
24103
  // ../../packages/schema/src/zod/settings-action.ts
24104
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24105
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23960
24106
  var SaveSettingsInput = external_exports.object({
23961
24107
  historicalAccess: external_exports.string(),
23962
- modelJudgeConsent: external_exports.boolean(),
23963
- historySyncConsent: external_exports.boolean(),
24108
+ modelJudgeConsent: ModelJudgeConsentChoice,
24109
+ historySyncConsent: HistorySyncConsentChoice,
23964
24110
  vaultConsent: external_exports.string(),
23965
24111
  vaultInlineReveal: external_exports.string()
23966
24112
  });
@@ -24110,9 +24256,9 @@ function deriveReviewReasons(trust, transports) {
24110
24256
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24111
24257
  return reasons;
24112
24258
  }
24113
- function buildReviewInfo(trust, transports) {
24259
+ function buildReviewInfo(trust, transports, decided) {
24114
24260
  const reasons = deriveReviewReasons(trust, transports);
24115
- return { needsReview: reasons.length > 0, reasons };
24261
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24116
24262
  }
24117
24263
  function distinctTransports(transports) {
24118
24264
  return Array.from(new Set(transports));
@@ -24269,8 +24415,8 @@ function readControlPlaneCredentialFile(settingsDir2, connection) {
24269
24415
  }
24270
24416
 
24271
24417
  // ../../packages/persistence/src/database.ts
24272
- import { randomUUID as randomUUID10 } from "crypto";
24273
- import { join as join4, sep } from "path";
24418
+ import { randomUUID as randomUUID11 } from "crypto";
24419
+ import { dirname as dirname2, join as join7, sep } from "path";
24274
24420
  import { DatabaseSync } from "node:sqlite";
24275
24421
 
24276
24422
  // ../../packages/persistence/src/ids.ts
@@ -24303,6 +24449,16 @@ function captureId(sessionId, contentHash, filePath = null) {
24303
24449
  canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
24304
24450
  );
24305
24451
  }
24452
+ function uuidFromDigest(digest) {
24453
+ const h = digest.slice(0, 32);
24454
+ const version2 = "8";
24455
+ const variant = (Number.parseInt(h.charAt(16), 16) & 3 | 8).toString(16);
24456
+ const b = h.slice(0, 12) + version2 + h.slice(13, 16) + variant + h.slice(17, 32);
24457
+ return `${b.slice(0, 8)}-${b.slice(8, 12)}-${b.slice(12, 16)}-${b.slice(16, 20)}-${b.slice(20, 32)}`;
24458
+ }
24459
+ function captureWireId(sessionId, contentHash, filePath = null) {
24460
+ return uuidFromDigest(captureId(sessionId, contentHash, filePath));
24461
+ }
24306
24462
 
24307
24463
  // ../../packages/persistence/src/internal/snapshot.ts
24308
24464
  import { randomUUID } from "crypto";
@@ -24514,6 +24670,10 @@ function allRows(stmt, params) {
24514
24670
  if (Array.isArray(params)) return stmt.all(...params);
24515
24671
  return stmt.all(params);
24516
24672
  }
24673
+ function* iterateRows(stmt, params) {
24674
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24675
+ for (const row of rows) yield row;
24676
+ }
24517
24677
  function getRow(stmt, params) {
24518
24678
  if (params === void 0) return stmt.get();
24519
24679
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24982,10 +25142,17 @@ function ensureSyncedAtColumn(db, table) {
24982
25142
  if (!columns.includes("sync_claimed_at")) {
24983
25143
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
24984
25144
  }
25145
+ if (!columns.includes("outbox_owed")) {
25146
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25147
+ }
24985
25148
  db.exec(
24986
25149
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24987
25150
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24988
25151
  );
25152
+ db.exec(
25153
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25154
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25155
+ );
24989
25156
  db.exec(
24990
25157
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24991
25158
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25090,7 +25257,6 @@ function decodeKeysetCursor(cursor) {
25090
25257
  // ../../packages/persistence/src/repositories/activity.ts
25091
25258
  var DAY_MS = 864e5;
25092
25259
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25093
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25094
25260
  function defaultTimeZone() {
25095
25261
  try {
25096
25262
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25145,6 +25311,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25145
25311
  error: "error",
25146
25312
  active: "active"
25147
25313
  };
25314
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25148
25315
  function safeParseStringArray(raw) {
25149
25316
  if (!raw) return [];
25150
25317
  const parsed2 = safeJson(raw, null);
@@ -25218,6 +25385,37 @@ var TIMELINE_COLUMNS = `
25218
25385
  json_extract(attributes, '$.targetId') AS target_id,
25219
25386
  json_extract(attributes, '$.internal') AS internal,
25220
25387
  json_extract(attributes, '$.flagged') AS flagged`;
25388
+ var LLM_USAGE_SELECT = `
25389
+ SELECT root_session_id AS sessionId,
25390
+ provider,
25391
+ model,
25392
+ service_tier AS serviceTier,
25393
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25394
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25395
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25396
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25397
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25398
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25399
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25400
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25401
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25402
+ function usageLeaves(rows) {
25403
+ return rows.map((row) => {
25404
+ const attributes = {
25405
+ input_tokens: row.inputTokens,
25406
+ output_tokens: row.outputTokens,
25407
+ cache_creation_input_tokens: row.cacheCreationTokens,
25408
+ cache_read_input_tokens: row.cacheReadTokens,
25409
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25410
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25411
+ web_search_requests: row.webSearchRequests
25412
+ };
25413
+ if (row.provider !== null) attributes.provider = row.provider;
25414
+ if (row.model !== null) attributes.model = row.model;
25415
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25416
+ return { sessionId: row.sessionId, attributes };
25417
+ });
25418
+ }
25221
25419
  var SESSION_ROOT = `event_type = 'session'`;
25222
25420
  var HAS_ACTIVITY = `EXISTS (
25223
25421
  SELECT 1 FROM audit_events c
@@ -25243,16 +25441,17 @@ var SqliteActivityRepository = class {
25243
25441
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25244
25442
  const liveNow = countScalar(
25245
25443
  this.db,
25246
- `SELECT count(*) AS n FROM audit_events s
25444
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25247
25445
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25248
- AND max(
25249
- s.started_at,
25250
- coalesce(
25251
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25252
- s.started_at
25253
- )
25254
- ) >= ?`,
25255
- [liveThreshold]
25446
+ AND s.id IN (
25447
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25448
+ UNION
25449
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25450
+ WHERE started_at >= ?
25451
+ UNION
25452
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25453
+ WHERE ended_at >= ?)`,
25454
+ [liveThreshold, liveThreshold, liveThreshold]
25256
25455
  );
25257
25456
  const toolCallsToday = countScalar(
25258
25457
  this.db,
@@ -25382,7 +25581,7 @@ var SqliteActivityRepository = class {
25382
25581
  this.db.prepare(
25383
25582
  `SELECT ${TIMELINE_COLUMNS}
25384
25583
  FROM audit_events
25385
- WHERE id = ? OR root_session_id = ?
25584
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25386
25585
  ORDER BY started_at ASC, id ASC`
25387
25586
  ),
25388
25587
  [sessionId, sessionId]
@@ -25395,14 +25594,14 @@ var SqliteActivityRepository = class {
25395
25594
  coalesce(sum(output_tokens), 0) AS output,
25396
25595
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25397
25596
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25398
- FROM audit_events
25597
+ FROM audit_events INDEXED BY idx_audit_session_type
25399
25598
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25400
25599
  ),
25401
25600
  [sessionId]
25402
25601
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25403
25602
  const primaryModel = getRow(
25404
25603
  this.db.prepare(
25405
- `SELECT model, provider FROM audit_events
25604
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25406
25605
  WHERE root_session_id = ? AND event_type = 'llm_call'
25407
25606
  ORDER BY started_at ASC, id ASC
25408
25607
  LIMIT 1`
@@ -25413,7 +25612,7 @@ var SqliteActivityRepository = class {
25413
25612
  this.db.prepare(
25414
25613
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25415
25614
  count(*) AS n
25416
- FROM audit_events
25615
+ FROM audit_events INDEXED BY idx_audit_session
25417
25616
  WHERE root_session_id = ? AND event_type = 'tool_call'
25418
25617
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25419
25618
  ),
@@ -25421,7 +25620,7 @@ var SqliteActivityRepository = class {
25421
25620
  );
25422
25621
  const modelRows = allRows(
25423
25622
  this.db.prepare(
25424
- `SELECT DISTINCT model FROM audit_events
25623
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25425
25624
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25426
25625
  ORDER BY model`
25427
25626
  ),
@@ -25430,7 +25629,7 @@ var SqliteActivityRepository = class {
25430
25629
  const derivedModels = modelRows.map((r) => r.model);
25431
25630
  const commits = countScalar(
25432
25631
  this.db,
25433
- `SELECT count(*) AS n FROM audit_events
25632
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25434
25633
  WHERE root_session_id = ? AND event_type = 'commit'`,
25435
25634
  [sessionId]
25436
25635
  );
@@ -25466,25 +25665,57 @@ var SqliteActivityRepository = class {
25466
25665
  return Promise.resolve(session);
25467
25666
  }
25468
25667
  /**
25469
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25470
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25471
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25472
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25473
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25474
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25668
+ * Cross-session token report — every `llm_call` in the store (or in a
25669
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25670
+ * session, with USD cost DERIVED at read time via the shared
25671
+ * `defaultCostModel` (never stored). The caller collapses these onto
25672
+ * per-model rows with `aggregateTokenUsage`.
25673
+ *
25674
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25675
+ * the members the rollup sums — and priced once per group, which is exact
25676
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25677
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25678
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25679
+ * the index stores the values once, at write, and answers the same window in
25680
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25681
+ * planner prefers the general event-type index and fetches every row to
25682
+ * recompute the columns it could have read. The index is one every open
25683
+ * store carries, since opening runs the migrations, so the hard requirement
25684
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25685
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25686
+ * per call, no bag parsed.
25475
25687
  */
25476
25688
  tokenReports(fromMs) {
25477
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25478
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25689
+ const rows = allRows(
25690
+ this.db.prepare(
25691
+ `${LLM_USAGE_SELECT}
25692
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25693
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25694
+ ${LLM_USAGE_GROUP}`
25695
+ ),
25696
+ fromMs === void 0 ? void 0 : [fromMs]
25697
+ );
25698
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25479
25699
  }
25480
25700
  /**
25481
- * One session's token report — its `llm_call` leaves grouped per (provider,
25482
- * model) with derived cost, or `null` when the session made no `llm_call`s
25483
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25484
- * breakdown + estimated cost.
25701
+ * One session's token report — its `llm_call`s grouped per (provider,
25702
+ * model, tier) with derived cost, or `null` when the session made no
25703
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25704
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25705
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25706
+ * it replaces walked every `llm_call` in the store to find one session's.
25485
25707
  */
25486
25708
  tokenReportForSession(sessionId) {
25487
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25709
+ const rows = allRows(
25710
+ this.db.prepare(
25711
+ `${LLM_USAGE_SELECT}
25712
+ FROM audit_events
25713
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25714
+ ${LLM_USAGE_GROUP}`
25715
+ ),
25716
+ [sessionId]
25717
+ );
25718
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25488
25719
  return Promise.resolve(reports[0] ?? null);
25489
25720
  }
25490
25721
  /**
@@ -25508,42 +25739,6 @@ var SqliteActivityRepository = class {
25508
25739
  for (const row of rows) seen.add(toHarness(row.harness));
25509
25740
  return Promise.resolve([...seen]);
25510
25741
  }
25511
- /**
25512
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25513
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25514
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25515
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25516
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25517
- */
25518
- readLlmCallLeaves(opts = {}) {
25519
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25520
- const params = [];
25521
- if (opts.sessionId !== void 0) {
25522
- conditions.push("root_session_id = ?");
25523
- params.push(opts.sessionId);
25524
- }
25525
- if (opts.fromMs !== void 0) {
25526
- conditions.push("started_at >= ?");
25527
- params.push(opts.fromMs);
25528
- }
25529
- const rows = allRows(
25530
- this.db.prepare(
25531
- `SELECT root_session_id AS sessionId, attributes
25532
- FROM audit_events
25533
- WHERE ${conditions.join(" AND ")}`
25534
- ),
25535
- params
25536
- );
25537
- return mapRowsTolerant(
25538
- rows.filter(
25539
- (row) => row.sessionId !== null
25540
- ),
25541
- (row) => ({
25542
- sessionId: row.sessionId,
25543
- attributes: JSON.parse(row.attributes)
25544
- })
25545
- );
25546
- }
25547
25742
  /**
25548
25743
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25549
25744
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25558,20 +25753,23 @@ var SqliteActivityRepository = class {
25558
25753
  const inClause = placeholders(sessionIds.length);
25559
25754
  const lastActivityRows = allRows(
25560
25755
  this.db.prepare(
25561
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25562
- WHERE root_session_id IN (${inClause})
25563
- GROUP BY root_session_id`
25756
+ `SELECT ids.value AS id,
25757
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25758
+ (SELECT max(ended_at) FROM audit_events e
25759
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25760
+ FROM json_each(?) AS ids`
25564
25761
  ),
25565
- sessionIds
25762
+ [JSON.stringify(sessionIds)]
25566
25763
  );
25567
25764
  for (const row of lastActivityRows) {
25568
- if (row.id === null) continue;
25569
25765
  const entry = result.get(row.id);
25570
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25766
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25767
+ if (entry && last > 0) entry.lastActivityMs = last;
25571
25768
  }
25572
25769
  const turnsRows = allRows(
25573
25770
  this.db.prepare(
25574
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25771
+ `SELECT root_session_id AS id, count(*) AS n
25772
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25575
25773
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25576
25774
  GROUP BY root_session_id`
25577
25775
  ),
@@ -25586,7 +25784,7 @@ var SqliteActivityRepository = class {
25586
25784
  this.db.prepare(
25587
25785
  `SELECT root_session_id AS id,
25588
25786
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25589
- FROM audit_events
25787
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25590
25788
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25591
25789
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25592
25790
  GROUP BY root_session_id`
@@ -25616,7 +25814,7 @@ var SqliteActivityRepository = class {
25616
25814
  this.db.prepare(
25617
25815
  `SELECT root_session_id AS id,
25618
25816
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25619
- FROM audit_events
25817
+ FROM audit_events INDEXED BY idx_audit_session_share
25620
25818
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25621
25819
  GROUP BY root_session_id`
25622
25820
  ),
@@ -26645,7 +26843,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26645
26843
 
26646
26844
  // ../../packages/persistence/src/repositories/findings.ts
26647
26845
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26648
- var SCAN_BATCH_ROWS = 1e3;
26649
26846
  var DEFAULT_LOCATIONS_LIMIT = 100;
26650
26847
  var LOCATION_RULE_IDS_CAP = 20;
26651
26848
  function compareLocationOrder(a, b) {
@@ -26674,6 +26871,25 @@ function deriveInstanceStatus(row) {
26674
26871
  latestResolutionStatus: row.latest_status
26675
26872
  });
26676
26873
  }
26874
+ function toFlatFindingRow(r) {
26875
+ return {
26876
+ id: r.id,
26877
+ ruleId: r.rule_id,
26878
+ category: r.category,
26879
+ severity: r.severity,
26880
+ maskedMatch: r.masked_match,
26881
+ actionTaken: r.action_taken,
26882
+ confidence: r.confidence,
26883
+ occurredAt: epochMillisToIso(r.occurred_at),
26884
+ sourceTool: r.source_tool,
26885
+ repo: r.repo ?? "",
26886
+ file: r.file ?? "",
26887
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26888
+ eventId: r.event_id,
26889
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26890
+ status: deriveInstanceStatus(r)
26891
+ };
26892
+ }
26677
26893
  function encodeGroupCursor(group) {
26678
26894
  const payload = {
26679
26895
  sev: group.severity,
@@ -26749,7 +26965,7 @@ var SqliteFindingsRepository = class {
26749
26965
  this.db.prepare(
26750
26966
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26751
26967
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26752
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26968
+ e.source_tool AS source_tool,
26753
26969
  e.event_type AS kind
26754
26970
  FROM audit_events e
26755
26971
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26857,56 +27073,11 @@ var SqliteFindingsRepository = class {
26857
27073
  predicate,
26858
27074
  params: sessionParams
26859
27075
  });
26860
- const rows = allRows(
26861
- this.db.prepare(
26862
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26863
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26864
- kind, finding_key, latest_status
26865
- FROM (
26866
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26867
- d.severity AS severity, f.masked_match AS masked_match,
26868
- f.action_taken AS action_taken, f.confidence AS confidence,
26869
- e.started_at AS occurred_at,
26870
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26871
- json_extract(e.attributes, '$.repo') AS repo,
26872
- json_extract(e.attributes, '$.file_path') AS file,
26873
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26874
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26875
- e.event_type AS kind, f.finding_key AS finding_key,
26876
- latest.status AS latest_status,
26877
- ROW_NUMBER() OVER (
26878
- PARTITION BY d.rule_id
26879
- ORDER BY e.started_at DESC, f.id DESC
26880
- ) AS rn
26881
- FROM inspection_findings f
26882
- JOIN audit_events e ON e.id = f.audit_event_id
26883
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26884
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26885
- ON latest.finding_key = f.finding_key
26886
- ${predicate}
26887
- )
26888
- WHERE rn <= :cap
26889
- ORDER BY occurred_at DESC, id DESC`
26890
- ),
26891
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26892
- );
26893
- const groupable = rows.map((r) => ({
26894
- id: r.id,
26895
- ruleId: r.rule_id,
26896
- category: r.category,
26897
- severity: r.severity,
26898
- maskedMatch: r.masked_match,
26899
- actionTaken: r.action_taken,
26900
- confidence: r.confidence,
26901
- occurredAt: epochMillisToIso(r.occurred_at),
26902
- sourceTool: r.source_tool,
26903
- repo: r.repo ?? "",
26904
- file: r.file ?? "",
26905
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26906
- eventId: r.event_id,
26907
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26908
- status: deriveInstanceStatus(r)
26909
- }));
27076
+ const rows = this.previewRows(aggregates, {
27077
+ sessionId: query.sessionId,
27078
+ from: query.from
27079
+ });
27080
+ const groupable = rows.map(toFlatFindingRow);
26910
27081
  const allGroups = buildFindingGroups(groupable, { aggregates });
26911
27082
  const filterOpts = {
26912
27083
  severity: query.severity,
@@ -26992,8 +27163,10 @@ var SqliteFindingsRepository = class {
26992
27163
  *
26993
27164
  * The scan runs from the top of the scope on every request, not from the
26994
27165
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26995
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26996
- * while the counting runs, and only the page itself is retained.
27166
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27167
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27168
+ * counting runs — a generator streaming the index order, not a sequence of
27169
+ * fetched batches; only the page itself is retained.
26997
27170
  */
26998
27171
  listFindingInstances(query) {
26999
27172
  const opts = {
@@ -27009,6 +27182,10 @@ var SqliteFindingsRepository = class {
27009
27182
  };
27010
27183
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27011
27184
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27185
+ const isPastCursor = cursor === null ? () => true : (row) => {
27186
+ const rowMs = isoToEpochMillis(row.occurredAt);
27187
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27188
+ };
27012
27189
  const accumulator = createInstanceFacetAccumulator(opts);
27013
27190
  const items = [];
27014
27191
  let total = 0;
@@ -27021,6 +27198,7 @@ var SqliteFindingsRepository = class {
27021
27198
  accumulator.add(row);
27022
27199
  if (!matchesInstanceFilters(row, opts)) continue;
27023
27200
  total += 1;
27201
+ if (!isPastCursor(row)) continue;
27024
27202
  if (items.length < limit) {
27025
27203
  items.push(toInstanceDetail(row));
27026
27204
  last = row;
@@ -27029,15 +27207,6 @@ var SqliteFindingsRepository = class {
27029
27207
  }
27030
27208
  }
27031
27209
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27032
- if (cursor !== null) {
27033
- const resumed = this.pageAfter(cursor, opts, limit, query);
27034
- return Promise.resolve({
27035
- totals: { findings: total },
27036
- facets: accumulator.facets(),
27037
- items: resumed.items,
27038
- nextCursor: resumed.nextCursor
27039
- });
27040
- }
27041
27210
  return Promise.resolve({
27042
27211
  totals: { findings: total },
27043
27212
  facets: accumulator.facets(),
@@ -27045,35 +27214,6 @@ var SqliteFindingsRepository = class {
27045
27214
  nextCursor
27046
27215
  });
27047
27216
  }
27048
- /**
27049
- * The page of matching rows strictly after `cursor`. Separate from the
27050
- * counting pass because that one starts at the top of the scope by design;
27051
- * this one narrows the scan with the same keyset predicate the activity list
27052
- * uses, so a later page costs less than the first rather than more.
27053
- */
27054
- pageAfter(cursor, opts, limit, query) {
27055
- const items = [];
27056
- let last;
27057
- let hasMore = false;
27058
- for (const row of this.scanFindingRows({
27059
- sessionId: query.sessionId,
27060
- from: query.from,
27061
- after: cursor
27062
- })) {
27063
- if (!matchesInstanceFilters(row, opts)) continue;
27064
- if (items.length < limit) {
27065
- items.push(toInstanceDetail(row));
27066
- last = row;
27067
- } else {
27068
- hasMore = true;
27069
- break;
27070
- }
27071
- }
27072
- return {
27073
- items,
27074
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27075
- };
27076
- }
27077
27217
  /**
27078
27218
  * The same findings folded by location: repository, then file within it.
27079
27219
  *
@@ -27156,25 +27296,111 @@ var SqliteFindingsRepository = class {
27156
27296
  });
27157
27297
  }
27158
27298
  /**
27159
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27299
+ * Each group's newest instances, for the table's expanded rows.
27300
+ *
27301
+ * ONE index-ordered scan with early termination, and the shape is the point.
27302
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27303
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27304
+ * through a temp B-tree to keep a bounded preview of each group, and then
27305
+ * sorts the survivors again for the page order. Both sorts grow with the
27306
+ * store while the answer does not.
27307
+ *
27308
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27309
+ * (or the session or window index the scope names — see `findingScanSql`),
27310
+ * which is already the order the page wants, and keeps rows per rule until
27311
+ * each rule has as many as it can show. The aggregate the caller already holds
27312
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27313
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27314
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27315
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27316
+ * store with many firing rules widens it. The bound that DOES hold
27317
+ * unconditionally is the sorted form's floor: this scan visits at most as
27318
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27319
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27320
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27321
+ * wanted instances sitting at the tail of the scope — is one pass over
27322
+ * everything in scope with a block sort of the id tie-break only, never a
27323
+ * sort of the scope, which is still that floor.
27324
+ *
27325
+ * A row whose rule the aggregate did not see is skipped: the two statements
27326
+ * run without a shared snapshot, so a capture landing between them can add a
27327
+ * rule here that has no counts there, and the counts are what the group is
27328
+ * built from.
27329
+ */
27330
+ previewRows(aggregates, scope) {
27331
+ const wanted = /* @__PURE__ */ new Map();
27332
+ let remaining = 0;
27333
+ for (const [ruleId, agg] of aggregates) {
27334
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27335
+ wanted.set(ruleId, n);
27336
+ remaining += n;
27337
+ }
27338
+ const rows = [];
27339
+ if (remaining === 0) return rows;
27340
+ const { sql, params } = this.findingScanSql(scope);
27341
+ const taken = /* @__PURE__ */ new Map();
27342
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27343
+ const want = wanted.get(r.rule_id);
27344
+ if (want === void 0) continue;
27345
+ const have = taken.get(r.rule_id) ?? 0;
27346
+ if (have >= want) continue;
27347
+ taken.set(r.rule_id, have + 1);
27348
+ rows.push(r);
27349
+ remaining -= 1;
27350
+ if (remaining === 0) break;
27351
+ }
27352
+ return rows;
27353
+ }
27354
+ /**
27355
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27160
27356
  *
27161
27357
  * A generator so a caller streams the scope without it ever being an array:
27162
27358
  * the flat list counts and facets the whole filtered scope, which on a large
27163
- * store is far more rows than any page. Each batch advances the same keyset
27164
- * predicate the page read uses, so the scan is a sequence of bounded reads
27165
- * rather than one unbounded result set.
27359
+ * store is far more rows than any page. The rows come off ONE statement,
27360
+ * iterated rather than materialized, in the index order `findingScanSql`
27361
+ * arranges so the scan is a single pass with a block sort of the id
27362
+ * tie-break only, never a sort of the scope, where a sequence of
27363
+ * keyset-bounded batches re-sorted everything below the cursor on every
27364
+ * batch and cost the square of the scope.
27166
27365
  *
27167
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27168
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27169
- * makes it a point lookup per row, and the derived table would re-materialize
27170
- * a window over the whole resolution table once per batch.
27171
- *
27172
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27173
- * would be missing from its own facet, which is computed by excluding that
27174
- * dimension — see listFindingInstances.
27366
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27367
+ * dimension narrowed here would be missing from its own facet, which is
27368
+ * computed by excluding that dimension (see listFindingInstances). There is
27369
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27370
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27371
+ * narrower statement, since the counting pass already visits every row a
27372
+ * page-2+ request would otherwise re-seek for.
27175
27373
  */
27176
27374
  *scanFindingRows(scope) {
27177
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27375
+ const { sql, params } = this.findingScanSql(scope);
27376
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27377
+ yield toFlatFindingRow(r);
27378
+ }
27379
+ }
27380
+ /**
27381
+ * The one statement both instance-level scans run: every finding in scope,
27382
+ * joined to its event and definition, newest first.
27383
+ *
27384
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27385
+ * the same two `recentFindings` documents at length, for the same reason:
27386
+ *
27387
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27388
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27389
+ * yields `started_at` order per event type, not across the four, so
27390
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27391
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27392
+ * `idx_audit_session` for a session scope, which is also `started_at`
27393
+ * ordered within the session — and the order falls out of the index.
27394
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27395
+ * JOINs the planner drives from the findings and sorts everything.
27396
+ *
27397
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27398
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27399
+ * index probe per keyed row, and a derived table over the whole resolution
27400
+ * table would be materialized before the first row streamed.
27401
+ */
27402
+ findingScanSql(scope) {
27403
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27178
27404
  const params = [];
27179
27405
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27180
27406
  conditions.push("e.root_session_id = ?");
@@ -27188,58 +27414,24 @@ var SqliteFindingsRepository = class {
27188
27414
  d.severity AS severity, f.masked_match AS masked_match,
27189
27415
  f.action_taken AS action_taken, f.confidence AS confidence,
27190
27416
  e.started_at AS occurred_at,
27191
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27192
- json_extract(e.attributes, '$.repo') AS repo,
27193
- json_extract(e.attributes, '$.file_path') AS file,
27194
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27417
+ e.source_tool AS source_tool,
27418
+ e.repo AS repo,
27419
+ e.file_path AS file,
27420
+ e.tool_name AS tool_name,
27195
27421
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27196
27422
  e.event_type AS kind, f.finding_key AS finding_key,
27197
27423
  ${latestResolutionStatusSql("f")} AS latest_status
27198
- FROM inspection_findings f
27199
- JOIN audit_events e ON e.id = f.audit_event_id
27200
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27424
+ FROM audit_events e
27425
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27426
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27201
27427
  WHERE ${conditions.join(" AND ")}
27202
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27203
- ORDER BY e.started_at DESC, f.id DESC
27204
- LIMIT ?`;
27205
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27206
- for (; ; ) {
27207
- const rows = allRows(this.db.prepare(sql), [
27208
- ...params,
27209
- after.startedAtMs,
27210
- after.startedAtMs,
27211
- after.id,
27212
- SCAN_BATCH_ROWS
27213
- ]);
27214
- for (const r of rows) {
27215
- yield {
27216
- id: r.id,
27217
- ruleId: r.rule_id,
27218
- category: r.category,
27219
- severity: r.severity,
27220
- maskedMatch: r.masked_match,
27221
- actionTaken: r.action_taken,
27222
- confidence: r.confidence,
27223
- occurredAt: epochMillisToIso(r.occurred_at),
27224
- sourceTool: r.source_tool,
27225
- repo: r.repo ?? "",
27226
- file: r.file ?? "",
27227
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27228
- eventId: r.event_id,
27229
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27230
- status: deriveInstanceStatus(r)
27231
- };
27232
- }
27233
- if (rows.length < SCAN_BATCH_ROWS) return;
27234
- const lastRow = rows[rows.length - 1];
27235
- if (lastRow === void 0) return;
27236
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27237
- }
27428
+ ORDER BY e.started_at DESC, f.id DESC`;
27429
+ return { sql, params };
27238
27430
  }
27239
27431
  groupAggregates(withSearchText, scope) {
27240
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27241
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27242
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27432
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27433
+ group_concat(DISTINCT e.file_path) AS files,
27434
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27243
27435
  const rows = this.db.prepare(
27244
27436
  `SELECT rule_id,
27245
27437
  sum(tuple_count) AS instance_count,
@@ -27257,7 +27449,7 @@ var SqliteFindingsRepository = class {
27257
27449
  coalesce(latest.status, '') AS status_tuple,
27258
27450
  count(*) AS tuple_count,
27259
27451
  max(e.started_at) AS latest_at,
27260
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27452
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27261
27453
  group_concat(DISTINCT f.action_taken) AS actions_taken
27262
27454
  ${innerSearchColumns}
27263
27455
  FROM inspection_findings f
@@ -27388,6 +27580,8 @@ function isoDay(ms) {
27388
27580
  // ../../packages/persistence/src/repositories/history-sync.ts
27389
27581
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27390
27582
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27583
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27584
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27391
27585
  var SKIPPED = -1;
27392
27586
  var ROW_COLUMNS = `id,
27393
27587
  parent_id AS parentId,
@@ -27427,9 +27621,23 @@ var SqliteHistorySyncRepository = class {
27427
27621
  ORDER BY (event_type = 'session') DESC, started_at
27428
27622
  LIMIT :limit`
27429
27623
  );
27430
- this.stampStmt = db.prepare(
27431
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27432
- );
27624
+ this.captureRowsStmt = db.prepare(
27625
+ `SELECT ${ROW_COLUMNS}
27626
+ FROM audit_events
27627
+ WHERE synced_at IS NULL
27628
+ AND sync_claimed_at IS NULL
27629
+ AND outbox_owed = 1
27630
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27631
+ AND started_at < :before
27632
+ ORDER BY started_at
27633
+ LIMIT :limit`
27634
+ );
27635
+ this.markOwedStmt = db.prepare(
27636
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27637
+ );
27638
+ this.stampStmt = db.prepare(
27639
+ `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27640
+ );
27433
27641
  this.claimRowStmt = db.prepare(
27434
27642
  `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27435
27643
  );
@@ -27458,6 +27666,12 @@ var SqliteHistorySyncRepository = class {
27458
27666
  FROM audit_events
27459
27667
  WHERE event_type IN (${TYPE_LIST})`
27460
27668
  );
27669
+ this.captureSkipCountStmt = db.prepare(
27670
+ `SELECT COUNT(*) AS skipped
27671
+ FROM audit_events
27672
+ WHERE synced_at = ${String(SKIPPED)}
27673
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27674
+ );
27461
27675
  this.fingerprintStmt = db.prepare(
27462
27676
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27463
27677
  FROM history_sync WHERE id = 1`
@@ -27467,6 +27681,10 @@ var SqliteHistorySyncRepository = class {
27467
27681
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27468
27682
  WHERE id = 1`
27469
27683
  );
27684
+ this.disownCapturesStmt = db.prepare(
27685
+ `UPDATE audit_events SET outbox_owed = NULL
27686
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27687
+ );
27470
27688
  this.rearmStmt = db.prepare(
27471
27689
  `UPDATE audit_events SET synced_at = NULL
27472
27690
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27539,6 +27757,10 @@ var SqliteHistorySyncRepository = class {
27539
27757
  closeWindowStmt;
27540
27758
  releaseBoundaryStmt;
27541
27759
  freezeBoundaryStmt;
27760
+ captureRowsStmt;
27761
+ markOwedStmt;
27762
+ captureSkipCountStmt;
27763
+ disownCapturesStmt;
27542
27764
  partitionStmt;
27543
27765
  claimRowStmt;
27544
27766
  releaseRowStmt;
@@ -27572,6 +27794,34 @@ var SqliteHistorySyncRepository = class {
27572
27794
  pendingRows(sessionId, limit, before) {
27573
27795
  return allRows(this.rowsStmt, { sessionId, limit, before });
27574
27796
  }
27797
+ /**
27798
+ * Captures this machine still owes the deployment, oldest first.
27799
+ *
27800
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27801
+ * by a time window — see captureRowsStmt for why a window could not express
27802
+ * this. `before` is the grace window that leaves a just-recorded capture to
27803
+ * the live path.
27804
+ */
27805
+ pendingCaptureRows(limit, before) {
27806
+ return allRows(this.captureRowsStmt, { limit, before });
27807
+ }
27808
+ /**
27809
+ * Record that a capture is OWED to the deployment.
27810
+ *
27811
+ * Written by the attached forward path when a live send did not confirm
27812
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27813
+ * a fact rather than an inference: the machine was attached, the send did not
27814
+ * land, so the row is owed — which no time window can state, because the same
27815
+ * window that holds the rows a past attachment left owed also holds every
27816
+ * capture recorded while the machine was DETACHED, and those were never
27817
+ * offered to anyone.
27818
+ *
27819
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27820
+ * out of the drain's read.
27821
+ */
27822
+ markCaptureOwed(id) {
27823
+ this.markOwedStmt.run({ id });
27824
+ }
27575
27825
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27576
27826
  markSynced(ids, atMs) {
27577
27827
  this.stampAll(ids, atMs);
@@ -27655,10 +27905,12 @@ var SqliteHistorySyncRepository = class {
27655
27905
  this.countsStmt,
27656
27906
  { before }
27657
27907
  );
27908
+ const captures = getRow(this.captureSkipCountStmt);
27658
27909
  return {
27659
27910
  pending: row?.pending ?? 0,
27660
27911
  sent: row?.sent ?? 0,
27661
- skipped: row?.skipped ?? 0
27912
+ skipped: row?.skipped ?? 0,
27913
+ capturesSkipped: captures?.skipped ?? 0
27662
27914
  };
27663
27915
  }
27664
27916
  /**
@@ -27699,7 +27951,11 @@ var SqliteHistorySyncRepository = class {
27699
27951
  withTransaction(
27700
27952
  this.db,
27701
27953
  () => {
27954
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27702
27955
  this.rearmStmt.run();
27956
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27957
+ this.disownCapturesStmt.run();
27958
+ }
27703
27959
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27704
27960
  },
27705
27961
  "IMMEDIATE"
@@ -27896,7 +28152,231 @@ var SqliteInspectionFindingsRepository = class {
27896
28152
  };
27897
28153
 
27898
28154
  // ../../packages/persistence/src/repositories/installed-packs.ts
27899
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28155
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28156
+
28157
+ // ../../packages/persistence/src/policy-floor.ts
28158
+ import { readFileSync as readFileSync5 } from "fs";
28159
+ import { join as join6 } from "path";
28160
+
28161
+ // ../../packages/persistence/src/local-layout.ts
28162
+ import { renameSync as renameSync3 } from "fs";
28163
+ import { mkdir } from "fs/promises";
28164
+ import { homedir } from "os";
28165
+ import { join as join4 } from "path";
28166
+ function defaultDataDir() {
28167
+ return join4(homedir(), ".aka");
28168
+ }
28169
+ function settingsDir(base = defaultDataDir()) {
28170
+ return join4(base, "settings");
28171
+ }
28172
+ function dataDir(base = defaultDataDir()) {
28173
+ return join4(base, "data");
28174
+ }
28175
+
28176
+ // ../../packages/persistence/src/settings.ts
28177
+ import { readFileSync as readFileSync4 } from "fs";
28178
+ import { join as join5 } from "path";
28179
+
28180
+ // ../../packages/persistence/src/file-lock.ts
28181
+ import { randomUUID as randomUUID3 } from "crypto";
28182
+ import {
28183
+ closeSync,
28184
+ existsSync as existsSync2,
28185
+ openSync,
28186
+ readFileSync as readFileSync2,
28187
+ rmSync as rmSync5,
28188
+ statSync as statSync3,
28189
+ writeFileSync as writeFileSync2
28190
+ } from "fs";
28191
+ import { hostname as hostname3 } from "os";
28192
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28193
+
28194
+ // ../../packages/persistence/src/managed-settings.ts
28195
+ import { readFileSync as readFileSync3 } from "fs";
28196
+ import { posix, win32 } from "path";
28197
+ function managedSettingsPaths(platform2 = process.platform) {
28198
+ if (platform2 === "darwin") {
28199
+ return [
28200
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28201
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28202
+ ];
28203
+ }
28204
+ if (platform2 === "win32") {
28205
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28206
+ }
28207
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28208
+ }
28209
+ function readManagedSettings(paths = managedSettingsPaths()) {
28210
+ for (const path of paths) {
28211
+ let text;
28212
+ try {
28213
+ text = readFileSync3(path, "utf8");
28214
+ } catch {
28215
+ continue;
28216
+ }
28217
+ const record2 = parseJsonObject(text);
28218
+ if (!record2) continue;
28219
+ const parsed2 = ManagedSettings.safeParse(record2);
28220
+ if (parsed2.success) return parsed2.data;
28221
+ }
28222
+ return null;
28223
+ }
28224
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28225
+ if (!managed) return settings;
28226
+ const { values } = managed;
28227
+ const merged = { ...settings };
28228
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28229
+ if (values.controlPlane !== void 0) {
28230
+ merged.controlPlane = {
28231
+ ...values.controlPlane,
28232
+ // The administrator pinned WHICH deployment, not WHEN this machine
28233
+ // joined it. Keep the user's own attach time when the endpoint is
28234
+ // unchanged, so a managed machine does not appear to re-attach on every
28235
+ // read; stamp a fresh one when the administrator moved it.
28236
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28237
+ };
28238
+ }
28239
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28240
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28241
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28242
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28243
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28244
+ if (values.vaultConsent !== void 0) {
28245
+ merged.vaultConsent = values.vaultConsent ? (
28246
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28247
+ // at the current version otherwise.
28248
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28249
+ ) : void 0;
28250
+ }
28251
+ if (values.modelJudgeConsent !== void 0) {
28252
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28253
+ acknowledgedAt: now().toISOString(),
28254
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28255
+ } : void 0;
28256
+ }
28257
+ return merged;
28258
+ }
28259
+
28260
+ // ../../packages/persistence/src/settings.ts
28261
+ var SETTINGS_FILENAME = "settings.json";
28262
+ function readWorkspaceSettings(base = defaultDataDir()) {
28263
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28264
+ }
28265
+ function readUserSettings(base) {
28266
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28267
+ if (!record2) return defaultWorkspaceSettings();
28268
+ try {
28269
+ return WorkspaceSettings.parse(record2);
28270
+ } catch {
28271
+ return defaultWorkspaceSettings();
28272
+ }
28273
+ }
28274
+ function readJson(file2) {
28275
+ let text;
28276
+ try {
28277
+ text = readFileSync4(file2, "utf8");
28278
+ } catch {
28279
+ return null;
28280
+ }
28281
+ return parseJsonObject(text) ?? null;
28282
+ }
28283
+
28284
+ // ../../packages/persistence/src/policy-floor.ts
28285
+ function refusalMessage(pack, attempted, floor, refusal) {
28286
+ switch (refusal) {
28287
+ case "lock":
28288
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28289
+ case "disable":
28290
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28291
+ case "floor":
28292
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28293
+ }
28294
+ }
28295
+ var PolicyFloorError = class extends Error {
28296
+ /** `namespace/packId` of the detection whose write was refused. */
28297
+ pack;
28298
+ /**
28299
+ * The archetype the caller asked for, or null when the write named none —
28300
+ * clearing the assignment, or switching the detection off.
28301
+ */
28302
+ attempted;
28303
+ /** The weakest archetype the control plane permits for this pack. */
28304
+ floor;
28305
+ refusal;
28306
+ constructor(pack, attempted, floor, refusal) {
28307
+ super(refusalMessage(pack, attempted, floor, refusal));
28308
+ this.name = "PolicyFloorError";
28309
+ this.pack = pack;
28310
+ this.attempted = attempted;
28311
+ this.floor = floor;
28312
+ this.refusal = refusal;
28313
+ }
28314
+ };
28315
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28316
+ try {
28317
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28318
+ const parsed2 = JSON.parse(raw);
28319
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28320
+ return PolicyBundle.parse(parsed2.bundle);
28321
+ } catch {
28322
+ return null;
28323
+ }
28324
+ }
28325
+ function indexEnabled(policies) {
28326
+ const byRuleId = /* @__PURE__ */ new Map();
28327
+ const byCategory = /* @__PURE__ */ new Map();
28328
+ for (const policy of policies) {
28329
+ if (!policy.enabled) continue;
28330
+ if ("ruleId" in policy.target) {
28331
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28332
+ } else if (!byCategory.has(policy.target.category)) {
28333
+ byCategory.set(policy.target.category, policy.action);
28334
+ }
28335
+ }
28336
+ return { byRuleId, byCategory };
28337
+ }
28338
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28339
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28340
+ const categories = new Set(rules.map((rule) => rule.category));
28341
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28342
+ return policies.some((policy) => {
28343
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28344
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28345
+ });
28346
+ }
28347
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28348
+ const floors = openControlPlaneFloors(base);
28349
+ return floors === null ? null : floors.floorFor(rules);
28350
+ }
28351
+ function openControlPlaneFloors(base = defaultDataDir()) {
28352
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28353
+ const bundle = readCachedPolicyBundle(base);
28354
+ if (bundle === null) return null;
28355
+ const indexes = indexEnabled(bundle.policies);
28356
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28357
+ }
28358
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28359
+ let action = null;
28360
+ for (const rule of rules) {
28361
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28362
+ if (resolved === void 0) continue;
28363
+ action = action === null ? resolved : strongerAction(action, resolved);
28364
+ }
28365
+ if (action === null) return null;
28366
+ return {
28367
+ floor: weakestBuiltinAtLeast(action),
28368
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28369
+ };
28370
+ }
28371
+ function policyAssignmentRefusal(policyId, floor) {
28372
+ if (floor.locked) return "lock";
28373
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28374
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28375
+ }
28376
+ function packEnablementRefusal(enabled, floor) {
28377
+ if (floor === null || enabled) return null;
28378
+ return "disable";
28379
+ }
27900
28380
 
27901
28381
  // ../../packages/persistence/src/semver.ts
27902
28382
  function parse3(version2) {
@@ -27990,8 +28470,19 @@ function ruleIdsOf(rulesJson) {
27990
28470
  return ids;
27991
28471
  }
27992
28472
  var SqliteInstalledPacksRepository = class {
27993
- constructor(db) {
28473
+ /**
28474
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28475
+ * floor needs both halves of it (settings/ says whether this machine is
28476
+ * attached, data/ holds the cached bundle). It is optional because a caller
28477
+ * holding only a DatabaseSync — every test construction site, and any embedder
28478
+ * that opens the store itself — has no layout to point at, and such a caller
28479
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28480
+ * from `openLocalDatabase`, which is the single construction site that owns a
28481
+ * real `~/.aka`.
28482
+ */
28483
+ constructor(db, baseDir) {
27994
28484
  this.db = db;
28485
+ this.baseDir = baseDir;
27995
28486
  this.insertMissingStmt = db.prepare(
27996
28487
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27997
28488
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28013,11 +28504,17 @@ var SqliteInstalledPacksRepository = class {
28013
28504
  this.signatureStmt = db.prepare(
28014
28505
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28015
28506
  );
28507
+ this.packRulesStmt = db.prepare(
28508
+ `SELECT rules_json AS rulesJson FROM installed_packs
28509
+ WHERE namespace = ? AND pack_id = ?`
28510
+ );
28016
28511
  }
28017
28512
  db;
28513
+ baseDir;
28018
28514
  insertMissingStmt;
28019
28515
  upsertAvailableStmt;
28020
28516
  signatureStmt;
28517
+ packRulesStmt;
28021
28518
  /**
28022
28519
  * Record the running binary's detection inventory. Refreshes the
28023
28520
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28059,7 +28556,7 @@ var SqliteInstalledPacksRepository = class {
28059
28556
  let behind = false;
28060
28557
  for (const row of rows) {
28061
28558
  const params = {
28062
- id: randomUUID3(),
28559
+ id: randomUUID4(),
28063
28560
  namespace: row.namespace,
28064
28561
  packId: row.packId,
28065
28562
  version: row.version,
@@ -28071,7 +28568,7 @@ var SqliteInstalledPacksRepository = class {
28071
28568
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28072
28569
  this.upsertAvailableStmt.run({
28073
28570
  ...params,
28074
- id: randomUUID3(),
28571
+ id: randomUUID4(),
28075
28572
  recordedBy: meta4?.recordedBy ?? null
28076
28573
  });
28077
28574
  } else {
@@ -28317,9 +28814,65 @@ var SqliteInstalledPacksRepository = class {
28317
28814
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28318
28815
  // caller rather than swallowing them. Each returns whether a row matched, so the
28319
28816
  // caller can tell an edit from a no-such-detection.
28817
+ /**
28818
+ * The rules one installed pack owns, reduced to what a floor computation
28819
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28820
+ * unreadable contributes no rules to a scan either, so it is not a detection
28821
+ * the control plane can be governing, and an empty list correctly imposes no
28822
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28823
+ * the user can re-enable, and its assignment stays governed meanwhile.
28824
+ */
28825
+ packFloorRules(namespace, packId) {
28826
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28827
+ if (!row) return [];
28828
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28829
+ }
28830
+ /**
28831
+ * What the connected control plane imposes on one installed pack, or null on a
28832
+ * machine that is its own authority (standalone, no cached bundle, or a
28833
+ * repository constructed without a layout base).
28834
+ *
28835
+ * Exposed as a READ so a surface can render the constraint — grey out the
28836
+ * choices below the floor, mark a locked detection as locked — rather than
28837
+ * offer the user a picker whose selections it will then be told it may not
28838
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28839
+ */
28840
+ policyFloor(namespace, packId) {
28841
+ if (this.baseDir === void 0) return null;
28842
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28843
+ }
28844
+ /**
28845
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28846
+ * entry only for a pack the control plane actually governs.
28847
+ *
28848
+ * A surface listing every detection asks per pack, and asking through
28849
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28850
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28851
+ * answer, repeated for each row, on every render. This reads all of that once.
28852
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28853
+ * exactly as the single-pack read returns null for them.
28854
+ */
28855
+ policyFloors(packs) {
28856
+ const floors = /* @__PURE__ */ new Map();
28857
+ if (this.baseDir === void 0) return floors;
28858
+ const source = openControlPlaneFloors(this.baseDir);
28859
+ if (source === null) return floors;
28860
+ for (const pack of packs) {
28861
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28862
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28863
+ }
28864
+ return floors;
28865
+ }
28320
28866
  /**
28321
28867
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28322
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28868
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28869
+ *
28870
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28871
+ * write below, and a detection the organization has authored a policy for is
28872
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28873
+ * a throw rather than a silently substituted value. This is the one device-local
28874
+ * write path for the assignment, so the check belongs here rather than on any
28875
+ * surface that offers the choice.
28323
28876
  */
28324
28877
  setPolicy(namespace, packId, policyId) {
28325
28878
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28327,14 +28880,38 @@ var SqliteInstalledPacksRepository = class {
28327
28880
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28328
28881
  );
28329
28882
  }
28883
+ const requested = policyId;
28884
+ const floor = this.policyFloor(namespace, packId);
28885
+ if (floor !== null) {
28886
+ const refusal = policyAssignmentRefusal(requested, floor);
28887
+ if (refusal !== null) {
28888
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28889
+ }
28890
+ }
28330
28891
  const res = this.db.prepare(
28331
28892
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28332
28893
  WHERE namespace = :namespace AND pack_id = :packId`
28333
28894
  ).run({ policyId, now: Date.now(), namespace, packId });
28334
28895
  return Number(res.changes) > 0;
28335
28896
  }
28336
- /** Enable or disable one installed pack. */
28897
+ /**
28898
+ * Enable or disable one installed pack.
28899
+ *
28900
+ * On an ATTACHED machine a detection the organization's bundle governs at all
28901
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
28902
+ * merely another point below the floor, and why re-enabling stays open. Like
28903
+ * the assignment above, the check belongs at this write path rather than on a
28904
+ * surface: this is the one device-local writer of the column, and a refusal
28905
+ * that lived in a page would leave the CLI free.
28906
+ */
28337
28907
  setEnabled(namespace, packId, enabled) {
28908
+ const floor = this.policyFloor(namespace, packId);
28909
+ if (floor !== null) {
28910
+ const refusal = packEnablementRefusal(enabled, floor);
28911
+ if (refusal !== null) {
28912
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
28913
+ }
28914
+ }
28338
28915
  const res = this.db.prepare(
28339
28916
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28340
28917
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28420,7 +28997,7 @@ var SqliteInventoryRepository = class {
28420
28997
  };
28421
28998
 
28422
28999
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28423
- import { randomUUID as randomUUID4 } from "crypto";
29000
+ import { randomUUID as randomUUID5 } from "crypto";
28424
29001
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28425
29002
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28426
29003
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28909,7 +29486,7 @@ var SqliteInventoryAssetsRepository = class {
28909
29486
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28910
29487
  VALUES (:id, :projectId, :path, :access, :now, :now)
28911
29488
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28912
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29489
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28913
29490
  }
28914
29491
  return true;
28915
29492
  }
@@ -28930,7 +29507,7 @@ var SqliteInventoryAssetsRepository = class {
28930
29507
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28931
29508
  VALUES (:id, :assetId, :trust, :now, :now)
28932
29509
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28933
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29510
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28934
29511
  }
28935
29512
  this.configRowsCache = void 0;
28936
29513
  return "ok";
@@ -29227,7 +29804,7 @@ var SqliteInventoryAssetsRepository = class {
29227
29804
  };
29228
29805
 
29229
29806
  // ../../packages/persistence/src/repositories/policies.ts
29230
- import { randomUUID as randomUUID5 } from "crypto";
29807
+ import { randomUUID as randomUUID6 } from "crypto";
29231
29808
  var SqlitePoliciesRepository = class {
29232
29809
  constructor(db) {
29233
29810
  this.db = db;
@@ -29262,7 +29839,7 @@ var SqlitePoliciesRepository = class {
29262
29839
  failOpenTransaction(this.db, () => {
29263
29840
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29264
29841
  stmt.run({
29265
- id: randomUUID5(),
29842
+ id: randomUUID6(),
29266
29843
  target: JSON.stringify({ category }),
29267
29844
  action,
29268
29845
  now: Date.now()
@@ -29282,7 +29859,7 @@ var SqlitePoliciesRepository = class {
29282
29859
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29283
29860
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29284
29861
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29285
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29862
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29286
29863
  }
29287
29864
  // Caps every global per-category policy currently set to block/redact down
29288
29865
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29350,7 +29927,7 @@ var SqlitePolicyCatalogRepository = class {
29350
29927
  };
29351
29928
 
29352
29929
  // ../../packages/persistence/src/repositories/project-files.ts
29353
- import { randomUUID as randomUUID6 } from "crypto";
29930
+ import { randomUUID as randomUUID7 } from "crypto";
29354
29931
  var SqliteProjectFilesRepository = class {
29355
29932
  constructor(db) {
29356
29933
  this.db = db;
@@ -29382,7 +29959,7 @@ var SqliteProjectFilesRepository = class {
29382
29959
  const stamp = Math.max(now, maxStamp + 1);
29383
29960
  for (const file2 of scan2.files) {
29384
29961
  this.upsertStmt.run({
29385
- id: randomUUID6(),
29962
+ id: randomUUID7(),
29386
29963
  projectId,
29387
29964
  path: file2.path,
29388
29965
  name: file2.name,
@@ -29396,9 +29973,9 @@ var SqliteProjectFilesRepository = class {
29396
29973
  };
29397
29974
 
29398
29975
  // ../../packages/persistence/src/repositories/resolutions.ts
29399
- import { randomUUID as randomUUID7 } from "crypto";
29976
+ import { randomUUID as randomUUID8 } from "crypto";
29400
29977
  var SqliteResolutionsRepository = class {
29401
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
29978
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29402
29979
  this.db = db;
29403
29980
  this.now = now;
29404
29981
  this.newId = newId;
@@ -29611,7 +30188,7 @@ var SqliteScanLedgerRepository = class {
29611
30188
  };
29612
30189
 
29613
30190
  // ../../packages/persistence/src/repositories/secret-vault.ts
29614
- import { randomUUID as randomUUID8 } from "crypto";
30191
+ import { randomUUID as randomUUID9 } from "crypto";
29615
30192
  function pageLimit(requested, fallback) {
29616
30193
  if (requested === void 0) return fallback;
29617
30194
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29657,12 +30234,14 @@ var SELECT_COLUMNS = `
29657
30234
  ciphertext,
29658
30235
  nonce,
29659
30236
  auth_tag AS authTag,
30237
+ user_authorized AS userAuthorized,
29660
30238
  occurrence_count AS occurrenceCount,
29661
30239
  first_seen AS firstSeen,
29662
30240
  last_seen AS lastSeen`;
29663
30241
  function toRow(raw) {
29664
- const { provider, ...rest } = raw;
29665
- return provider === null ? rest : { ...rest, provider };
30242
+ const { provider, userAuthorized, ...rest } = raw;
30243
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30244
+ return provider === null ? row : { ...row, provider };
29666
30245
  }
29667
30246
  var SqliteSecretVaultRepository = class {
29668
30247
  constructor(db) {
@@ -29672,17 +30251,18 @@ var SqliteSecretVaultRepository = class {
29672
30251
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29673
30252
  format_version, category, rule_id, masked_match, provider,
29674
30253
  ciphertext, nonce, auth_tag,
29675
- occurrence_count, first_seen, last_seen
30254
+ user_authorized, occurrence_count, first_seen, last_seen
29676
30255
  ) VALUES (
29677
30256
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29678
30257
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29679
30258
  :ciphertext, :nonce, :authTag,
29680
- 1, :now, :now
30259
+ :userAuthorized, 1, :now, :now
29681
30260
  )`
29682
30261
  );
29683
30262
  this.bumpStmt = db.prepare(
29684
30263
  `UPDATE secret_vault
29685
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30264
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30265
+ user_authorized = max(user_authorized, :userAuthorized)
29686
30266
  WHERE value_fingerprint = :valueFingerprint`
29687
30267
  );
29688
30268
  this.byPointerStmt = db.prepare(
@@ -29702,6 +30282,7 @@ var SqliteSecretVaultRepository = class {
29702
30282
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29703
30283
  WHERE pointer_id = :pointerId`
29704
30284
  );
30285
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29705
30286
  this.derefStmt = db.prepare(
29706
30287
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29707
30288
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29715,6 +30296,7 @@ var SqliteSecretVaultRepository = class {
29715
30296
  listStmt;
29716
30297
  replaceCiphertextStmt;
29717
30298
  refreshFingerprintStmt;
30299
+ deleteByPointerStmt;
29718
30300
  derefStmt;
29719
30301
  /**
29720
30302
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29723,6 +30305,11 @@ var SqliteSecretVaultRepository = class {
29723
30305
  * pointer, category and ciphertext, so the same secret always resolves to one
29724
30306
  * wire token. `minted` is true only when this call created the row.
29725
30307
  *
30308
+ * `userAuthorized` is the one field a repeat call may still change, and only
30309
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30310
+ * the row is shared with every automatic path that vaults the same value. See
30311
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30312
+ *
29726
30313
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29727
30314
  * writers cannot both decide they are minting.
29728
30315
  */
@@ -29749,13 +30336,18 @@ var SqliteSecretVaultRepository = class {
29749
30336
  ciphertext: input2.ciphertext,
29750
30337
  nonce: input2.nonce,
29751
30338
  authTag: input2.authTag,
30339
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29752
30340
  now
29753
30341
  })
29754
30342
  );
29755
30343
  minted = true;
29756
30344
  return;
29757
30345
  }
29758
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30346
+ this.bumpStmt.run({
30347
+ valueFingerprint: input2.valueFingerprint,
30348
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30349
+ now
30350
+ });
29759
30351
  },
29760
30352
  "IMMEDIATE"
29761
30353
  );
@@ -29815,6 +30407,42 @@ var SqliteSecretVaultRepository = class {
29815
30407
  );
29816
30408
  return destroyed;
29817
30409
  }
30410
+ /**
30411
+ * Destroy the named entries and report WHICH ones went — the scoped
30412
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30413
+ * values back where they came from. Ids the store does not hold are absent
30414
+ * from the answer rather than an error, so a set assembled from a stale read
30415
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30416
+ * it.
30417
+ *
30418
+ * The ids come back rather than a count because the caller's next act is to
30419
+ * write a purge row per destroyed entry, and a record of destruction has to
30420
+ * be a record of what was really destroyed: a selection is a claim about a
30421
+ * read that has since gone stale, and auditing from it invents a purge for an
30422
+ * entry still sitting in the vault.
30423
+ *
30424
+ * One transaction over the whole set rather than a statement per id: the
30425
+ * caller hands this the result of a restore pass it has completed, and a
30426
+ * fault partway through must leave the vault as it was found rather than
30427
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30428
+ * stands for, so half a delete is not a state anything can recover from.
30429
+ */
30430
+ deleteByPointerIds(pointerIds) {
30431
+ if (pointerIds.length === 0) return [];
30432
+ const deleted = [];
30433
+ withTransaction(
30434
+ this.db,
30435
+ () => {
30436
+ for (const pointerId of pointerIds) {
30437
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30438
+ deleted.push(pointerId);
30439
+ }
30440
+ }
30441
+ },
30442
+ "IMMEDIATE"
30443
+ );
30444
+ return deleted;
30445
+ }
29818
30446
  /**
29819
30447
  * Record (or re-stamp) one place a pointer has been written. One row per
29820
30448
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29827,7 +30455,7 @@ var SqliteSecretVaultRepository = class {
29827
30455
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29828
30456
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29829
30457
  ).run({
29830
- id: randomUUID8(),
30458
+ id: randomUUID9(),
29831
30459
  pointerId: entry.pointerId,
29832
30460
  location: entry.location,
29833
30461
  kind: entry.kind,
@@ -30340,15 +30968,15 @@ var SqliteSecurityRepository = class {
30340
30968
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30341
30969
  const rows = allRows(
30342
30970
  this.db.prepare(
30343
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
30971
+ `SELECT e.repo AS repo, count(*) AS c
30344
30972
  FROM inspection_findings f
30345
30973
  JOIN audit_events e ON e.id = f.audit_event_id
30346
30974
  WHERE e.started_at >= :from AND e.started_at < :to
30347
30975
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30348
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30349
- AND json_extract(e.attributes, '$.repo') != ''
30350
- GROUP BY repo
30351
- ORDER BY c DESC, repo
30976
+ AND e.repo IS NOT NULL
30977
+ AND e.repo != ''
30978
+ GROUP BY e.repo
30979
+ ORDER BY c DESC, e.repo
30352
30980
  LIMIT :limit`
30353
30981
  ),
30354
30982
  { from, to: now, limit }
@@ -30410,7 +31038,7 @@ var SqliteSecurityRepository = class {
30410
31038
  `SELECT f.finding_key AS finding_key,
30411
31039
  d.rule_id AS rule_id,
30412
31040
  d.severity AS severity,
30413
- json_extract(e.attributes, '$.file_path') AS path,
31041
+ e.file_path AS path,
30414
31042
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30415
31043
  latest.resolved_at AS latest_resolved_at
30416
31044
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30463,7 +31091,7 @@ var SqliteSecurityRepository = class {
30463
31091
  };
30464
31092
 
30465
31093
  // ../../packages/persistence/src/repositories/shares.ts
30466
- import { randomUUID as randomUUID9 } from "crypto";
31094
+ import { randomUUID as randomUUID10 } from "crypto";
30467
31095
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30468
31096
  var IN_CHUNK = 500;
30469
31097
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30551,7 +31179,7 @@ function buildSummary(dest, endpoints) {
30551
31179
  callSiteCount,
30552
31180
  transports: distinctTransports(transports),
30553
31181
  dataClasses: distinctDataClasses(dataClasses),
30554
- review: buildReviewInfo(dest.trust, transports),
31182
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30555
31183
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30556
31184
  endpoints: endpoints.map(toEndpointSummary)
30557
31185
  };
@@ -30578,7 +31206,7 @@ function buildDetail(dest, endpoints, callSites) {
30578
31206
  lastSeen: new Date(lastSeenMs).toISOString(),
30579
31207
  transports: distinctTransports(transports),
30580
31208
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30581
- review: buildReviewInfo(dest.trust, transports),
31209
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30582
31210
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30583
31211
  note: dest.note,
30584
31212
  endpoints: endpoints.map((ep) => ({
@@ -30607,7 +31235,11 @@ var SqliteSharesRepository = class {
30607
31235
  FROM share_destination d
30608
31236
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30609
31237
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30610
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31238
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31239
+ AND NOT EXISTS (
31240
+ SELECT 1 FROM egress_decision_override o
31241
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31242
+ )`
30611
31243
  );
30612
31244
  const kindCounts = countBy(
30613
31245
  this.db,
@@ -30719,7 +31351,7 @@ var SqliteSharesRepository = class {
30719
31351
  (id, destination_id, host, decision, created_at, updated_at)
30720
31352
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30721
31353
  ).run({
30722
- id: randomUUID9(),
31354
+ id: randomUUID10(),
30723
31355
  destinationId,
30724
31356
  host: dest.host,
30725
31357
  decision,
@@ -30868,7 +31500,7 @@ var SqliteSharesRepository = class {
30868
31500
  let destinationId = destIds.get(hit.host);
30869
31501
  if (destinationId === void 0) {
30870
31502
  destStmt.run({
30871
- id: randomUUID9(),
31503
+ id: randomUUID10(),
30872
31504
  kind: hit.kind,
30873
31505
  name: hit.name,
30874
31506
  host: hit.host,
@@ -30884,7 +31516,7 @@ var SqliteSharesRepository = class {
30884
31516
  let endpointId = endpointIds.get(endpointKey);
30885
31517
  if (endpointId === void 0) {
30886
31518
  endpointStmt.run({
30887
- id: randomUUID9(),
31519
+ id: randomUUID10(),
30888
31520
  destinationId,
30889
31521
  method: hit.method,
30890
31522
  transport: hit.transport,
@@ -30897,7 +31529,7 @@ var SqliteSharesRepository = class {
30897
31529
  endpointIds.set(endpointKey, endpointId);
30898
31530
  }
30899
31531
  siteStmt.run({
30900
- id: randomUUID9(),
31532
+ id: randomUUID10(),
30901
31533
  endpointId,
30902
31534
  project: input2.project,
30903
31535
  projectKey: input2.projectKey,
@@ -31262,6 +31894,7 @@ function purgeSampleData(db) {
31262
31894
  }
31263
31895
 
31264
31896
  // ../../packages/persistence/src/database.ts
31897
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31265
31898
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31266
31899
  "aka.persistence.unsafeTestOnlyRawHandle"
31267
31900
  );
@@ -31309,7 +31942,7 @@ function backupLegacyStore(db, file2) {
31309
31942
  discardStore(file2, backup);
31310
31943
  return backup;
31311
31944
  }
31312
- function openAndInitialize(file2) {
31945
+ function openAndInitialize(file2, base) {
31313
31946
  let db = openWithPragmas(file2);
31314
31947
  try {
31315
31948
  if (isForeignSqliteLineage(db)) {
@@ -31322,7 +31955,7 @@ function openAndInitialize(file2) {
31322
31955
  applyMigrations(db, file2);
31323
31956
  tightenPerms(file2);
31324
31957
  const policies = new SqlitePoliciesRepository(db);
31325
- const installedPacks = new SqliteInstalledPacksRepository(db);
31958
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31326
31959
  const repositories = {
31327
31960
  events: new SqliteEventsRepository(db),
31328
31961
  findings: new SqliteFindingsRepository(db),
@@ -31358,7 +31991,7 @@ function openAndInitialize(file2) {
31358
31991
  }
31359
31992
  function openLocalDatabase(dir) {
31360
31993
  ensureDataDirSync(dir);
31361
- const file2 = join4(dir, DB_FILENAME);
31994
+ const file2 = join7(dir, DB_FILENAME);
31362
31995
  reapStalePartials(file2);
31363
31996
  const {
31364
31997
  db,
@@ -31386,7 +32019,13 @@ function openLocalDatabase(dir) {
31386
32019
  inspectionDefinitions,
31387
32020
  inspectionFindings,
31388
32021
  configInventory
31389
- } = openAndInitialize(file2);
32022
+ } = openAndInitialize(
32023
+ file2,
32024
+ // `dir` is always `<base>/data` — every caller resolves it through
32025
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32026
+ // settings/ and data/, and the pack-policy floor needs both halves.
32027
+ dirname2(dir)
32028
+ );
31390
32029
  function captureRowId(event) {
31391
32030
  return captureId(
31392
32031
  event.metadata?.sessionId ?? null,
@@ -31399,6 +32038,21 @@ function openLocalDatabase(dir) {
31399
32038
  historySync.markSynced([captureRowId(event)], atMs);
31400
32039
  });
31401
32040
  }
32041
+ function markCaptureOwed(event) {
32042
+ failOpenTransaction(db, () => {
32043
+ historySync.markCaptureOwed(captureRowId(event));
32044
+ });
32045
+ }
32046
+ function markAuditEventsDelivered(events2, atMs) {
32047
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32048
+ if (stampable.length === 0) return;
32049
+ failOpenTransaction(db, () => {
32050
+ historySync.markSynced(
32051
+ stampable.map((event) => event.id),
32052
+ atMs
32053
+ );
32054
+ });
32055
+ }
31402
32056
  function recordCapture(event, detected) {
31403
32057
  failOpenTransaction(db, () => {
31404
32058
  const sessionId = event.metadata?.sessionId;
@@ -31485,7 +32139,7 @@ function openLocalDatabase(dir) {
31485
32139
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31486
32140
  if (!definitionId) continue;
31487
32141
  inspectionFindings.insertFinding({
31488
- id: randomUUID10(),
32142
+ id: randomUUID11(),
31489
32143
  auditEventId: record2.scanEvent.id,
31490
32144
  inspectionDefinitionId: definitionId,
31491
32145
  span: finding.span,
@@ -31581,6 +32235,8 @@ function openLocalDatabase(dir) {
31581
32235
  inspectionFindings,
31582
32236
  recordCapture,
31583
32237
  markCaptureDelivered,
32238
+ markCaptureOwed,
32239
+ markAuditEventsDelivered,
31584
32240
  ensureInventory,
31585
32241
  recordConfigScan,
31586
32242
  recordProjectFiles,
@@ -31599,179 +32255,360 @@ function openLocalDatabase(dir) {
31599
32255
  };
31600
32256
  }
31601
32257
 
31602
- // ../../packages/persistence/src/file-lock.ts
31603
- import { randomUUID as randomUUID11 } from "crypto";
31604
- import {
31605
- closeSync,
31606
- existsSync as existsSync2,
31607
- openSync,
31608
- readFileSync as readFileSync2,
31609
- rmSync as rmSync5,
31610
- statSync as statSync3,
31611
- writeFileSync as writeFileSync2
31612
- } from "fs";
31613
- import { hostname as hostname3 } from "os";
31614
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31615
-
31616
32258
  // ../../packages/persistence/src/finding-key.ts
31617
32259
  import { createHash as createHash3 } from "crypto";
31618
32260
 
31619
32261
  // ../../packages/persistence/src/fingerprint.ts
31620
32262
  import { createHmac, randomBytes } from "crypto";
31621
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31622
- import { join as join5 } from "path";
32263
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32264
+ import { join as join8 } from "path";
31623
32265
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31624
32266
 
31625
32267
  // ../../packages/persistence/src/history-preview.ts
31626
32268
  import { existsSync as existsSync4 } from "fs";
31627
- import { join as join6 } from "path";
32269
+ import { join as join9 } from "path";
31628
32270
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31629
32271
 
31630
- // ../../packages/persistence/src/local-layout.ts
31631
- import { renameSync as renameSync3 } from "fs";
31632
- import { mkdir } from "fs/promises";
31633
- import { homedir } from "os";
31634
- import { join as join7 } from "path";
31635
- function defaultDataDir() {
31636
- return join7(homedir(), ".aka");
31637
- }
31638
- function settingsDir(base = defaultDataDir()) {
31639
- return join7(base, "settings");
31640
- }
31641
- function dataDir(base = defaultDataDir()) {
31642
- return join7(base, "data");
31643
- }
32272
+ // ../../packages/persistence/src/store-symlinks.ts
32273
+ import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32274
+ import { dirname as dirname3, join as join10, resolve } from "path";
31644
32275
 
31645
- // ../../packages/persistence/src/managed-settings.ts
31646
- import { readFileSync as readFileSync4 } from "fs";
31647
- import { posix, win32 } from "path";
31648
- function managedSettingsPaths(platform2 = process.platform) {
31649
- if (platform2 === "darwin") {
31650
- return [
31651
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31652
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31653
- ];
31654
- }
31655
- if (platform2 === "win32") {
31656
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
32276
+ // ../../packages/persistence/src/vault/crypto.ts
32277
+ import {
32278
+ createCipheriv,
32279
+ createDecipheriv,
32280
+ createHmac as createHmac2,
32281
+ hkdfSync,
32282
+ timingSafeEqual
32283
+ } from "crypto";
32284
+
32285
+ // ../../packages/persistence/src/vault/key-provider.ts
32286
+ import { execFileSync } from "child_process";
32287
+ import { randomBytes as randomBytes2 } from "crypto";
32288
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32289
+ import { join as join11 } from "path";
32290
+
32291
+ // ../../packages/persistence/src/vault/vault.ts
32292
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32293
+
32294
+ // ../../packages/persistence/src/warn-era-cap.ts
32295
+ import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32296
+ import { join as join12 } from "path";
32297
+
32298
+ // ../../packages/remote/src/http.ts
32299
+ import { request as httpRequest } from "http";
32300
+ import { request as httpsRequest } from "https";
32301
+ var DEFAULT_TIMEOUT_MS = 1e4;
32302
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
32303
+ var RemoteRequestError = class extends Error {
32304
+ constructor(status) {
32305
+ super(`control-plane request failed with status ${String(status)}`);
32306
+ this.status = status;
32307
+ this.name = "RemoteRequestError";
31657
32308
  }
31658
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31659
- }
31660
- function readManagedSettings(paths = managedSettingsPaths()) {
31661
- for (const path of paths) {
31662
- let text;
31663
- try {
31664
- text = readFileSync4(path, "utf8");
31665
- } catch {
31666
- continue;
31667
- }
31668
- const record2 = parseJsonObject(text);
31669
- if (!record2) continue;
31670
- const parsed2 = ManagedSettings.safeParse(record2);
31671
- if (parsed2.success) return parsed2.data;
32309
+ status;
32310
+ };
32311
+ var RemoteRouteAbsent = class extends Error {
32312
+ constructor(route) {
32313
+ super(`control plane does not serve ${route}`);
32314
+ this.route = route;
32315
+ this.name = "RemoteRouteAbsent";
31672
32316
  }
31673
- return null;
31674
- }
31675
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31676
- if (!managed) return settings;
31677
- const { values } = managed;
31678
- const merged = { ...settings };
31679
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31680
- if (values.controlPlane !== void 0) {
31681
- merged.controlPlane = {
31682
- ...values.controlPlane,
31683
- // The administrator pinned WHICH deployment, not WHEN this machine
31684
- // joined it. Keep the user's own attach time when the endpoint is
31685
- // unchanged, so a managed machine does not appear to re-attach on every
31686
- // read; stamp a fresh one when the administrator moved it.
31687
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31688
- };
32317
+ route;
32318
+ };
32319
+ var RemoteRequestInvalid = class extends Error {
32320
+ constructor(route, cause) {
32321
+ super(`refusing to send a malformed body to ${route}`);
32322
+ this.cause = cause;
32323
+ this.name = "RemoteRequestInvalid";
31689
32324
  }
31690
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31691
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31692
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31693
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31694
- if (values.vaultConsent !== void 0) {
31695
- merged.vaultConsent = values.vaultConsent ? (
31696
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31697
- // at the current version otherwise.
31698
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31699
- ) : void 0;
32325
+ cause;
32326
+ };
32327
+ var RemoteResponseInvalid = class extends Error {
32328
+ constructor(route, detail) {
32329
+ super(`control plane answered ${route} with ${detail}`);
32330
+ this.name = "RemoteResponseInvalid";
31700
32331
  }
31701
- if (values.modelJudgeConsent !== void 0) {
31702
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31703
- acknowledgedAt: now().toISOString(),
31704
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31705
- } : void 0;
32332
+ };
32333
+ var RemoteTransportError = class extends Error {
32334
+ /**
32335
+ * The status the peer sent, when headers arrived and only the BODY was
32336
+ * refused.
32337
+ *
32338
+ * Undefined for the ordinary case this class was written for — no answer at
32339
+ * all. It exists because two paths reject after a status has already been
32340
+ * delivered: an oversized body and an aborted response. Discarding it there
32341
+ * reported a deployment answering 401 with a verbose body as a network
32342
+ * outage, which sends the reader to look at their network instead of their
32343
+ * credential.
32344
+ */
32345
+ constructor(reason, status) {
32346
+ super(`control-plane request did not complete: ${reason}`);
32347
+ this.status = status;
32348
+ this.name = "RemoteTransportError";
31706
32349
  }
31707
- return merged;
32350
+ status;
32351
+ };
32352
+ async function send(options) {
32353
+ const url2 = new URL(options.url);
32354
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32355
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
32356
+ const requestOptions = {
32357
+ method: options.method,
32358
+ headers: {
32359
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
32360
+ // last they win, and two of the values below are ones no caller may
32361
+ // replace: `x-api-key` is the credential, and `content-length` is the
32362
+ // byte count that stops a multi-byte body being truncated by the
32363
+ // receiver. `SendOptions.headers` is a free-form record on an exported
32364
+ // function, so "no caller does that today" is not the guarantee to rely
32365
+ // on. The one header any caller actually passes — `if-none-match` on the
32366
+ // conditional GET — is untouched by this order.
32367
+ ...options.headers,
32368
+ // The credential. One header, matching what the deployment authenticates
32369
+ // on; a second copy in an `Authorization` header would be one more place
32370
+ // it can be logged by an intermediary for no gain.
32371
+ //
32372
+ // Spread conditionally rather than assigned as `undefined`: Node's header
32373
+ // handling and `content-length` bookkeeping treat a present-but-undefined
32374
+ // key differently from an absent one, and "the header is not there" is
32375
+ // the property the attach flow needs.
32376
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
32377
+ accept: "application/json",
32378
+ ...options.body === void 0 ? {} : {
32379
+ "content-type": "application/json",
32380
+ // Byte length, not string length: a multi-byte body sent with a
32381
+ // character count is truncated by the receiver.
32382
+ "content-length": String(Buffer.byteLength(options.body))
32383
+ }
32384
+ }
32385
+ };
32386
+ return new Promise((resolve2, reject) => {
32387
+ let settled = false;
32388
+ const fail = (reason, status) => {
32389
+ if (settled) return;
32390
+ settled = true;
32391
+ reject(new RemoteTransportError(reason, status));
32392
+ };
32393
+ const req = send_(url2, requestOptions, (res) => {
32394
+ const chunks = [];
32395
+ let size = 0;
32396
+ res.on("data", (chunk) => {
32397
+ size += chunk.length;
32398
+ if (size > MAX_RESPONSE_BYTES) {
32399
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
32400
+ res.destroy();
32401
+ req.destroy();
32402
+ return;
32403
+ }
32404
+ chunks.push(chunk);
32405
+ });
32406
+ res.on("aborted", () => {
32407
+ fail("the response was aborted", res.statusCode);
32408
+ });
32409
+ res.on("end", () => {
32410
+ if (settled) return;
32411
+ settled = true;
32412
+ resolve2({
32413
+ status: res.statusCode ?? 0,
32414
+ headers: res.headers,
32415
+ body: Buffer.concat(chunks).toString("utf8")
32416
+ });
32417
+ });
32418
+ });
32419
+ const deadline = setTimeout(() => {
32420
+ fail(`no response within ${String(timeoutMs)}ms`);
32421
+ req.destroy();
32422
+ }, timeoutMs);
32423
+ deadline.unref();
32424
+ req.on("upgrade", (_res, socket) => {
32425
+ fail("the deployment answered with a protocol upgrade");
32426
+ socket.destroy();
32427
+ });
32428
+ req.on("close", () => {
32429
+ fail("the connection closed before a response was read");
32430
+ clearTimeout(deadline);
32431
+ });
32432
+ req.on("error", (err) => {
32433
+ fail(err.message);
32434
+ });
32435
+ if (options.body !== void 0) req.write(options.body);
32436
+ req.end();
32437
+ });
31708
32438
  }
31709
32439
 
31710
- // ../../packages/persistence/src/settings.ts
31711
- import { readFileSync as readFileSync5 } from "fs";
31712
- import { join as join8 } from "path";
31713
- var SETTINGS_FILENAME = "settings.json";
31714
- function readWorkspaceSettings(base = defaultDataDir()) {
31715
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
32440
+ // ../../packages/remote/src/client.ts
32441
+ var ROUTES = {
32442
+ events: "/v1/events",
32443
+ auditEvents: "/v1/audit-events",
32444
+ auditEventsBatch: "/v1/audit-events/batch",
32445
+ inventory: "/v1/inventory",
32446
+ storePosture: "/v1/store-posture",
32447
+ policyBundle: "/v1/policy-bundle",
32448
+ whoami: "/v1/plugin/whoami",
32449
+ shares: "/v1/shares",
32450
+ commands: "/v1/plugin/commands"
32451
+ };
32452
+ function ackRoute(id) {
32453
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
31716
32454
  }
31717
- function readUserSettings(base) {
31718
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31719
- if (!record2) return defaultWorkspaceSettings();
31720
- try {
31721
- return WorkspaceSettings.parse(record2);
31722
- } catch {
31723
- return defaultWorkspaceSettings();
32455
+ function headerValue(response, name) {
32456
+ const raw = response.headers[name];
32457
+ if (raw === void 0) return void 0;
32458
+ return Array.isArray(raw) ? raw[0] : raw;
32459
+ }
32460
+ function okBody(response) {
32461
+ if (response.status < 200 || response.status >= 300) {
32462
+ throw new RemoteRequestError(response.status);
31724
32463
  }
32464
+ return response.body;
31725
32465
  }
31726
- function readJson(file2) {
31727
- let text;
32466
+ function parsed(schema, body, route) {
32467
+ let json2;
31728
32468
  try {
31729
- text = readFileSync5(file2, "utf8");
32469
+ json2 = JSON.parse(body);
31730
32470
  } catch {
31731
- return null;
32471
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
31732
32472
  }
31733
- return parseJsonObject(text) ?? null;
32473
+ const result = schema.safeParse(json2);
32474
+ if (!result.success) {
32475
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
32476
+ }
32477
+ return result.data;
32478
+ }
32479
+ function withoutTrailingSlashes(endpoint) {
32480
+ let end = endpoint.length;
32481
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
32482
+ return endpoint.slice(0, end);
32483
+ }
32484
+ var SLASH = "/".charCodeAt(0);
32485
+ function createRemoteClient(options) {
32486
+ const base = withoutTrailingSlashes(options.endpoint);
32487
+ const url2 = (route) => `${base}${route}`;
32488
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
32489
+ const sendOne = async (event) => {
32490
+ const validated = RecordAuditEventRequest.safeParse(event);
32491
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
32492
+ const response = await send({
32493
+ ...common,
32494
+ method: "POST",
32495
+ url: url2(ROUTES.auditEvents),
32496
+ body: JSON.stringify(validated.data)
32497
+ });
32498
+ okBody(response);
32499
+ };
32500
+ return {
32501
+ async ingestEvents(batch) {
32502
+ const response = await send({
32503
+ ...common,
32504
+ method: "POST",
32505
+ url: url2(ROUTES.events),
32506
+ body: JSON.stringify(batch)
32507
+ });
32508
+ return parsed(IngestAck, okBody(response), ROUTES.events);
32509
+ },
32510
+ async ingestInventory(context) {
32511
+ const response = await send({
32512
+ ...common,
32513
+ method: "POST",
32514
+ url: url2(ROUTES.inventory),
32515
+ body: JSON.stringify(context)
32516
+ });
32517
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
32518
+ },
32519
+ async recordAuditEvent(event) {
32520
+ await sendOne(event);
32521
+ },
32522
+ async recordAuditEvents(events, opts) {
32523
+ const validated = RecordAuditEventBatch.safeParse({ events });
32524
+ if (!validated.success) {
32525
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
32526
+ }
32527
+ const response = await send({
32528
+ ...common,
32529
+ method: "POST",
32530
+ url: url2(ROUTES.auditEventsBatch),
32531
+ body: JSON.stringify(validated.data)
32532
+ });
32533
+ if (response.status === 404) {
32534
+ if (opts?.fallbackToSingleEvents !== true) {
32535
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
32536
+ }
32537
+ for (const event of validated.data.events) await sendOne(event);
32538
+ return { accepted: validated.data.events.length };
32539
+ }
32540
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
32541
+ },
32542
+ async reportStorePosture(snapshot) {
32543
+ const response = await send({
32544
+ ...common,
32545
+ method: "POST",
32546
+ url: url2(ROUTES.storePosture),
32547
+ body: JSON.stringify(snapshot)
32548
+ });
32549
+ okBody(response);
32550
+ },
32551
+ async getPolicyBundle(etag) {
32552
+ const response = await send({
32553
+ ...common,
32554
+ method: "GET",
32555
+ url: url2(ROUTES.policyBundle),
32556
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
32557
+ });
32558
+ if (response.status === 304) {
32559
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
32560
+ }
32561
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
32562
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
32563
+ },
32564
+ async whoami() {
32565
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
32566
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
32567
+ },
32568
+ async recordProjectEgress(request) {
32569
+ const validated = EgressIngestRequest.safeParse(request);
32570
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
32571
+ const response = await send({
32572
+ ...common,
32573
+ method: "POST",
32574
+ url: url2(ROUTES.shares),
32575
+ body: JSON.stringify(validated.data)
32576
+ });
32577
+ okBody(response);
32578
+ },
32579
+ async pollCommand() {
32580
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
32581
+ if (response.status === 404) return null;
32582
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
32583
+ },
32584
+ async ackCommand(id, body) {
32585
+ const validated = DeviceCommandAckBody.safeParse(body);
32586
+ const route = ackRoute(id);
32587
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
32588
+ const response = await send({
32589
+ ...common,
32590
+ method: "POST",
32591
+ url: url2(route),
32592
+ body: JSON.stringify(validated.data)
32593
+ });
32594
+ okBody(response);
32595
+ }
32596
+ };
31734
32597
  }
31735
32598
 
31736
- // ../../packages/persistence/src/store-symlinks.ts
31737
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31738
- import { dirname as dirname2, join as join9, resolve } from "path";
31739
-
31740
- // ../../packages/persistence/src/vault/crypto.ts
31741
- import {
31742
- createCipheriv,
31743
- createDecipheriv,
31744
- createHmac as createHmac2,
31745
- hkdfSync,
31746
- timingSafeEqual
31747
- } from "crypto";
31748
-
31749
- // ../../packages/persistence/src/vault/key-provider.ts
31750
- import { execFileSync } from "child_process";
31751
- import { randomBytes as randomBytes2 } from "crypto";
31752
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
31753
- import { join as join10 } from "path";
31754
-
31755
- // ../../packages/persistence/src/vault/vault.ts
31756
- import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
31757
-
31758
- // ../../packages/persistence/src/warn-era-cap.ts
31759
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
31760
- import { join as join11 } from "path";
31761
-
31762
32599
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
31763
- import { readFileSync as readFileSync7 } from "fs";
31764
- import { join as join12 } from "path";
32600
+ import { readFileSync as readFileSync8 } from "fs";
32601
+ import { join as join13 } from "path";
31765
32602
 
31766
32603
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
31767
32604
  import { randomUUID as randomUUID15 } from "crypto";
31768
- import { readFileSync as readFileSync13 } from "fs";
32605
+ import { readFileSync as readFileSync14 } from "fs";
31769
32606
  import { readFile, rename, writeFile } from "fs/promises";
31770
- import { join as join21 } from "path";
32607
+ import { join as join22 } from "path";
31771
32608
 
31772
32609
  // ../../packages/plugin-sdk/src/config.ts
31773
32610
  import { existsSync as existsSync7 } from "fs";
31774
- import { join as join13 } from "path";
32611
+ import { join as join14 } from "path";
31775
32612
 
31776
32613
  // ../../packages/plugin-sdk/src/provider-env.ts
31777
32614
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -31792,9 +32629,9 @@ var providerEnvShape = {
31792
32629
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
31793
32630
 
31794
32631
  // ../../packages/plugin-sdk/src/config-inventory.ts
31795
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32632
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
31796
32633
  import { homedir as homedir2 } from "os";
31797
- import { basename as basename3, join as join15 } from "path";
32634
+ import { basename as basename3, join as join16 } from "path";
31798
32635
 
31799
32636
  // ../../packages/detections/src/egress/registry.ts
31800
32637
  var EXTRACTOR_VERSION = "1";
@@ -32552,8 +33389,8 @@ var CPU_CORROBORATION_SHARE = 0.2;
32552
33389
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
32553
33390
 
32554
33391
  // ../../packages/plugin-sdk/src/repo.ts
32555
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
32556
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join14, sep as sep2 } from "path";
33392
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
33393
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
32557
33394
 
32558
33395
  // ../../packages/plugin-sdk/src/events.ts
32559
33396
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
@@ -32565,8 +33402,8 @@ import { Worker } from "worker_threads";
32565
33402
 
32566
33403
  // ../../packages/plugin-sdk/src/ignore-layers.ts
32567
33404
  var import_ignore = __toESM(require_ignore(), 1);
32568
- import { readFileSync as readFileSync10 } from "fs";
32569
- import { join as join16 } from "path";
33405
+ import { readFileSync as readFileSync11 } from "fs";
33406
+ import { join as join17 } from "path";
32570
33407
 
32571
33408
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
32572
33409
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -32577,24 +33414,24 @@ import {
32577
33414
  fstatSync,
32578
33415
  mkdirSync as mkdirSync2,
32579
33416
  openSync as openSync2,
32580
- readFileSync as readFileSync11,
33417
+ readFileSync as readFileSync12,
32581
33418
  readSync,
32582
33419
  writeFileSync as writeFileSync5
32583
33420
  } from "fs";
32584
- import { join as join17 } from "path";
33421
+ import { join as join18 } from "path";
32585
33422
  var TAIL_BYTES = 256 * 1024;
32586
33423
 
32587
33424
  // ../../packages/plugin-sdk/src/nudge.ts
32588
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
32589
- import { join as join18 } from "path";
33425
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
33426
+ import { join as join19 } from "path";
32590
33427
 
32591
33428
  // ../../packages/plugin-sdk/src/paths.ts
32592
33429
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
32593
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
33430
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
32594
33431
 
32595
33432
  // ../../packages/plugin-sdk/src/project-files.ts
32596
33433
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
32597
- import { basename as basename5, join as join19 } from "path";
33434
+ import { basename as basename5, join as join20 } from "path";
32598
33435
 
32599
33436
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
32600
33437
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -32630,7 +33467,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
32630
33467
 
32631
33468
  // ../../packages/plugin-sdk/src/throttle.ts
32632
33469
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
32633
- import { join as join20 } from "path";
33470
+ import { join as join21 } from "path";
32634
33471
 
32635
33472
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
32636
33473
  var BREAKER_COOLDOWN_MS = 3e4;
@@ -32656,15 +33493,15 @@ function parseBreakerState(raw, nowMs) {
32656
33493
  }
32657
33494
  function readForwardHealth(dir, nowMs = Date.now()) {
32658
33495
  try {
32659
- return parseBreakerState(readFileSync13(join21(dir, STATE_FILENAME), "utf8"), nowMs);
33496
+ return parseBreakerState(readFileSync14(join22(dir, STATE_FILENAME), "utf8"), nowMs);
32660
33497
  } catch {
32661
33498
  return null;
32662
33499
  }
32663
33500
  }
32664
33501
 
32665
33502
  // ../../packages/plugin-runtime/src/attached/history-state.ts
32666
- import { readFileSync as readFileSync14 } from "fs";
32667
- import { join as join22 } from "path";
33503
+ import { readFileSync as readFileSync15 } from "fs";
33504
+ import { join as join23 } from "path";
32668
33505
  var HISTORY_SYNC_STATE_FILENAME = ATTACHED_HISTORY_SYNC_STATE_FILENAME;
32669
33506
  var PHASES = /* @__PURE__ */ new Set(["filling", "complete"]);
32670
33507
  var OUTCOMES = /* @__PURE__ */ new Set([
@@ -32672,332 +33509,159 @@ var OUTCOMES = /* @__PURE__ */ new Set([
32672
33509
  "unreachable",
32673
33510
  "refused",
32674
33511
  "interrupted"
32675
- ]);
32676
- var SPEC_VERSION = 1;
32677
- function historySyncStatePath(dataDir2) {
32678
- return join22(dataDir2, HISTORY_SYNC_STATE_FILENAME);
32679
- }
32680
- function writeHistorySyncState(dataDir2, state) {
32681
- try {
32682
- ensureDataDirSync(dataDir2);
32683
- const persisted = { specVersion: SPEC_VERSION, ...state };
32684
- writeOwnerOnlyFileSync(historySyncStatePath(dataDir2), `${JSON.stringify(persisted)}
32685
- `);
32686
- } catch {
32687
- }
32688
- }
32689
- function readHistorySyncState(dataDir2) {
32690
- try {
32691
- const parsed2 = JSON.parse(readFileSync14(historySyncStatePath(dataDir2), "utf8"));
32692
- if (typeof parsed2 !== "object" || parsed2 === null) return null;
32693
- const r = parsed2;
32694
- if (r.specVersion !== SPEC_VERSION) return null;
32695
- if (typeof r.phase !== "string" || !PHASES.has(r.phase)) return null;
32696
- if (typeof r.lastOutcome !== "string" || !OUTCOMES.has(r.lastOutcome)) return null;
32697
- if (!isCount(r.lastPassAtMs)) return null;
32698
- if (!isCount(r.sentTotal) || !isCount(r.pendingTotal) || !isCount(r.skippedTotal)) return null;
32699
- if (!isNullableCount(r.startedAtMs) || !isNullableCount(r.completedAtMs)) return null;
32700
- return {
32701
- specVersion: SPEC_VERSION,
32702
- phase: r.phase,
32703
- lastOutcome: r.lastOutcome,
32704
- lastPassAtMs: r.lastPassAtMs,
32705
- sentTotal: r.sentTotal,
32706
- pendingTotal: r.pendingTotal,
32707
- skippedTotal: r.skippedTotal,
32708
- startedAtMs: r.startedAtMs,
32709
- completedAtMs: r.completedAtMs
32710
- };
32711
- } catch {
32712
- return null;
32713
- }
32714
- }
32715
- function isCount(v) {
32716
- return typeof v === "number" && Number.isFinite(v) && v >= 0;
32717
- }
32718
- function isNullableCount(v) {
32719
- return v === null || isCount(v);
32720
- }
32721
-
32722
- // ../../packages/plugin-runtime/src/attached/history-sync.ts
32723
- import { createHash as createHash6 } from "crypto";
32724
- import { hostname as hostname5 } from "os";
32725
-
32726
- // ../../packages/remote/src/http.ts
32727
- import { request as httpRequest } from "http";
32728
- import { request as httpsRequest } from "https";
32729
- var DEFAULT_TIMEOUT_MS = 1e4;
32730
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
32731
- var RemoteRequestError = class extends Error {
32732
- constructor(status) {
32733
- super(`control-plane request failed with status ${String(status)}`);
32734
- this.status = status;
32735
- this.name = "RemoteRequestError";
32736
- }
32737
- status;
32738
- };
32739
- var RemoteRequestInvalid = class extends Error {
32740
- constructor(route, cause) {
32741
- super(`refusing to send a malformed body to ${route}`);
32742
- this.cause = cause;
32743
- this.name = "RemoteRequestInvalid";
32744
- }
32745
- cause;
32746
- };
32747
- var RemoteResponseInvalid = class extends Error {
32748
- constructor(route, detail) {
32749
- super(`control plane answered ${route} with ${detail}`);
32750
- this.name = "RemoteResponseInvalid";
32751
- }
32752
- };
32753
- var RemoteTransportError = class extends Error {
32754
- /**
32755
- * The status the peer sent, when headers arrived and only the BODY was
32756
- * refused.
32757
- *
32758
- * Undefined for the ordinary case this class was written for — no answer at
32759
- * all. It exists because two paths reject after a status has already been
32760
- * delivered: an oversized body and an aborted response. Discarding it there
32761
- * reported a deployment answering 401 with a verbose body as a network
32762
- * outage, which sends the reader to look at their network instead of their
32763
- * credential.
32764
- */
32765
- constructor(reason, status) {
32766
- super(`control-plane request did not complete: ${reason}`);
32767
- this.status = status;
32768
- this.name = "RemoteTransportError";
32769
- }
32770
- status;
32771
- };
32772
- async function send(options) {
32773
- const url2 = new URL(options.url);
32774
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32775
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
32776
- const requestOptions = {
32777
- method: options.method,
32778
- headers: {
32779
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
32780
- // last they win, and two of the values below are ones no caller may
32781
- // replace: `x-api-key` is the credential, and `content-length` is the
32782
- // byte count that stops a multi-byte body being truncated by the
32783
- // receiver. `SendOptions.headers` is a free-form record on an exported
32784
- // function, so "no caller does that today" is not the guarantee to rely
32785
- // on. The one header any caller actually passes — `if-none-match` on the
32786
- // conditional GET — is untouched by this order.
32787
- ...options.headers,
32788
- // The credential. One header, matching what the deployment authenticates
32789
- // on; a second copy in an `Authorization` header would be one more place
32790
- // it can be logged by an intermediary for no gain.
32791
- //
32792
- // Spread conditionally rather than assigned as `undefined`: Node's header
32793
- // handling and `content-length` bookkeeping treat a present-but-undefined
32794
- // key differently from an absent one, and "the header is not there" is
32795
- // the property the attach flow needs.
32796
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
32797
- accept: "application/json",
32798
- ...options.body === void 0 ? {} : {
32799
- "content-type": "application/json",
32800
- // Byte length, not string length: a multi-byte body sent with a
32801
- // character count is truncated by the receiver.
32802
- "content-length": String(Buffer.byteLength(options.body))
32803
- }
32804
- }
32805
- };
32806
- return new Promise((resolve2, reject) => {
32807
- let settled = false;
32808
- const fail = (reason, status) => {
32809
- if (settled) return;
32810
- settled = true;
32811
- reject(new RemoteTransportError(reason, status));
32812
- };
32813
- const req = send_(url2, requestOptions, (res) => {
32814
- const chunks = [];
32815
- let size = 0;
32816
- res.on("data", (chunk) => {
32817
- size += chunk.length;
32818
- if (size > MAX_RESPONSE_BYTES) {
32819
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
32820
- res.destroy();
32821
- req.destroy();
32822
- return;
32823
- }
32824
- chunks.push(chunk);
32825
- });
32826
- res.on("aborted", () => {
32827
- fail("the response was aborted", res.statusCode);
32828
- });
32829
- res.on("end", () => {
32830
- if (settled) return;
32831
- settled = true;
32832
- resolve2({
32833
- status: res.statusCode ?? 0,
32834
- headers: res.headers,
32835
- body: Buffer.concat(chunks).toString("utf8")
32836
- });
32837
- });
32838
- });
32839
- const deadline = setTimeout(() => {
32840
- fail(`no response within ${String(timeoutMs)}ms`);
32841
- req.destroy();
32842
- }, timeoutMs);
32843
- deadline.unref();
32844
- req.on("upgrade", (_res, socket) => {
32845
- fail("the deployment answered with a protocol upgrade");
32846
- socket.destroy();
32847
- });
32848
- req.on("close", () => {
32849
- fail("the connection closed before a response was read");
32850
- clearTimeout(deadline);
32851
- });
32852
- req.on("error", (err) => {
32853
- fail(err.message);
32854
- });
32855
- if (options.body !== void 0) req.write(options.body);
32856
- req.end();
32857
- });
32858
- }
32859
-
32860
- // ../../packages/remote/src/client.ts
32861
- var ROUTES = {
32862
- events: "/v1/events",
32863
- auditEvents: "/v1/audit-events",
32864
- auditEventsBatch: "/v1/audit-events/batch",
32865
- inventory: "/v1/inventory",
32866
- storePosture: "/v1/store-posture",
32867
- policyBundle: "/v1/policy-bundle",
32868
- whoami: "/v1/plugin/whoami",
32869
- shares: "/v1/shares"
32870
- };
32871
- function headerValue(response, name) {
32872
- const raw = response.headers[name];
32873
- if (raw === void 0) return void 0;
32874
- return Array.isArray(raw) ? raw[0] : raw;
33512
+ ]);
33513
+ var SPEC_VERSION = 1;
33514
+ function historySyncStatePath(dataDir2) {
33515
+ return join23(dataDir2, HISTORY_SYNC_STATE_FILENAME);
32875
33516
  }
32876
- function okBody(response) {
32877
- if (response.status < 200 || response.status >= 300) {
32878
- throw new RemoteRequestError(response.status);
33517
+ function writeHistorySyncState(dataDir2, state) {
33518
+ try {
33519
+ ensureDataDirSync(dataDir2);
33520
+ const persisted = { specVersion: SPEC_VERSION, ...state };
33521
+ writeOwnerOnlyFileSync(historySyncStatePath(dataDir2), `${JSON.stringify(persisted)}
33522
+ `);
33523
+ } catch {
32879
33524
  }
32880
- return response.body;
32881
33525
  }
32882
- function parsed(schema, body, route) {
32883
- let json2;
33526
+ function readHistorySyncState(dataDir2) {
32884
33527
  try {
32885
- json2 = JSON.parse(body);
33528
+ const parsed2 = JSON.parse(readFileSync15(historySyncStatePath(dataDir2), "utf8"));
33529
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
33530
+ const r = parsed2;
33531
+ if (r.specVersion !== SPEC_VERSION) return null;
33532
+ if (typeof r.phase !== "string" || !PHASES.has(r.phase)) return null;
33533
+ if (typeof r.lastOutcome !== "string" || !OUTCOMES.has(r.lastOutcome)) return null;
33534
+ if (!isCount(r.lastPassAtMs)) return null;
33535
+ if (!isCount(r.sentTotal) || !isCount(r.pendingTotal) || !isCount(r.skippedTotal)) return null;
33536
+ if (!isNullableCount(r.startedAtMs) || !isNullableCount(r.completedAtMs)) return null;
33537
+ return {
33538
+ specVersion: SPEC_VERSION,
33539
+ phase: r.phase,
33540
+ lastOutcome: r.lastOutcome,
33541
+ lastPassAtMs: r.lastPassAtMs,
33542
+ sentTotal: r.sentTotal,
33543
+ pendingTotal: r.pendingTotal,
33544
+ skippedTotal: r.skippedTotal,
33545
+ startedAtMs: r.startedAtMs,
33546
+ completedAtMs: r.completedAtMs
33547
+ };
32886
33548
  } catch {
32887
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
33549
+ return null;
32888
33550
  }
32889
- const result = schema.safeParse(json2);
32890
- if (!result.success) {
32891
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
33551
+ }
33552
+ function isCount(v) {
33553
+ return typeof v === "number" && Number.isFinite(v) && v >= 0;
33554
+ }
33555
+ function isNullableCount(v) {
33556
+ return v === null || isCount(v);
33557
+ }
33558
+
33559
+ // ../../packages/plugin-runtime/src/attached/history-sync.ts
33560
+ import { createHash as createHash6 } from "crypto";
33561
+ import { hostname as hostname5 } from "os";
33562
+
33563
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
33564
+ function rebuildCapture(row) {
33565
+ const kind = EventKind.safeParse(row.eventType);
33566
+ if (!kind.success) return void 0;
33567
+ if (kind.data === "code_change") return void 0;
33568
+ const content = row.content ?? null;
33569
+ const contentHash = row.contentHash ?? null;
33570
+ const rootSessionId = row.rootSessionId ?? null;
33571
+ if (content === null || contentHash === null) return void 0;
33572
+ const occurredAt = isoOrUndefined(row.startedAt);
33573
+ if (occurredAt === void 0) return void 0;
33574
+ const attributes = parseAttributes(row.attributes ?? null);
33575
+ const sourceTool = SourceTool.safeParse(attributes.source_tool);
33576
+ if (!sourceTool.success) return void 0;
33577
+ const keep = (value, schema) => value !== void 0 && schema.safeParse(value).success ? value : void 0;
33578
+ const rawFilePath = typeof attributes.file_path === "string" ? attributes.file_path : null;
33579
+ const filePath = stringOrUndefined(attributes.file_path);
33580
+ const metadata = {
33581
+ ...rootSessionId === null ? {} : { sessionId: rootSessionId },
33582
+ ...filePath === void 0 ? {} : { filePath },
33583
+ ...pick2(attributes, { repo: "repo", toolName: "tool_name", model: "model" }),
33584
+ // The two constrained strings, kept only if they satisfy the wire.
33585
+ ...withField("traceId", keep(stringOrUndefined(attributes.trace_id), TRACE_ID)),
33586
+ ...withField(
33587
+ "correlationId",
33588
+ keep(stringOrUndefined(attributes.correlation_id), CORRELATION_ID)
33589
+ ),
33590
+ ...typeof attributes.gitignored === "boolean" ? { gitignored: attributes.gitignored } : {},
33591
+ ...typeof attributes.whole_file === "boolean" ? { wholeFile: attributes.whole_file } : {},
33592
+ ...typeof attributes.turn_index === "number" && Number.isInteger(attributes.turn_index) && attributes.turn_index >= 0 ? { turnIndex: attributes.turn_index } : {},
33593
+ // Carried, unlike inspectionMs below. The live forward sends it, and it is
33594
+ // the enforcement audit trail's link back to the grant that authorized a
33595
+ // bypass — dropping it would make the same capture mean different things to
33596
+ // the deployment depending on which route delivered it.
33597
+ //
33598
+ // Filtered ELEMENT-WISE against the wire's own element schema, for the same
33599
+ // reason the two constrained strings above are: a bag holding
33600
+ // ['legacy-grant-7'] is a plain array of strings, so a `typeof` filter passes
33601
+ // it, IngestEvent.safeParse then refuses the assembled event, and the whole
33602
+ // capture becomes a permanent skip. That is the per-row door the rest of this
33603
+ // function exists to avoid, and this field was the last one still using it.
33604
+ ...withList("exceptionIds", keepAll(attributes.exception_ids, EXCEPTION_ID))
33605
+ // inspectionMs is DELIBERATELY not carried. It measures latency a live host
33606
+ // session actually waited on, and a row being drained hours later is not
33607
+ // that; the field's own contract says a replay leaves it absent rather than
33608
+ // reporting a number no session experienced.
33609
+ };
33610
+ const parsed2 = IngestEvent.safeParse({
33611
+ id: captureWireId(rootSessionId, contentHash, rawFilePath),
33612
+ sourceTool: sourceTool.data,
33613
+ kind: kind.data,
33614
+ occurredAt,
33615
+ contentHash,
33616
+ content,
33617
+ metadata
33618
+ });
33619
+ return parsed2.success ? parsed2.data : void 0;
33620
+ }
33621
+ function withField(name, value) {
33622
+ return value === void 0 ? {} : { [name]: value };
33623
+ }
33624
+ function withList(name, values) {
33625
+ return values.length === 0 ? {} : { [name]: values };
33626
+ }
33627
+ function keepAll(value, schema) {
33628
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string" && schema.safeParse(v).success) : [];
33629
+ }
33630
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
33631
+ var TRACE_ID = EventMetadata.shape.traceId;
33632
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
33633
+ function isoOrUndefined(epochMs) {
33634
+ if (!Number.isFinite(epochMs)) return void 0;
33635
+ const iso = new Date(epochMs);
33636
+ return Number.isNaN(iso.getTime()) ? void 0 : iso.toISOString();
33637
+ }
33638
+ function parseAttributes(raw) {
33639
+ if (raw === null) return {};
33640
+ try {
33641
+ const parsed2 = JSON.parse(raw);
33642
+ return typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2) ? parsed2 : {};
33643
+ } catch {
33644
+ return {};
32892
33645
  }
32893
- return result.data;
32894
33646
  }
32895
- function withoutTrailingSlashes(endpoint) {
32896
- let end = endpoint.length;
32897
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
32898
- return endpoint.slice(0, end);
33647
+ function stringOrUndefined(value) {
33648
+ return typeof value === "string" && value.length > 0 ? value : void 0;
32899
33649
  }
32900
- var SLASH = "/".charCodeAt(0);
32901
- function createRemoteClient(options) {
32902
- const base = withoutTrailingSlashes(options.endpoint);
32903
- const url2 = (route) => `${base}${route}`;
32904
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
32905
- const sendOne = async (event) => {
32906
- const validated = RecordAuditEventRequest.safeParse(event);
32907
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
32908
- const response = await send({
32909
- ...common,
32910
- method: "POST",
32911
- url: url2(ROUTES.auditEvents),
32912
- body: JSON.stringify(validated.data)
32913
- });
32914
- okBody(response);
32915
- };
32916
- return {
32917
- async ingestEvents(batch) {
32918
- const response = await send({
32919
- ...common,
32920
- method: "POST",
32921
- url: url2(ROUTES.events),
32922
- body: JSON.stringify(batch)
32923
- });
32924
- return parsed(IngestAck, okBody(response), ROUTES.events);
32925
- },
32926
- async ingestInventory(context) {
32927
- const response = await send({
32928
- ...common,
32929
- method: "POST",
32930
- url: url2(ROUTES.inventory),
32931
- body: JSON.stringify(context)
32932
- });
32933
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
32934
- },
32935
- async recordAuditEvent(event) {
32936
- await sendOne(event);
32937
- },
32938
- async recordAuditEvents(events) {
32939
- const validated = RecordAuditEventBatch.safeParse({ events });
32940
- if (!validated.success) {
32941
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
32942
- }
32943
- const response = await send({
32944
- ...common,
32945
- method: "POST",
32946
- url: url2(ROUTES.auditEventsBatch),
32947
- body: JSON.stringify(validated.data)
32948
- });
32949
- if (response.status === 404) {
32950
- for (const event of validated.data.events) await sendOne(event);
32951
- return { accepted: validated.data.events.length };
32952
- }
32953
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
32954
- },
32955
- async reportStorePosture(snapshot) {
32956
- const response = await send({
32957
- ...common,
32958
- method: "POST",
32959
- url: url2(ROUTES.storePosture),
32960
- body: JSON.stringify(snapshot)
32961
- });
32962
- okBody(response);
32963
- },
32964
- async getPolicyBundle(etag) {
32965
- const response = await send({
32966
- ...common,
32967
- method: "GET",
32968
- url: url2(ROUTES.policyBundle),
32969
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
32970
- });
32971
- if (response.status === 304) {
32972
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
32973
- }
32974
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
32975
- return { changed: true, bundle, etag: headerValue(response, "etag") };
32976
- },
32977
- async whoami() {
32978
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
32979
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
32980
- },
32981
- async recordProjectEgress(request) {
32982
- const validated = EgressIngestRequest.safeParse(request);
32983
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
32984
- const response = await send({
32985
- ...common,
32986
- method: "POST",
32987
- url: url2(ROUTES.shares),
32988
- body: JSON.stringify(validated.data)
32989
- });
32990
- okBody(response);
32991
- }
32992
- };
33650
+ function pick2(attributes, mapping) {
33651
+ const out = {};
33652
+ for (const [wire, column] of Object.entries(mapping)) {
33653
+ const value = stringOrUndefined(attributes[column]);
33654
+ if (value !== void 0) out[wire] = value;
33655
+ }
33656
+ return out;
32993
33657
  }
32994
33658
 
32995
33659
  // ../../packages/plugin-runtime/src/attached/history-rebuild.ts
32996
33660
  var CAPTURE_VERSION_PREFIX2 = "capture/";
32997
33661
  function rebuildAuditEvent(row, inspections = []) {
32998
- const startedAt = isoOrUndefined(row.startedAt);
33662
+ const startedAt = isoOrUndefined2(row.startedAt);
32999
33663
  if (startedAt === void 0) return void 0;
33000
- const endedAt = isoOrUndefined(row.endedAt);
33664
+ const endedAt = isoOrUndefined2(row.endedAt);
33001
33665
  const candidate = {
33002
33666
  id: row.id,
33003
33667
  eventType: row.eventType,
@@ -33026,7 +33690,7 @@ function rebuildAuditEvent(row, inspections = []) {
33026
33690
  return parsed2.success ? parsed2.data : void 0;
33027
33691
  }
33028
33692
  var MAX_EPOCH_MS = 864e13;
33029
- function isoOrUndefined(ms) {
33693
+ function isoOrUndefined2(ms) {
33030
33694
  if (ms === null || ms === void 0 || !Number.isFinite(ms)) return void 0;
33031
33695
  if (Math.abs(ms) > MAX_EPOCH_MS) return void 0;
33032
33696
  return epochMillisToIso(ms);
@@ -33052,10 +33716,17 @@ var MAX_ATTEMPTS = 4;
33052
33716
  var MAX_BACKOFF_MS = 6e4;
33053
33717
  var SESSION_PAGE = 25;
33054
33718
  var ROW_PAGE = 200;
33719
+ var CAPTURE_GRACE_MS = 3e4;
33720
+ var CAPTURE_BATCH_SIZE = INGEST_BATCH_MAX;
33721
+ var STRUCTURAL_BUDGET_SHARE = 0.7;
33055
33722
  var BATCH_SIZE = AUDIT_EVENT_BATCH_MAX;
33056
33723
  function endpointFingerprint(endpoint) {
33057
33724
  return createHash6("sha256").update(endpoint).digest("hex");
33058
33725
  }
33726
+ var didNotRun = (reason) => ({
33727
+ attempted: false,
33728
+ reason
33729
+ });
33059
33730
  async function runHistorySync(deps) {
33060
33731
  const now = deps.now ?? (() => Date.now());
33061
33732
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -33064,19 +33735,23 @@ async function runHistorySync(deps) {
33064
33735
  let db;
33065
33736
  try {
33066
33737
  const settings = readWorkspaceSettings(deps.base);
33067
- if (!isAttached(settings)) return null;
33738
+ if (!isAttached(settings)) return didNotRun("not-attached");
33068
33739
  const connection = settings.controlPlane;
33069
- if (connection === void 0) return null;
33070
- if (!isHistorySyncConsentValid(settings.historySyncConsent, connection.endpoint)) return null;
33740
+ if (connection === void 0) return didNotRun("not-attached");
33741
+ if (!isHistorySyncConsentValid(settings.historySyncConsent, connection.endpoint)) {
33742
+ return didNotRun("no-consent");
33743
+ }
33071
33744
  const state = readControlPlaneCredentialFile(deps.settingsDir, connection);
33072
- if (!state.usable) return null;
33745
+ if (!state.usable) return didNotRun("credential-unusable");
33073
33746
  const nowMs = now();
33074
33747
  const openedAtMs = readForwardHealth(deps.dataDir, nowMs)?.openedAtMs ?? null;
33075
- if (openedAtMs !== null && nowMs - openedAtMs < BREAKER_COOLDOWN_MS) return null;
33748
+ if (openedAtMs !== null && nowMs - openedAtMs < BREAKER_COOLDOWN_MS) {
33749
+ return didNotRun("breaker-open");
33750
+ }
33076
33751
  db = (deps.openStore ?? openLocalDatabase)(deps.dataDir);
33077
33752
  const ledger = db.historySync;
33078
33753
  const attachedAtMs = Date.parse(connection.attachedAt);
33079
- if (!Number.isFinite(attachedAtMs)) return null;
33754
+ if (!Number.isFinite(attachedAtMs)) return didNotRun("attachment-unreadable");
33080
33755
  const fingerprint = endpointFingerprint(connection.endpoint);
33081
33756
  const recorded = ledger.deployment();
33082
33757
  let backlogBefore;
@@ -33090,24 +33765,46 @@ async function runHistorySync(deps) {
33090
33765
  backlogBefore = recorded.backlogBefore;
33091
33766
  }
33092
33767
  const pid = process.pid;
33093
- if (!ledger.claim(pid, hostname5(), now(), HISTORY_LEASE_STALE_MS)) return null;
33094
- const send2 = deps.sendBatch ?? (() => {
33095
- const client = createRemoteClient({
33096
- endpoint: connection.endpoint,
33097
- apiKey: state.credential.apiKey,
33098
- timeoutMs: HISTORY_REQUEST_TIMEOUT_MS
33099
- });
33100
- return async (events) => {
33101
- await client.recordAuditEvents(events);
33102
- };
33103
- })();
33768
+ if (!ledger.claim(pid, hostname5(), now(), HISTORY_LEASE_STALE_MS)) {
33769
+ return didNotRun("already-running");
33770
+ }
33771
+ const client = deps.sendBatch !== void 0 && deps.sendCaptures !== void 0 && deps.sendOne !== void 0 ? void 0 : createRemoteClient({
33772
+ endpoint: connection.endpoint,
33773
+ apiKey: state.credential.apiKey,
33774
+ timeoutMs: HISTORY_REQUEST_TIMEOUT_MS
33775
+ });
33776
+ const send2 = deps.sendBatch ?? (async (events) => {
33777
+ if (client === void 0) throw new Error("history sync: no transport");
33778
+ const ack = await client.recordAuditEvents(events, { fallbackToSingleEvents: true });
33779
+ return { settled: ack.accepted };
33780
+ });
33781
+ const sendOne = deps.sendOne ?? (async (event) => {
33782
+ if (client === void 0) throw new Error("history sync: no transport");
33783
+ await client.recordAuditEvent(event);
33784
+ });
33785
+ const sendCaptures = deps.sendCaptures ?? (async (events) => {
33786
+ if (client === void 0) throw new Error("history sync: no transport");
33787
+ const ack = await client.ingestEvents({ events: [...events] });
33788
+ return { settled: ack.accepted + ack.duplicates };
33789
+ });
33104
33790
  try {
33105
- return await drain({ ledger, send: send2, now, sleep, random, budgetMs, pid, backlogBefore });
33791
+ return await drain({
33792
+ ledger,
33793
+ send: send2,
33794
+ sendOne,
33795
+ sendCaptures,
33796
+ now,
33797
+ sleep,
33798
+ random,
33799
+ budgetMs,
33800
+ pid,
33801
+ backlogBefore
33802
+ });
33106
33803
  } finally {
33107
33804
  ledger.release(pid);
33108
33805
  }
33109
33806
  } catch {
33110
- return null;
33807
+ return didNotRun("failed");
33111
33808
  } finally {
33112
33809
  try {
33113
33810
  db?.close();
@@ -33118,76 +33815,207 @@ async function runHistorySync(deps) {
33118
33815
  async function drain(d) {
33119
33816
  const startedAt = d.now();
33120
33817
  const deadline = startedAt + d.budgetMs;
33818
+ const structuralDeadline = Math.min(deadline, startedAt + d.budgetMs * STRUCTURAL_BUDGET_SHARE);
33121
33819
  let sent = 0;
33122
33820
  let skipped = 0;
33123
33821
  let lastHeartbeat = startedAt;
33124
- let outcome = "ok";
33125
33822
  const beat = () => {
33126
33823
  const at = d.now();
33127
33824
  if (at - lastHeartbeat < HEARTBEAT_EVERY_MS) return;
33128
33825
  d.ledger.heartbeat(d.pid, at);
33129
33826
  lastHeartbeat = at;
33130
33827
  };
33131
- outer: while (d.now() < deadline) {
33132
- const sessions = d.ledger.pendingSessions(SESSION_PAGE, d.backlogBefore);
33133
- if (sessions.length === 0) break;
33134
- for (const sessionId of sessions) {
33135
- const rows = d.ledger.pendingRows(sessionId, ROW_PAGE, d.backlogBefore);
33136
- if (rows.length === 0) continue;
33137
- const ready = [];
33138
- for (const row of rows) {
33139
- const event = rebuildAuditEvent(row, d.ledger.inspectionsFor(row.id));
33140
- if (event === void 0) {
33141
- d.ledger.markSkipped([row.id]);
33142
- skipped += 1;
33143
- continue;
33144
- }
33145
- ready.push({ id: row.id, event });
33146
- }
33147
- for (let i = 0; i < ready.length; i += BATCH_SIZE) {
33148
- if (d.now() >= deadline) {
33149
- outcome = "interrupted";
33150
- break outer;
33828
+ const drainStructural = async (until) => {
33829
+ let stopped = "ok";
33830
+ outer: while (d.now() < until) {
33831
+ const sessions = d.ledger.pendingSessions(SESSION_PAGE, d.backlogBefore);
33832
+ if (sessions.length === 0) break;
33833
+ for (const sessionId of sessions) {
33834
+ const rows = d.ledger.pendingRows(sessionId, ROW_PAGE, d.backlogBefore);
33835
+ if (rows.length === 0) continue;
33836
+ const ready = [];
33837
+ for (const row of rows) {
33838
+ const event = rebuildAuditEvent(row, d.ledger.inspectionsFor(row.id));
33839
+ if (event === void 0) {
33840
+ d.ledger.markSkipped([row.id]);
33841
+ skipped += 1;
33842
+ continue;
33843
+ }
33844
+ ready.push({ id: row.id, event });
33151
33845
  }
33152
- const chunk = ready.slice(i, i + BATCH_SIZE);
33153
- const result = await sendChunk(d, chunk, beat);
33154
- sent += result.sent;
33155
- skipped += result.skipped;
33156
- if (result.stopped !== void 0) {
33157
- outcome = result.stopped;
33158
- break outer;
33846
+ for (let i = 0; i < ready.length; i += BATCH_SIZE) {
33847
+ if (d.now() >= until) {
33848
+ stopped = "interrupted";
33849
+ break outer;
33850
+ }
33851
+ const chunk = ready.slice(i, i + BATCH_SIZE);
33852
+ const result = await sendChunk(d, chunk, beat);
33853
+ sent += result.sent;
33854
+ skipped += result.skipped;
33855
+ if (result.stopped !== void 0) {
33856
+ stopped = result.stopped;
33857
+ break outer;
33858
+ }
33859
+ beat();
33860
+ await d.sleep(PACE_INTERVAL_MS);
33159
33861
  }
33160
- beat();
33161
- await d.sleep(PACE_INTERVAL_MS);
33162
33862
  }
33163
33863
  }
33864
+ return stopped;
33865
+ };
33866
+ let outcome = await drainStructural(structuralDeadline);
33867
+ if (outcome === "interrupted" && d.now() < deadline) outcome = "ok";
33868
+ if (outcome === "ok") {
33869
+ const captures = await drainCaptures(d, deadline, beat);
33870
+ sent += captures.sent;
33871
+ skipped += captures.skipped;
33872
+ if (captures.stopped !== void 0) outcome = captures.stopped;
33873
+ }
33874
+ if (outcome === "ok" && d.now() < deadline && d.ledger.pendingCaptureRows(1, d.now() - CAPTURE_GRACE_MS).length === 0) {
33875
+ outcome = await drainStructural(deadline);
33164
33876
  }
33165
33877
  if (outcome === "ok" && d.now() >= deadline && d.ledger.counts(d.backlogBefore).pending > 0) {
33166
33878
  outcome = "interrupted";
33167
33879
  }
33168
- return { outcome, sent, skipped, counts: d.ledger.counts(d.backlogBefore), atMs: d.now() };
33880
+ return {
33881
+ attempted: true,
33882
+ outcome,
33883
+ sent,
33884
+ skipped,
33885
+ // LIMIT 1 — this asks "is anything owed", never "how much", so it must not
33886
+ // pay for a count over the capture grain on every pass.
33887
+ capturesPending: d.ledger.pendingCaptureRows(1, d.now() - CAPTURE_GRACE_MS).length > 0,
33888
+ counts: d.ledger.counts(d.backlogBefore),
33889
+ atMs: d.now()
33890
+ };
33891
+ }
33892
+ async function drainCaptures(d, deadline, beat) {
33893
+ let sent = 0;
33894
+ let skipped = 0;
33895
+ for (; ; ) {
33896
+ if (d.now() >= deadline) return { sent, skipped, stopped: "interrupted" };
33897
+ const rows = d.ledger.pendingCaptureRows(CAPTURE_BATCH_SIZE, d.now() - CAPTURE_GRACE_MS);
33898
+ if (rows.length === 0) return { sent, skipped };
33899
+ const ready = [];
33900
+ const unbuildable = [];
33901
+ for (const row of rows) {
33902
+ const event = rebuildCapture(row);
33903
+ if (event === void 0) {
33904
+ unbuildable.push(row.id);
33905
+ continue;
33906
+ }
33907
+ ready.push({ id: row.id, event });
33908
+ }
33909
+ if (unbuildable.length > 0) {
33910
+ d.ledger.markSkipped(unbuildable);
33911
+ skipped += unbuildable.length;
33912
+ }
33913
+ if (ready.length === 0) {
33914
+ beat();
33915
+ await d.sleep(PACE_INTERVAL_MS);
33916
+ continue;
33917
+ }
33918
+ const result = await sendCaptureChunk(d, ready, beat);
33919
+ sent += result.sent;
33920
+ skipped += result.skipped;
33921
+ if (result.stopped !== void 0) return { sent, skipped, stopped: result.stopped };
33922
+ if (result.sent === 0 && result.skipped === 0) return { sent, skipped, stopped: "unreachable" };
33923
+ beat();
33924
+ await d.sleep(PACE_INTERVAL_MS);
33925
+ }
33169
33926
  }
33170
- async function sendChunk(d, chunk, beat) {
33171
- const verdict = await sendWithRetries(
33172
- d,
33173
- chunk.map((c) => c.event),
33174
- beat
33175
- );
33176
- if (verdict === "sent") {
33927
+ async function sendCaptureChunk(d, chunk, beat) {
33928
+ const outcome = await sendCapturesWithRetries(d, chunk, beat);
33929
+ if (outcome.verdict === "refused") return { sent: 0, skipped: 0, stopped: "refused" };
33930
+ if (outcome.verdict === "unreachable") return { sent: 0, skipped: 0, stopped: "unreachable" };
33931
+ if (outcome.verdict === "sent") {
33932
+ const settled = outcome.settled;
33933
+ if (settled < chunk.length) {
33934
+ if (chunk.length > 1) return await isolate(d, chunk, beat);
33935
+ return { sent: 0, skipped: 0, stopped: "unreachable" };
33936
+ }
33177
33937
  d.ledger.markSynced(
33178
33938
  chunk.map((c) => c.id),
33179
33939
  d.now()
33180
33940
  );
33181
33941
  return { sent: chunk.length, skipped: 0 };
33182
33942
  }
33183
- if (verdict === "refused" || verdict === "unreachable") {
33184
- return { sent: 0, skipped: 0, stopped: verdict };
33943
+ {
33944
+ const only = chunk.length === 1 ? chunk[0] : void 0;
33945
+ if (only !== void 0) {
33946
+ d.ledger.markSkipped([only.id]);
33947
+ return { sent: 0, skipped: 1 };
33948
+ }
33949
+ return await isolate(d, chunk, beat);
33950
+ }
33951
+ }
33952
+ async function isolate(d, chunk, beat) {
33953
+ let sent = 0;
33954
+ let skipped = 0;
33955
+ for (const [index, one] of chunk.entries()) {
33956
+ if (index > 0) await d.sleep(PACE_INTERVAL_MS);
33957
+ beat();
33958
+ const single = await sendCaptureChunk(d, [one], beat);
33959
+ sent += single.sent;
33960
+ skipped += single.skipped;
33961
+ if (single.stopped !== void 0) return { sent, skipped, stopped: single.stopped };
33962
+ }
33963
+ return { sent, skipped };
33964
+ }
33965
+ async function sendCapturesWithRetries(d, chunk, beat) {
33966
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
33967
+ try {
33968
+ const ack = await d.sendCaptures(chunk.map((c) => c.event));
33969
+ return { verdict: "sent", settled: ack.settled };
33970
+ } catch (err) {
33971
+ const kind = classify(err);
33972
+ if (kind === "refused") return { verdict: "refused" };
33973
+ if (kind === "skip") return { verdict: "skip" };
33974
+ if (attempt === MAX_ATTEMPTS - 1) return { verdict: "unreachable" };
33975
+ const ceiling = Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** attempt);
33976
+ beat();
33977
+ await d.sleep(Math.floor(d.random() * ceiling));
33978
+ }
33185
33979
  }
33980
+ return { verdict: "unreachable" };
33981
+ }
33982
+ async function sendChunk(d, chunk, beat) {
33186
33983
  const only = chunk.length === 1 ? chunk[0] : void 0;
33187
- if (only !== void 0) {
33188
- d.ledger.markSkipped([only.id]);
33189
- return { sent: 0, skipped: 1 };
33984
+ if (only !== void 0) return await sendSingleRow(d, only, beat);
33985
+ const outcome = await sendWithRetries(d, () => d.send(chunk.map((c) => c.event)), beat);
33986
+ if (outcome.verdict === "sent") {
33987
+ const settled = outcome.settled;
33988
+ if (settled < chunk.length) {
33989
+ return await isolateStructural(d, chunk, beat);
33990
+ }
33991
+ d.ledger.markSynced(
33992
+ chunk.map((c) => c.id),
33993
+ d.now()
33994
+ );
33995
+ return { sent: chunk.length, skipped: 0 };
33996
+ }
33997
+ if (outcome.verdict === "refused" || outcome.verdict === "unreachable") {
33998
+ return { sent: 0, skipped: 0, stopped: outcome.verdict };
33999
+ }
34000
+ return await isolateStructural(d, chunk, beat);
34001
+ }
34002
+ async function sendSingleRow(d, one, beat) {
34003
+ const outcome = await sendWithRetries(
34004
+ d,
34005
+ () => d.sendOne(one.event).then(() => ({ settled: 1 })),
34006
+ beat
34007
+ );
34008
+ if (outcome.verdict === "sent") {
34009
+ d.ledger.markSynced([one.id], d.now());
34010
+ return { sent: 1, skipped: 0 };
34011
+ }
34012
+ if (outcome.verdict === "refused" || outcome.verdict === "unreachable") {
34013
+ return { sent: 0, skipped: 0, stopped: outcome.verdict };
33190
34014
  }
34015
+ d.ledger.markSkipped([one.id]);
34016
+ return { sent: 0, skipped: 1 };
34017
+ }
34018
+ async function isolateStructural(d, chunk, beat) {
33191
34019
  let sent = 0;
33192
34020
  let skipped = 0;
33193
34021
  for (const [index, one] of chunk.entries()) {
@@ -33200,24 +34028,24 @@ async function sendChunk(d, chunk, beat) {
33200
34028
  }
33201
34029
  return { sent, skipped };
33202
34030
  }
33203
- async function sendWithRetries(d, events, beat) {
33204
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
34031
+ async function sendWithRetries(d, attempt, beat) {
34032
+ for (let i = 0; i < MAX_ATTEMPTS; i += 1) {
33205
34033
  try {
33206
- await d.send(events);
33207
- return "sent";
34034
+ const { settled } = await attempt();
34035
+ return { verdict: "sent", settled };
33208
34036
  } catch (err) {
33209
34037
  const kind = classify(err);
33210
- if (kind === "refused") return "refused";
33211
- if (kind === "skip") return "skip";
33212
- if (attempt === MAX_ATTEMPTS - 1) return "unreachable";
33213
- const ceiling = Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** attempt);
34038
+ if (kind === "refused") return { verdict: "refused" };
34039
+ if (kind === "skip") return { verdict: "skip" };
34040
+ if (i === MAX_ATTEMPTS - 1) return { verdict: "unreachable" };
34041
+ const ceiling = Math.min(MAX_BACKOFF_MS, 1e3 * 2 ** i);
33214
34042
  beat();
33215
34043
  await d.sleep(Math.floor(d.random() * ceiling));
33216
34044
  }
33217
34045
  }
33218
- return "unreachable";
34046
+ return { verdict: "unreachable" };
33219
34047
  }
33220
- function statusOf(err) {
34048
+ function statusOf2(err) {
33221
34049
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
33222
34050
  const { status } = err;
33223
34051
  if (typeof status !== "number" || !Number.isInteger(status)) return null;
@@ -33225,7 +34053,7 @@ function statusOf(err) {
33225
34053
  }
33226
34054
  function classify(err) {
33227
34055
  if (err.name === "RemoteRequestInvalid") return "skip";
33228
- switch (statusOf(err)) {
34056
+ switch (statusOf2(err)) {
33229
34057
  // Terminal in a way a timeout is not: the credential may have died with an
33230
34058
  // offboarded member, and every later row would fail the same way.
33231
34059
  case 401:
@@ -33243,33 +34071,58 @@ function classify(err) {
33243
34071
  }
33244
34072
 
33245
34073
  // ../../packages/plugin-runtime/src/attached/history-sync-entry.ts
33246
- async function runHistorySyncPass(base = defaultDataDir()) {
34074
+ async function runHistorySyncPass(base = defaultDataDir(), seams = {}) {
33247
34075
  try {
33248
34076
  const dir = dataDir(base);
33249
34077
  const result = await runHistorySync({
33250
34078
  base,
33251
34079
  settingsDir: settingsDir(base),
33252
- dataDir: dir
34080
+ dataDir: dir,
34081
+ ...seams
33253
34082
  });
33254
- if (result === null) return;
34083
+ if (!result.attempted) return result.reason;
33255
34084
  const previous = readHistorySyncState(dir);
33256
- const done = result.counts.pending === 0;
34085
+ const done = result.counts.pending === 0 && !result.capturesPending;
33257
34086
  writeHistorySyncState(dir, {
33258
34087
  phase: done ? "complete" : "filling",
33259
34088
  lastOutcome: result.outcome,
33260
34089
  lastPassAtMs: result.atMs,
33261
34090
  sentTotal: result.counts.sent,
33262
34091
  pendingTotal: result.counts.pending,
33263
- skippedTotal: result.counts.skipped,
34092
+ // BOTH lanes, and BOTH lifetime. `counts.skipped` filters to structural
34093
+ // rows, so a capture rebuildCapture refused — stamped -1 and dropped for
34094
+ // ever — was counted on no surface at all. Silence is the right shape for a
34095
+ // TRANSIENT failure everywhere else in this repo; this one is terminal, so
34096
+ // it owes a number.
34097
+ //
34098
+ // Both terms are ledger totals rather than this pass's tally, which is the
34099
+ // part that matters: a per-pass delta added to a lifetime total gives a
34100
+ // field whose capture half is one pass wide, so the surface beside
34101
+ // `sentTotal` and `pendingTotal` would announce a permanent loss once and
34102
+ // drop it on the next pass, while the rows stayed gone.
34103
+ skippedTotal: result.counts.skipped + result.counts.capturesSkipped,
33264
34104
  // The first pass that ran is when this machine started sending, and it
33265
34105
  // keeps that answer across every later pass.
33266
34106
  startedAtMs: previous?.startedAtMs ?? result.atMs,
33267
- // Stamped when the drain empties. The backlog is a FIXED set everything
33268
- // recorded before the machine attached — so unlike a tally over a store
33269
- // that keeps growing, this one genuinely finishes and stays finished.
33270
- completedAtMs: done ? previous?.completedAtMs ?? result.atMs : null
34107
+ // Stamped the first time BOTH lanes were empty. The structural backlog is
34108
+ // a fixed set — everything recorded before the machine attached — and it
34109
+ // genuinely finishes. The capture lane does not: its subject grows with
34110
+ // every live session that fails to forward, so `phase` can go back to
34111
+ // 'filling' after reading 'complete'. This keeps its original meaning
34112
+ // either way — the first moment this machine owed the deployment nothing —
34113
+ // which is why it is pinned rather than recomputed.
34114
+ // WRITTEN ONCE, on the false→true transition, and never cleared. Under v1
34115
+ // this was monotone because the structural lane only ever drained; the
34116
+ // capture lane is what makes `done` flap, and clearing on every flap would
34117
+ // erase the pin and re-stamp it on the next catch-up — so a consumer
34118
+ // reading "when this machine first caught up" would get the most recent
34119
+ // one instead. Carrying the previous value through the false case is what
34120
+ // keeps the original meaning.
34121
+ completedAtMs: previous?.completedAtMs ?? (done ? result.atMs : null)
33271
34122
  });
34123
+ return result.outcome;
33272
34124
  } catch {
34125
+ return "failed";
33273
34126
  }
33274
34127
  }
33275
34128
 
@@ -33279,12 +34132,12 @@ import { fileURLToPath as fileURLToPath2 } from "url";
33279
34132
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
33280
34133
 
33281
34134
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
33282
- import { readFileSync as readFileSync15 } from "fs";
34135
+ import { readFileSync as readFileSync16 } from "fs";
33283
34136
 
33284
34137
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
33285
34138
  import { randomUUID as randomUUID16 } from "crypto";
33286
34139
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
33287
- import { join as join23 } from "path";
34140
+ import { join as join24 } from "path";
33288
34141
 
33289
34142
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
33290
34143
  import { rename as rename2 } from "fs/promises";
@@ -33299,11 +34152,11 @@ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
33299
34152
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
33300
34153
  import { randomUUID as randomUUID17 } from "crypto";
33301
34154
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
33302
- import { join as join24 } from "path";
34155
+ import { join as join25 } from "path";
33303
34156
 
33304
34157
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
33305
- import { readFileSync as readFileSync16 } from "fs";
33306
- import { join as join25 } from "path";
34158
+ import { readFileSync as readFileSync17 } from "fs";
34159
+ import { join as join26 } from "path";
33307
34160
 
33308
34161
  // ../../packages/plugin-runtime/src/attached/status.ts
33309
34162
  var REFUSAL_LINES = {