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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -494,6 +494,7 @@ var require_ignore = __commonJS({
494
494
  // ../../packages/persistence/src/attached-derived.ts
495
495
  import { rmSync } from "fs";
496
496
  import { join } from "path";
497
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
497
498
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
498
499
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
499
500
 
@@ -594,6 +595,30 @@ var SQLITE_MIGRATIONS = [
594
595
  {
595
596
  tag: "0022_audit_inspection_ms",
596
597
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
598
+ },
599
+ {
600
+ tag: "0023_secret_vault_user_authorized",
601
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
602
+ },
603
+ {
604
+ tag: "0024_finding_resolution_key_created_index",
605
+ 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`);"
606
+ },
607
+ {
608
+ tag: "0025_audit_capture_attribute_columns",
609
+ 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;"
610
+ },
611
+ {
612
+ tag: "0026_audit_llm_call_usage_columns",
613
+ 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;"
614
+ },
615
+ {
616
+ tag: "0027_audit_llm_usage_index",
617
+ 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;"
618
+ },
619
+ {
620
+ tag: "0028_activity_session_probe_indexes",
621
+ 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"
597
622
  }
598
623
  ];
599
624
 
@@ -22133,6 +22158,26 @@ var AttachTokenResponse = external_exports.union([
22133
22158
  AttachTokenExpired,
22134
22159
  external_exports.object({ status: printable(64) })
22135
22160
  ]);
22161
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22162
+ var DeviceCommand = external_exports.object({
22163
+ id: printable(128).min(1),
22164
+ kind: DeviceCommandKind,
22165
+ issuedAt: printable(64).min(1),
22166
+ expiresAt: printable(64).min(1)
22167
+ }).strict();
22168
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22169
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22170
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22171
+ external_exports.object({
22172
+ outcome: external_exports.literal("reported"),
22173
+ projectsScanned: external_exports.number().int().nonnegative()
22174
+ }).strict(),
22175
+ external_exports.object({
22176
+ outcome: external_exports.literal("failed"),
22177
+ reason: DeviceCommandFailureReason,
22178
+ projectsScanned: external_exports.number().int().nonnegative()
22179
+ }).strict()
22180
+ ]);
22136
22181
 
22137
22182
  // ../../packages/schema/src/zod/registry.ts
22138
22183
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22299,7 +22344,7 @@ var PackManifest = external_exports.object({
22299
22344
  }).meta({ id: "PackManifest" });
22300
22345
 
22301
22346
  // ../../packages/schema/src/zod/detection.ts
22302
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22347
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22303
22348
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22304
22349
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22305
22350
  var DetectionCounts = external_exports.object({
@@ -22436,14 +22481,17 @@ function optional2(key, parsed2, raw) {
22436
22481
  function isStringArray(value) {
22437
22482
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22438
22483
  }
22484
+ var ORIGIN_VALUES = { library: true, custom: true };
22485
+ function resolveOrigin(origin) {
22486
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22487
+ }
22439
22488
  function summaryToDetectionListItem(s) {
22440
22489
  return {
22441
22490
  id: `${s.namespace}/${s.packId}`,
22442
22491
  name: s.name,
22443
22492
  version: s.version,
22444
22493
  enabled: s.enabled,
22445
- origin: "library",
22446
- // v1: every installed pack is library origin
22494
+ origin: resolveOrigin(s.origin),
22447
22495
  namespace: s.namespace,
22448
22496
  packId: s.packId,
22449
22497
  ruleCount: s.ruleCount,
@@ -22495,7 +22543,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22495
22543
  name: row.name,
22496
22544
  version: row.version,
22497
22545
  enabled: row.enabled,
22498
- origin: "library",
22546
+ origin: resolveOrigin(row.origin),
22499
22547
  namespace: row.namespace,
22500
22548
  packId: row.packId,
22501
22549
  ruleCount: row.rules.length,
@@ -22515,16 +22563,20 @@ function splitDetectionId(id) {
22515
22563
  }
22516
22564
  function buildDetectionsList(summaries, query) {
22517
22565
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22566
+ const originOf = (s) => resolveOrigin(s.origin);
22518
22567
  const counts = {
22519
22568
  all: summaries.length,
22520
- library: summaries.length,
22521
- // all origin=library in v1
22522
- custom: 0,
22569
+ library: summaries.filter((s) => originOf(s) === "library").length,
22570
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22571
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22572
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22573
+ // place, and that state does not exist — editing a library pack forks it. See
22574
+ // OriginEnum.
22523
22575
  customized: 0,
22524
22576
  updates: withUpdate.length
22525
22577
  };
22526
22578
  const filter = query.filter;
22527
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22579
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22528
22580
  if (query.q) {
22529
22581
  const q = query.q.toLowerCase();
22530
22582
  filtered = filtered.filter(
@@ -22604,8 +22656,9 @@ var Event = external_exports.object({
22604
22656
  metadata: EventMetadata.optional()
22605
22657
  }).meta({ id: "Event" });
22606
22658
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22659
+ var INGEST_BATCH_MAX = 100;
22607
22660
  var IngestBatch = external_exports.object({
22608
- events: external_exports.array(IngestEvent).min(1).max(100),
22661
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22609
22662
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22610
22663
  // additionally rejects any event whose contentHash the store has already
22611
22664
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23161,372 +23214,11 @@ var PatchInstalledPackRequest = external_exports.object({
23161
23214
  message: "At least one field must be provided"
23162
23215
  }).meta({ id: "PatchInstalledPackRequest" });
23163
23216
 
23164
- // ../../packages/schema/src/zod/vault.ts
23165
- var POINTER_FORMAT_VERSION = 2;
23166
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23167
- var POINTER_TOKEN_PATTERN = new RegExp(
23168
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23169
- );
23170
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23171
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23172
- var ParsedPointer = external_exports.object({
23173
- category: DetectionCategory,
23174
- keyVersion: external_exports.number().int().positive(),
23175
- pointerId: external_exports.string(),
23176
- tag: external_exports.string()
23177
- });
23178
- var VaultEntry = external_exports.object({
23179
- pointerId: external_exports.string(),
23180
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23181
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23182
- // independently of the vault encryption key below.
23183
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23184
- fingerprintKeyVersion: external_exports.number().int().positive(),
23185
- // The vault-key epoch this row's ciphertext was sealed under.
23186
- keyVersion: external_exports.number().int().positive(),
23187
- // Fixed at first mint and never updated: the same value detected later under a
23188
- // different rule's category keeps the category it was minted with, so one
23189
- // value always produces exactly one wire token.
23190
- category: DetectionCategory,
23191
- ruleId: external_exports.string(),
23192
- // Partial-reveal preview for badges and listings. Never the raw value.
23193
- maskedMatch: external_exports.string(),
23194
- provider: external_exports.string().optional(),
23195
- ciphertext: external_exports.string(),
23196
- nonce: external_exports.string(),
23197
- authTag: external_exports.string(),
23198
- // How many times this value has been detected on this machine — the reuse
23199
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23200
- occurrenceCount: external_exports.number().int().nonnegative(),
23201
- firstSeen: external_exports.string(),
23202
- lastSeen: external_exports.string()
23203
- });
23204
- var PointerDescriptor = external_exports.object({
23205
- category: DetectionCategory,
23206
- provider: external_exports.string().optional(),
23207
- maskedMatch: external_exports.string(),
23208
- occurrences: external_exports.number().int().nonnegative(),
23209
- firstSeen: external_exports.string(),
23210
- lastSeen: external_exports.string()
23211
- });
23212
- var PointerIdentity = external_exports.object({
23213
- ruleId: external_exports.string(),
23214
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23215
- fingerprintKeyVersion: external_exports.number().int().positive()
23216
- });
23217
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23218
- var VaultDerefReason = external_exports.enum([
23219
- "display",
23220
- "explicit-reveal",
23221
- "view-render",
23222
- "model-input",
23223
- "remediation",
23224
- "purge"
23225
- ]);
23226
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23227
- var VaultDeref = external_exports.object({
23228
- id: external_exports.guid(),
23229
- pointerId: external_exports.string(),
23230
- at: external_exports.string(),
23231
- target: DetokenizeTarget,
23232
- reason: VaultDerefReason,
23233
- outcome: VaultDerefOutcome,
23234
- // Present only on a model-target crossing that a reveal grant authorized.
23235
- grantId: external_exports.string().optional(),
23236
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23237
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23238
- pointerCount: external_exports.number().int().positive().default(1)
23239
- });
23240
- var VaultSightingKind = external_exports.enum([
23241
- "prompt",
23242
- "tool-input",
23243
- "tool-output",
23244
- "file",
23245
- "transcript"
23246
- ]);
23247
- var VaultSighting = external_exports.object({
23248
- location: external_exports.string(),
23249
- kind: VaultSightingKind,
23250
- firstSeen: external_exports.string(),
23251
- lastSeen: external_exports.string()
23252
- });
23253
- var VaultInventoryEntry = external_exports.object({
23254
- pointerId: external_exports.string(),
23255
- category: DetectionCategory,
23256
- provider: external_exports.string().optional(),
23257
- maskedMatch: external_exports.string(),
23258
- occurrences: external_exports.number().int().nonnegative(),
23259
- firstSeen: external_exports.string(),
23260
- lastSeen: external_exports.string(),
23261
- // The active reveal-to-model grant covering this value, when one exists —
23262
- // the inventory badges it, the row links to revocation.
23263
- revealGrantId: external_exports.string().nullable(),
23264
- sightings: external_exports.array(VaultSighting)
23265
- });
23266
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23267
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23268
- var MAX_VAULT_PAGE_LIMIT = 200;
23269
- var ListVaultInventoryQuery = external_exports.object({
23270
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23271
- // Opaque; names the last row of the page just served.
23272
- cursor: external_exports.string().optional()
23273
- });
23274
- var ListVaultInventoryResponse = external_exports.object({
23275
- // Vaulted values across the whole store, not just this page — cursor-
23276
- // independent, so paging never changes what the count claims.
23277
- totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23278
- items: external_exports.array(VaultInventoryEntry),
23279
- // `null` once the last page is reached.
23280
- nextCursor: external_exports.string().nullable()
23281
- });
23282
- var ListVaultReuseQuery = external_exports.object({
23283
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23284
- cursor: external_exports.string().optional()
23285
- });
23286
- var ListVaultReuseResponse = external_exports.object({
23287
- // Reused values across the whole store — the number the section's claim
23288
- // ("values detected in more than one place") is about.
23289
- totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23290
- items: external_exports.array(VaultInventoryEntry),
23291
- nextCursor: external_exports.string().nullable()
23292
- });
23293
- var ListVaultDerefsQuery = external_exports.object({
23294
- // Include the batched, high-volume reasons (display, view-render). Omitted
23295
- // hides them and counts them into `hiddenBatched` instead, so the model
23296
- // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23297
- // over a Server Action, which preserves the type, never as a URL param.
23298
- includeBatched: external_exports.boolean().optional(),
23299
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23300
- cursor: external_exports.string().optional()
23301
- });
23302
- var ListVaultDerefsResponse = external_exports.object({
23303
- items: external_exports.array(VaultDeref),
23304
- nextCursor: external_exports.string().nullable(),
23305
- // Display/view-render rows the query hid, over the WHOLE trail rather than
23306
- // this page — it is the count the "N hidden" line and its toggle speak for.
23307
- // Always 0 when `includeBatched` was set, since nothing was hidden.
23308
- hiddenBatched: external_exports.number().int().nonnegative()
23309
- });
23310
- var VaultKeyCustody = external_exports.string();
23311
- var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23312
- var VAULT_CONSENT_VERSION = 1;
23313
- var VaultConsent = external_exports.object({
23314
- acknowledgedAt: external_exports.iso.datetime(),
23315
- version: external_exports.number().int().positive()
23316
- });
23317
-
23318
- // ../../packages/schema/src/zod/local.ts
23319
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23320
- var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23321
- var RunMode = external_exports.enum(["standalone", "attached"]);
23322
- var ControlPlaneConnection = external_exports.object({
23323
- endpoint: external_exports.string().min(1),
23324
- // Display name for the deployment, shown instead of the raw endpoint.
23325
- label: external_exports.string().min(1).optional(),
23326
- attachedAt: external_exports.iso.datetime()
23327
- }).meta({ id: "ControlPlaneConnection" });
23328
- var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23329
- var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23330
- var ModelJudgeConsent = external_exports.object({
23331
- acknowledgedAt: external_exports.iso.datetime(),
23332
- payloadVersion: external_exports.number().int().positive()
23333
- });
23334
- var HistorySyncConsent = external_exports.object({
23335
- acknowledgedAt: external_exports.iso.datetime(),
23336
- payloadVersion: external_exports.number().int().positive(),
23337
- endpoint: external_exports.string()
23338
- });
23339
- var WorkspaceSettings = external_exports.object({
23340
- specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23341
- runMode: RunMode.default("standalone"),
23342
- // Present only while attached; a detach clears it. Its presence is what makes
23343
- // `runMode: 'attached'` mean anything — see isAttached.
23344
- controlPlane: ControlPlaneConnection.optional(),
23345
- policy: SimpleDetectionPolicy.default("redact"),
23346
- // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23347
- historicalAccess: HistoricalAccess.default("session-only"),
23348
- // In-place egress extraction on the scan paths; disable to stop all Data
23349
- // Shares writes.
23350
- dataSharesInPlace: external_exports.boolean().default(true),
23351
- // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23352
- // vault, instead of destroying them. Absent by default: this is a custody
23353
- // change from one-way redaction, so it is never an assumed grant on upgrade.
23354
- // Revoking stops future vaulting; it does not erase what is already stored —
23355
- // purging the vault is the eraser.
23356
- vaultConsent: VaultConsent.optional(),
23357
- // Where the vault master key lives.
23358
- vaultKeyCustody: VaultKeyCustody.default("file"),
23359
- // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23360
- vaultInlineReveal: VaultInlineReveal.default("masked"),
23361
- // Absent until /aka:setup completes; its presence is what "onboarded" means.
23362
- onboardedAt: external_exports.iso.datetime().optional(),
23363
- // Records that the user consented to sending findings to the model API for
23364
- // the /aka:setup judge, along with the payload-shape version they agreed to.
23365
- // Absent until granted; a stale payloadVersion means the consent no longer
23366
- // covers the current payload and must be re-granted.
23367
- modelJudgeConsent: ModelJudgeConsent.optional(),
23368
- // Records that the user consented to sending the activity already recorded on
23369
- // this machine to the deployment it is attached to, along with the payload
23370
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23371
- // a different endpoint or an older payload no longer counts.
23372
- historySyncConsent: HistorySyncConsent.optional()
23373
- });
23374
- function defaultWorkspaceSettings() {
23375
- return WorkspaceSettings.parse({});
23376
- }
23377
- function isAttached(settings) {
23378
- return settings.runMode === "attached" && settings.controlPlane !== void 0;
23379
- }
23380
- function toInventoryRow(input2, id, now) {
23381
- return {
23382
- id,
23383
- objectType: input2.objectType,
23384
- location: input2.location ?? null,
23385
- title: input2.title ?? null,
23386
- hostId: input2.hostId ?? null,
23387
- attributes: JSON.stringify(input2.attributes),
23388
- firstSeen: now,
23389
- lastSeen: now
23390
- };
23391
- }
23392
- function toSourceProjectRow(input2, id, now) {
23393
- return {
23394
- id,
23395
- url: input2.url,
23396
- name: input2.name ?? null,
23397
- attributes: JSON.stringify(input2.attributes),
23398
- firstSeen: now,
23399
- lastSeen: now
23400
- };
23401
- }
23402
- function toAuditEventRow(input2) {
23403
- return {
23404
- id: input2.id,
23405
- parentId: input2.parentId ?? null,
23406
- rootSessionId: input2.rootSessionId ?? null,
23407
- eventType: input2.eventType,
23408
- hostId: input2.hostId ?? null,
23409
- harnessId: input2.harnessId ?? null,
23410
- sourceProjectId: input2.sourceProjectId ?? null,
23411
- startedAt: isoToEpochMillis(input2.startedAt),
23412
- endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23413
- severity: input2.severity ?? null,
23414
- priority: input2.priority ?? null,
23415
- content: input2.content ?? null,
23416
- contentHash: input2.contentHash ?? null,
23417
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23418
- };
23419
- }
23420
- function toClassifiedDataRow(input2, id) {
23421
- return {
23422
- id,
23423
- class: input2.class,
23424
- label: input2.label ?? null,
23425
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23426
- };
23427
- }
23428
- function toInspectionDefinitionRow(input2, id) {
23429
- return {
23430
- id,
23431
- ruleId: input2.ruleId,
23432
- name: input2.name,
23433
- category: input2.category,
23434
- severity: input2.severity,
23435
- definition: input2.definition,
23436
- version: input2.version
23437
- };
23438
- }
23439
- function toInspectionFindingRow(input2) {
23440
- return {
23441
- id: input2.id,
23442
- auditEventId: input2.auditEventId,
23443
- inspectionDefinitionId: input2.inspectionDefinitionId,
23444
- classifiedDataId: input2.classifiedDataId ?? null,
23445
- spanStart: input2.span.start,
23446
- spanEnd: input2.span.end,
23447
- maskedMatch: input2.maskedMatch,
23448
- actionTaken: input2.actionTaken,
23449
- confidence: input2.confidence,
23450
- findingKey: input2.findingKey ?? null,
23451
- firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23452
- };
23453
- }
23454
- function toCaptureAttributes(event) {
23455
- const metadata = event.metadata;
23456
- return {
23457
- source_tool: event.sourceTool,
23458
- ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23459
- ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23460
- ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23461
- ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23462
- ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23463
- ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23464
- ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23465
- ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23466
- ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23467
- // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23468
- // has ever populated either), but every legacy metadata key still rides
23469
- // the bag rather than being silently dropped — CaptureAttributes'
23470
- // `.catchall(z.unknown())` carries the long tail.
23471
- ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23472
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23473
- };
23474
- }
23475
- function captureDefinitionVersion(finding) {
23476
- return `capture/${finding.category}/${finding.severity}`;
23477
- }
23478
- function toCaptureDefinitionInput(finding) {
23479
- return {
23480
- ruleId: finding.ruleId,
23481
- version: captureDefinitionVersion(finding),
23482
- name: finding.ruleId,
23483
- category: finding.category,
23484
- severity: finding.severity,
23485
- definition: JSON.stringify({ ruleId: finding.ruleId })
23486
- };
23487
- }
23488
-
23489
- // ../../packages/schema/src/zod/managed.ts
23490
- var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23491
- var MANAGED_SETTINGS_SPEC_VERSION = 1;
23492
- var ManagedSettingKey = external_exports.enum([
23493
- "runMode",
23494
- "historicalAccess",
23495
- "vaultConsent",
23496
- "vaultKeyCustody",
23497
- "vaultInlineReveal",
23498
- "modelJudgeConsent",
23499
- "dataSharesInPlace"
23500
- ]).meta({ id: "ManagedSettingKey" });
23501
- var ManagedSettingsValues = external_exports.object({
23502
- runMode: external_exports.enum(["standalone", "attached"]).optional(),
23503
- controlPlane: external_exports.object({
23504
- endpoint: external_exports.string().min(1),
23505
- label: external_exports.string().min(1).optional()
23506
- }).optional(),
23507
- historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23508
- vaultConsent: external_exports.boolean().optional(),
23509
- vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23510
- vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23511
- modelJudgeConsent: external_exports.boolean().optional(),
23512
- dataSharesInPlace: external_exports.boolean().optional()
23513
- }).meta({ id: "ManagedSettingsValues" });
23514
- var ManagedSettings = external_exports.object({
23515
- specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23516
- // Shown on every locked control, so the user can tell an administrative
23517
- // decision from a bug. Absent renders as a generic "your organization".
23518
- organization: external_exports.string().min(1).optional(),
23519
- // What the administrator pinned.
23520
- values: ManagedSettingsValues.default({}),
23521
- // Which of those the user may not change. A key here with no matching value
23522
- // freezes whatever the user last chose; a value with no lock is a DEFAULT
23523
- // the user may still override. The two are separable on purpose.
23524
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23525
- }).meta({ id: "ManagedSettings" });
23526
-
23527
23217
  // ../../packages/schema/src/zod/policy.ts
23528
23218
  var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23529
23219
  var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23220
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23221
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23530
23222
  var Policy = external_exports.object({
23531
23223
  id: external_exports.guid(),
23532
23224
  scope: PolicyScope,
@@ -23536,7 +23228,27 @@ var Policy = external_exports.object({
23536
23228
  customKeywords: external_exports.array(external_exports.string()).optional(),
23537
23229
  // Display name — optional so older policy rows without name still parse.
23538
23230
  // Added for the findings API (policy.name column migration).
23539
- name: external_exports.string().optional()
23231
+ name: external_exports.string().optional(),
23232
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23233
+ // which row this is. A producer that collapses several rows onto one target
23234
+ // must carry the marker onto whichever row survives, or the collapse decides
23235
+ // the answer; a survivor may therefore be a built-in expansion still marked
23236
+ // 'authored' because an authored sibling targeted the same thing.
23237
+ // Optional so an older producer — and an older on-disk cache — still parses;
23238
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23239
+ //
23240
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23241
+ // built-in archetype catalog entry a policy is, which every catalog surface
23242
+ // reads and which a caller may state. This one is a statement the PRODUCER
23243
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23244
+ // — the CRUD routes neither accept nor set it.
23245
+ //
23246
+ // A device consumes this in exactly one direction: an 'authored' policy
23247
+ // arriving from a control plane marks the rules it targets as not
23248
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23249
+ // which is what makes it safe to honour from an unsigned cache — the same
23250
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23251
+ provenance: PolicyProvenance.optional()
23540
23252
  }).meta({ id: "Policy" });
23541
23253
  var PolicyBundle = external_exports.object({
23542
23254
  version: external_exports.string(),
@@ -23588,6 +23300,12 @@ var PolicyBundle = external_exports.object({
23588
23300
  customKeywords: external_exports.array(external_exports.string()),
23589
23301
  fetchedAt: external_exports.iso.datetime()
23590
23302
  }).meta({ id: "PolicyBundle" });
23303
+ var POLICY_BUNDLE_SHAPE_ID = [
23304
+ ...Object.keys(PolicyBundle.shape),
23305
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23306
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23307
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23308
+ ].sort().join(",");
23591
23309
  var OBSERVE_ONLY_CATEGORIES = ["config"];
23592
23310
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23593
23311
  var CATEGORY_PEAK_SEVERITY = {
@@ -23608,9 +23326,11 @@ function severityFloorPolicy(category) {
23608
23326
  const peak = CATEGORY_PEAK_SEVERITY[category];
23609
23327
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23610
23328
  }
23611
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23612
23329
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23613
23330
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23331
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23332
+ id: "RedactFallback"
23333
+ });
23614
23334
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23615
23335
  var BUILTIN_POLICY_SPECS = {
23616
23336
  monitor: {
@@ -23647,6 +23367,42 @@ var BUILTIN_POLICY_SPECS = {
23647
23367
  function builtinPolicyToAction(id) {
23648
23368
  return BUILTIN_POLICY_SPECS[id].action;
23649
23369
  }
23370
+ var PALETTE_WEAKEST_FIRST = [
23371
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23372
+ ];
23373
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23374
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23375
+ );
23376
+ var ACTION_STRENGTH_ORDER = [
23377
+ ...BELOW_PALETTE,
23378
+ ...PALETTE_WEAKEST_FIRST
23379
+ ];
23380
+ function actionRank(action) {
23381
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23382
+ }
23383
+ function isActionAtLeast(action, floor) {
23384
+ return actionRank(action) >= actionRank(floor);
23385
+ }
23386
+ function strongerAction(a, b) {
23387
+ return actionRank(a) >= actionRank(b) ? a : b;
23388
+ }
23389
+ function weakestBuiltinAtLeast(floor) {
23390
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23391
+ }
23392
+ var PackPolicyFloor = external_exports.object({
23393
+ /**
23394
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23395
+ * rather than a raw ActionTaken because that is the vocabulary the user
23396
+ * picks from — a floor a UI cannot name is one it cannot explain.
23397
+ */
23398
+ floor: BuiltinPolicyId,
23399
+ /**
23400
+ * True when the organization AUTHORED a policy governing this pack rather
23401
+ * than stating a minimum: it gave the answer, so the pack is not
23402
+ * re-assignable locally in either direction.
23403
+ */
23404
+ locked: external_exports.boolean()
23405
+ }).describe("PackPolicyFloor");
23650
23406
  var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23651
23407
  (id) => !BUILTIN_POLICY_SPECS[id].reversible
23652
23408
  );
@@ -23704,6 +23460,394 @@ var PolicyStatsResponse = external_exports.object({
23704
23460
  detectionsGoverned: external_exports.number().int().nonnegative()
23705
23461
  }).meta({ id: "PolicyStatsResponse" });
23706
23462
 
23463
+ // ../../packages/schema/src/zod/vault.ts
23464
+ var POINTER_FORMAT_VERSION = 2;
23465
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23466
+ var POINTER_TOKEN_PATTERN = new RegExp(
23467
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23468
+ );
23469
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23470
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23471
+ var ParsedPointer = external_exports.object({
23472
+ category: DetectionCategory,
23473
+ keyVersion: external_exports.number().int().positive(),
23474
+ pointerId: external_exports.string(),
23475
+ tag: external_exports.string()
23476
+ });
23477
+ var VaultEntry = external_exports.object({
23478
+ pointerId: external_exports.string(),
23479
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23480
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23481
+ // independently of the vault encryption key below.
23482
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23483
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23484
+ // The vault-key epoch this row's ciphertext was sealed under.
23485
+ keyVersion: external_exports.number().int().positive(),
23486
+ // Fixed at first mint and never updated: the same value detected later under a
23487
+ // different rule's category keeps the category it was minted with, so one
23488
+ // value always produces exactly one wire token.
23489
+ category: DetectionCategory,
23490
+ ruleId: external_exports.string(),
23491
+ // Partial-reveal preview for badges and listings. Never the raw value.
23492
+ maskedMatch: external_exports.string(),
23493
+ provider: external_exports.string().optional(),
23494
+ ciphertext: external_exports.string(),
23495
+ nonce: external_exports.string(),
23496
+ authTag: external_exports.string(),
23497
+ // How many times this value has been detected on this machine — the reuse
23498
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23499
+ occurrenceCount: external_exports.number().int().nonnegative(),
23500
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23501
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23502
+ // one row however many paths vault it, so this is what tells a policy sweep
23503
+ // that the row carries somebody's own instruction and not just an assignment
23504
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23505
+ // vaulting of the same value must never clear it — what the user said about
23506
+ // the value does not expire.
23507
+ userAuthorized: external_exports.boolean(),
23508
+ firstSeen: external_exports.string(),
23509
+ lastSeen: external_exports.string()
23510
+ });
23511
+ var PointerDescriptor = external_exports.object({
23512
+ category: DetectionCategory,
23513
+ provider: external_exports.string().optional(),
23514
+ maskedMatch: external_exports.string(),
23515
+ occurrences: external_exports.number().int().nonnegative(),
23516
+ firstSeen: external_exports.string(),
23517
+ lastSeen: external_exports.string()
23518
+ });
23519
+ var PointerIdentity = external_exports.object({
23520
+ ruleId: external_exports.string(),
23521
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23522
+ fingerprintKeyVersion: external_exports.number().int().positive()
23523
+ });
23524
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23525
+ var VaultDerefReason = external_exports.enum([
23526
+ "display",
23527
+ "explicit-reveal",
23528
+ "view-render",
23529
+ "model-input",
23530
+ "remediation",
23531
+ "purge"
23532
+ ]);
23533
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23534
+ var VaultDeref = external_exports.object({
23535
+ id: external_exports.guid(),
23536
+ pointerId: external_exports.string(),
23537
+ at: external_exports.string(),
23538
+ target: DetokenizeTarget,
23539
+ reason: VaultDerefReason,
23540
+ outcome: VaultDerefOutcome,
23541
+ // Present only on a model-target crossing that a reveal grant authorized.
23542
+ grantId: external_exports.string().optional(),
23543
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23544
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23545
+ pointerCount: external_exports.number().int().positive().default(1)
23546
+ });
23547
+ var VaultSightingKind = external_exports.enum([
23548
+ "prompt",
23549
+ "tool-input",
23550
+ "tool-output",
23551
+ "file",
23552
+ "transcript"
23553
+ ]);
23554
+ var VaultSighting = external_exports.object({
23555
+ location: external_exports.string(),
23556
+ kind: VaultSightingKind,
23557
+ firstSeen: external_exports.string(),
23558
+ lastSeen: external_exports.string()
23559
+ });
23560
+ var VaultInventoryEntry = external_exports.object({
23561
+ pointerId: external_exports.string(),
23562
+ category: DetectionCategory,
23563
+ provider: external_exports.string().optional(),
23564
+ maskedMatch: external_exports.string(),
23565
+ occurrences: external_exports.number().int().nonnegative(),
23566
+ firstSeen: external_exports.string(),
23567
+ lastSeen: external_exports.string(),
23568
+ // The active reveal-to-model grant covering this value, when one exists —
23569
+ // the inventory badges it, the row links to revocation.
23570
+ revealGrantId: external_exports.string().nullable(),
23571
+ sightings: external_exports.array(VaultSighting)
23572
+ });
23573
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23574
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23575
+ var MAX_VAULT_PAGE_LIMIT = 200;
23576
+ var ListVaultInventoryQuery = external_exports.object({
23577
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23578
+ // Opaque; names the last row of the page just served.
23579
+ cursor: external_exports.string().optional()
23580
+ });
23581
+ var ListVaultInventoryResponse = external_exports.object({
23582
+ // Vaulted values across the whole store, not just this page — cursor-
23583
+ // independent, so paging never changes what the count claims.
23584
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23585
+ items: external_exports.array(VaultInventoryEntry),
23586
+ // `null` once the last page is reached.
23587
+ nextCursor: external_exports.string().nullable()
23588
+ });
23589
+ var ListVaultReuseQuery = external_exports.object({
23590
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23591
+ cursor: external_exports.string().optional()
23592
+ });
23593
+ var ListVaultReuseResponse = external_exports.object({
23594
+ // Reused values across the whole store — the number the section's claim
23595
+ // ("values detected in more than one place") is about.
23596
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23597
+ items: external_exports.array(VaultInventoryEntry),
23598
+ nextCursor: external_exports.string().nullable()
23599
+ });
23600
+ var ListVaultDerefsQuery = external_exports.object({
23601
+ // Include the batched, high-volume reasons (display, view-render). Omitted
23602
+ // hides them and counts them into `hiddenBatched` instead, so the model
23603
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23604
+ // over a Server Action, which preserves the type, never as a URL param.
23605
+ includeBatched: external_exports.boolean().optional(),
23606
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23607
+ cursor: external_exports.string().optional()
23608
+ });
23609
+ var ListVaultDerefsResponse = external_exports.object({
23610
+ items: external_exports.array(VaultDeref),
23611
+ nextCursor: external_exports.string().nullable(),
23612
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
23613
+ // this page — it is the count the "N hidden" line and its toggle speak for.
23614
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
23615
+ hiddenBatched: external_exports.number().int().nonnegative()
23616
+ });
23617
+ var VaultKeyCustody = external_exports.string();
23618
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23619
+ var VAULT_CONSENT_VERSION = 1;
23620
+ var VaultConsent = external_exports.object({
23621
+ acknowledgedAt: external_exports.iso.datetime(),
23622
+ version: external_exports.number().int().positive()
23623
+ });
23624
+
23625
+ // ../../packages/schema/src/zod/local.ts
23626
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23627
+ var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23628
+ var RunMode = external_exports.enum(["standalone", "attached"]);
23629
+ var ControlPlaneConnection = external_exports.object({
23630
+ endpoint: external_exports.string().min(1),
23631
+ // Display name for the deployment, shown instead of the raw endpoint.
23632
+ label: external_exports.string().min(1).optional(),
23633
+ attachedAt: external_exports.iso.datetime()
23634
+ }).meta({ id: "ControlPlaneConnection" });
23635
+ var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23636
+ var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23637
+ var ModelJudgeConsent = external_exports.object({
23638
+ acknowledgedAt: external_exports.iso.datetime(),
23639
+ payloadVersion: external_exports.number().int().positive()
23640
+ });
23641
+ var HistorySyncConsent = external_exports.object({
23642
+ acknowledgedAt: external_exports.iso.datetime(),
23643
+ payloadVersion: external_exports.number().int().positive(),
23644
+ endpoint: external_exports.string()
23645
+ });
23646
+ var WorkspaceSettings = external_exports.object({
23647
+ specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23648
+ runMode: RunMode.default("standalone"),
23649
+ // Present only while attached; a detach clears it. Its presence is what makes
23650
+ // `runMode: 'attached'` mean anything — see isAttached.
23651
+ controlPlane: ControlPlaneConnection.optional(),
23652
+ policy: SimpleDetectionPolicy.default("redact"),
23653
+ // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23654
+ historicalAccess: HistoricalAccess.default("session-only"),
23655
+ // In-place egress extraction on the scan paths; disable to stop all Data
23656
+ // Shares writes.
23657
+ dataSharesInPlace: external_exports.boolean().default(true),
23658
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23659
+ // vault, instead of destroying them. Absent by default: this is a custody
23660
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
23661
+ // Revoking stops future vaulting; it does not erase what is already stored —
23662
+ // purging the vault is the eraser.
23663
+ vaultConsent: VaultConsent.optional(),
23664
+ // Where the vault master key lives.
23665
+ vaultKeyCustody: VaultKeyCustody.default("file"),
23666
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23667
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
23668
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23669
+ // place. Not a handling policy: the policy has already resolved to redact,
23670
+ // and this only says what happens when the host offers no channel to carry it
23671
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23672
+ // Claude Code decline to mask a field that EXECUTES because masking would
23673
+ // change what runs. Per FIELD rather than per host, so a host that can
23674
+ // rewrite some inputs keeps true redaction on those.
23675
+ //
23676
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23677
+ // an attached machine's merge is `strongerAction` over the one action ladder
23678
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23679
+ // word and stays out of the stored value.
23680
+ redactFallback: RedactFallback.default("warn"),
23681
+ // Absent until /aka:setup completes; its presence is what "onboarded" means.
23682
+ onboardedAt: external_exports.iso.datetime().optional(),
23683
+ // Records that the user consented to sending findings to the model API for
23684
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
23685
+ // Absent until granted; a stale payloadVersion means the consent no longer
23686
+ // covers the current payload and must be re-granted.
23687
+ modelJudgeConsent: ModelJudgeConsent.optional(),
23688
+ // Records that the user consented to the DEFERRED send — the outbox — along
23689
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23690
+ // that covers both the pre-attach backlog and undelivered captures (which
23691
+ // carry prompt/reply text in `content`); the key name predates the widening.
23692
+ // Absent until granted, and a grant for a different endpoint or an older
23693
+ // payload no longer counts.
23694
+ historySyncConsent: HistorySyncConsent.optional()
23695
+ });
23696
+ function defaultWorkspaceSettings() {
23697
+ return WorkspaceSettings.parse({});
23698
+ }
23699
+ function isAttached(settings) {
23700
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23701
+ }
23702
+ function toInventoryRow(input2, id, now) {
23703
+ return {
23704
+ id,
23705
+ objectType: input2.objectType,
23706
+ location: input2.location ?? null,
23707
+ title: input2.title ?? null,
23708
+ hostId: input2.hostId ?? null,
23709
+ attributes: JSON.stringify(input2.attributes),
23710
+ firstSeen: now,
23711
+ lastSeen: now
23712
+ };
23713
+ }
23714
+ function toSourceProjectRow(input2, id, now) {
23715
+ return {
23716
+ id,
23717
+ url: input2.url,
23718
+ name: input2.name ?? null,
23719
+ attributes: JSON.stringify(input2.attributes),
23720
+ firstSeen: now,
23721
+ lastSeen: now
23722
+ };
23723
+ }
23724
+ function toAuditEventRow(input2) {
23725
+ return {
23726
+ id: input2.id,
23727
+ parentId: input2.parentId ?? null,
23728
+ rootSessionId: input2.rootSessionId ?? null,
23729
+ eventType: input2.eventType,
23730
+ hostId: input2.hostId ?? null,
23731
+ harnessId: input2.harnessId ?? null,
23732
+ sourceProjectId: input2.sourceProjectId ?? null,
23733
+ startedAt: isoToEpochMillis(input2.startedAt),
23734
+ endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23735
+ severity: input2.severity ?? null,
23736
+ priority: input2.priority ?? null,
23737
+ content: input2.content ?? null,
23738
+ contentHash: input2.contentHash ?? null,
23739
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23740
+ };
23741
+ }
23742
+ function toClassifiedDataRow(input2, id) {
23743
+ return {
23744
+ id,
23745
+ class: input2.class,
23746
+ label: input2.label ?? null,
23747
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23748
+ };
23749
+ }
23750
+ function toInspectionDefinitionRow(input2, id) {
23751
+ return {
23752
+ id,
23753
+ ruleId: input2.ruleId,
23754
+ name: input2.name,
23755
+ category: input2.category,
23756
+ severity: input2.severity,
23757
+ definition: input2.definition,
23758
+ version: input2.version
23759
+ };
23760
+ }
23761
+ function toInspectionFindingRow(input2) {
23762
+ return {
23763
+ id: input2.id,
23764
+ auditEventId: input2.auditEventId,
23765
+ inspectionDefinitionId: input2.inspectionDefinitionId,
23766
+ classifiedDataId: input2.classifiedDataId ?? null,
23767
+ spanStart: input2.span.start,
23768
+ spanEnd: input2.span.end,
23769
+ maskedMatch: input2.maskedMatch,
23770
+ actionTaken: input2.actionTaken,
23771
+ confidence: input2.confidence,
23772
+ findingKey: input2.findingKey ?? null,
23773
+ firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23774
+ };
23775
+ }
23776
+ function toCaptureAttributes(event) {
23777
+ const metadata = event.metadata;
23778
+ return {
23779
+ source_tool: event.sourceTool,
23780
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23781
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23782
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23783
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23784
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23785
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23786
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23787
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23788
+ ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23789
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23790
+ // has ever populated either), but every legacy metadata key still rides
23791
+ // the bag rather than being silently dropped — CaptureAttributes'
23792
+ // `.catchall(z.unknown())` carries the long tail.
23793
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23794
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23795
+ };
23796
+ }
23797
+ function captureDefinitionVersion(finding) {
23798
+ return `capture/${finding.category}/${finding.severity}`;
23799
+ }
23800
+ function toCaptureDefinitionInput(finding) {
23801
+ return {
23802
+ ruleId: finding.ruleId,
23803
+ version: captureDefinitionVersion(finding),
23804
+ name: finding.ruleId,
23805
+ category: finding.category,
23806
+ severity: finding.severity,
23807
+ definition: JSON.stringify({ ruleId: finding.ruleId })
23808
+ };
23809
+ }
23810
+
23811
+ // ../../packages/schema/src/zod/managed.ts
23812
+ var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23813
+ var MANAGED_SETTINGS_SPEC_VERSION = 1;
23814
+ var ManagedSettingKey = external_exports.enum([
23815
+ "runMode",
23816
+ "historicalAccess",
23817
+ "vaultConsent",
23818
+ "vaultKeyCustody",
23819
+ "vaultInlineReveal",
23820
+ "modelJudgeConsent",
23821
+ "dataSharesInPlace",
23822
+ "redactFallback"
23823
+ ]).meta({ id: "ManagedSettingKey" });
23824
+ var ManagedSettingsValues = external_exports.object({
23825
+ runMode: external_exports.enum(["standalone", "attached"]).optional(),
23826
+ controlPlane: external_exports.object({
23827
+ endpoint: external_exports.string().min(1),
23828
+ label: external_exports.string().min(1).optional()
23829
+ }).optional(),
23830
+ historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23831
+ vaultConsent: external_exports.boolean().optional(),
23832
+ vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23833
+ vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23834
+ modelJudgeConsent: external_exports.boolean().optional(),
23835
+ dataSharesInPlace: external_exports.boolean().optional(),
23836
+ redactFallback: RedactFallback.optional()
23837
+ }).meta({ id: "ManagedSettingsValues" });
23838
+ var ManagedSettings = external_exports.object({
23839
+ specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23840
+ // Shown on every locked control, so the user can tell an administrative
23841
+ // decision from a bug. Absent renders as a generic "your organization".
23842
+ organization: external_exports.string().min(1).optional(),
23843
+ // What the administrator pinned.
23844
+ values: ManagedSettingsValues.default({}),
23845
+ // Which of those the user may not change. A key here with no matching value
23846
+ // freezes whatever the user last chose; a value with no lock is a DEFAULT
23847
+ // the user may still override. The two are separable on purpose.
23848
+ lockedFields: external_exports.array(ManagedSettingKey).default([])
23849
+ }).meta({ id: "ManagedSettings" });
23850
+
23707
23851
  // ../../packages/schema/src/zod/project-files.ts
23708
23852
  var ProjectFileInput = external_exports.object({
23709
23853
  path: external_exports.string().min(1),
@@ -23949,10 +24093,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23949
24093
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23950
24094
 
23951
24095
  // ../../packages/schema/src/zod/settings-action.ts
24096
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24097
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23952
24098
  var SaveSettingsInput = external_exports.object({
23953
24099
  historicalAccess: external_exports.string(),
23954
- modelJudgeConsent: external_exports.boolean(),
23955
- historySyncConsent: external_exports.boolean(),
24100
+ modelJudgeConsent: ModelJudgeConsentChoice,
24101
+ historySyncConsent: HistorySyncConsentChoice,
23956
24102
  vaultConsent: external_exports.string(),
23957
24103
  vaultInlineReveal: external_exports.string()
23958
24104
  });
@@ -24102,9 +24248,9 @@ function deriveReviewReasons(trust, transports) {
24102
24248
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24103
24249
  return reasons;
24104
24250
  }
24105
- function buildReviewInfo(trust, transports) {
24251
+ function buildReviewInfo(trust, transports, decided) {
24106
24252
  const reasons = deriveReviewReasons(trust, transports);
24107
- return { needsReview: reasons.length > 0, reasons };
24253
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24108
24254
  }
24109
24255
  function distinctTransports(transports) {
24110
24256
  return Array.from(new Set(transports));
@@ -24302,8 +24448,8 @@ function readControlPlaneCredential(settingsDir2, connection) {
24302
24448
  }
24303
24449
 
24304
24450
  // ../../packages/persistence/src/database.ts
24305
- import { randomUUID as randomUUID10 } from "crypto";
24306
- import { join as join4, sep } from "path";
24451
+ import { randomUUID as randomUUID11 } from "crypto";
24452
+ import { dirname as dirname2, join as join7, sep } from "path";
24307
24453
  import { DatabaseSync } from "node:sqlite";
24308
24454
 
24309
24455
  // ../../packages/persistence/src/ids.ts
@@ -24558,6 +24704,10 @@ function allRows(stmt, params) {
24558
24704
  if (Array.isArray(params)) return stmt.all(...params);
24559
24705
  return stmt.all(params);
24560
24706
  }
24707
+ function* iterateRows(stmt, params) {
24708
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24709
+ for (const row of rows) yield row;
24710
+ }
24561
24711
  function getRow(stmt, params) {
24562
24712
  if (params === void 0) return stmt.get();
24563
24713
  if (Array.isArray(params)) return stmt.get(...params);
@@ -25026,10 +25176,17 @@ function ensureSyncedAtColumn(db, table2) {
25026
25176
  if (!columns.includes("sync_claimed_at")) {
25027
25177
  db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
25028
25178
  }
25179
+ if (!columns.includes("outbox_owed")) {
25180
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25181
+ }
25029
25182
  db.exec(
25030
25183
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25031
25184
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
25032
25185
  );
25186
+ db.exec(
25187
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25188
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25189
+ );
25033
25190
  db.exec(
25034
25191
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
25035
25192
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25134,7 +25291,6 @@ function decodeKeysetCursor(cursor) {
25134
25291
  // ../../packages/persistence/src/repositories/activity.ts
25135
25292
  var DAY_MS = 864e5;
25136
25293
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25137
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25138
25294
  function defaultTimeZone() {
25139
25295
  try {
25140
25296
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25189,6 +25345,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25189
25345
  error: "error",
25190
25346
  active: "active"
25191
25347
  };
25348
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25192
25349
  function safeParseStringArray(raw) {
25193
25350
  if (!raw) return [];
25194
25351
  const parsed2 = safeJson(raw, null);
@@ -25262,6 +25419,37 @@ var TIMELINE_COLUMNS = `
25262
25419
  json_extract(attributes, '$.targetId') AS target_id,
25263
25420
  json_extract(attributes, '$.internal') AS internal,
25264
25421
  json_extract(attributes, '$.flagged') AS flagged`;
25422
+ var LLM_USAGE_SELECT = `
25423
+ SELECT root_session_id AS sessionId,
25424
+ provider,
25425
+ model,
25426
+ service_tier AS serviceTier,
25427
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25428
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25429
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25430
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25431
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25432
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25433
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25434
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25435
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25436
+ function usageLeaves(rows) {
25437
+ return rows.map((row) => {
25438
+ const attributes = {
25439
+ input_tokens: row.inputTokens,
25440
+ output_tokens: row.outputTokens,
25441
+ cache_creation_input_tokens: row.cacheCreationTokens,
25442
+ cache_read_input_tokens: row.cacheReadTokens,
25443
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25444
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25445
+ web_search_requests: row.webSearchRequests
25446
+ };
25447
+ if (row.provider !== null) attributes.provider = row.provider;
25448
+ if (row.model !== null) attributes.model = row.model;
25449
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25450
+ return { sessionId: row.sessionId, attributes };
25451
+ });
25452
+ }
25265
25453
  var SESSION_ROOT = `event_type = 'session'`;
25266
25454
  var HAS_ACTIVITY = `EXISTS (
25267
25455
  SELECT 1 FROM audit_events c
@@ -25287,16 +25475,17 @@ var SqliteActivityRepository = class {
25287
25475
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25288
25476
  const liveNow = countScalar(
25289
25477
  this.db,
25290
- `SELECT count(*) AS n FROM audit_events s
25478
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25291
25479
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25292
- AND max(
25293
- s.started_at,
25294
- coalesce(
25295
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25296
- s.started_at
25297
- )
25298
- ) >= ?`,
25299
- [liveThreshold]
25480
+ AND s.id IN (
25481
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25482
+ UNION
25483
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25484
+ WHERE started_at >= ?
25485
+ UNION
25486
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25487
+ WHERE ended_at >= ?)`,
25488
+ [liveThreshold, liveThreshold, liveThreshold]
25300
25489
  );
25301
25490
  const toolCallsToday = countScalar(
25302
25491
  this.db,
@@ -25426,7 +25615,7 @@ var SqliteActivityRepository = class {
25426
25615
  this.db.prepare(
25427
25616
  `SELECT ${TIMELINE_COLUMNS}
25428
25617
  FROM audit_events
25429
- WHERE id = ? OR root_session_id = ?
25618
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25430
25619
  ORDER BY started_at ASC, id ASC`
25431
25620
  ),
25432
25621
  [sessionId, sessionId]
@@ -25439,14 +25628,14 @@ var SqliteActivityRepository = class {
25439
25628
  coalesce(sum(output_tokens), 0) AS output,
25440
25629
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25441
25630
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25442
- FROM audit_events
25631
+ FROM audit_events INDEXED BY idx_audit_session_type
25443
25632
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25444
25633
  ),
25445
25634
  [sessionId]
25446
25635
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25447
25636
  const primaryModel = getRow(
25448
25637
  this.db.prepare(
25449
- `SELECT model, provider FROM audit_events
25638
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25450
25639
  WHERE root_session_id = ? AND event_type = 'llm_call'
25451
25640
  ORDER BY started_at ASC, id ASC
25452
25641
  LIMIT 1`
@@ -25457,7 +25646,7 @@ var SqliteActivityRepository = class {
25457
25646
  this.db.prepare(
25458
25647
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25459
25648
  count(*) AS n
25460
- FROM audit_events
25649
+ FROM audit_events INDEXED BY idx_audit_session
25461
25650
  WHERE root_session_id = ? AND event_type = 'tool_call'
25462
25651
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25463
25652
  ),
@@ -25465,7 +25654,7 @@ var SqliteActivityRepository = class {
25465
25654
  );
25466
25655
  const modelRows = allRows(
25467
25656
  this.db.prepare(
25468
- `SELECT DISTINCT model FROM audit_events
25657
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25469
25658
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25470
25659
  ORDER BY model`
25471
25660
  ),
@@ -25474,7 +25663,7 @@ var SqliteActivityRepository = class {
25474
25663
  const derivedModels = modelRows.map((r) => r.model);
25475
25664
  const commits = countScalar(
25476
25665
  this.db,
25477
- `SELECT count(*) AS n FROM audit_events
25666
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25478
25667
  WHERE root_session_id = ? AND event_type = 'commit'`,
25479
25668
  [sessionId]
25480
25669
  );
@@ -25510,25 +25699,57 @@ var SqliteActivityRepository = class {
25510
25699
  return Promise.resolve(session);
25511
25700
  }
25512
25701
  /**
25513
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25514
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25515
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25516
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25517
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25518
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25702
+ * Cross-session token report — every `llm_call` in the store (or in a
25703
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25704
+ * session, with USD cost DERIVED at read time via the shared
25705
+ * `defaultCostModel` (never stored). The caller collapses these onto
25706
+ * per-model rows with `aggregateTokenUsage`.
25707
+ *
25708
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25709
+ * the members the rollup sums — and priced once per group, which is exact
25710
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25711
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25712
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25713
+ * the index stores the values once, at write, and answers the same window in
25714
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25715
+ * planner prefers the general event-type index and fetches every row to
25716
+ * recompute the columns it could have read. The index is one every open
25717
+ * store carries, since opening runs the migrations, so the hard requirement
25718
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25719
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25720
+ * per call, no bag parsed.
25519
25721
  */
25520
25722
  tokenReports(fromMs) {
25521
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25522
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25723
+ const rows = allRows(
25724
+ this.db.prepare(
25725
+ `${LLM_USAGE_SELECT}
25726
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25727
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25728
+ ${LLM_USAGE_GROUP}`
25729
+ ),
25730
+ fromMs === void 0 ? void 0 : [fromMs]
25731
+ );
25732
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25523
25733
  }
25524
25734
  /**
25525
- * One session's token report — its `llm_call` leaves grouped per (provider,
25526
- * model) with derived cost, or `null` when the session made no `llm_call`s
25527
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25528
- * breakdown + estimated cost.
25735
+ * One session's token report — its `llm_call`s grouped per (provider,
25736
+ * model, tier) with derived cost, or `null` when the session made no
25737
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25738
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25739
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25740
+ * it replaces walked every `llm_call` in the store to find one session's.
25529
25741
  */
25530
25742
  tokenReportForSession(sessionId) {
25531
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25743
+ const rows = allRows(
25744
+ this.db.prepare(
25745
+ `${LLM_USAGE_SELECT}
25746
+ FROM audit_events
25747
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25748
+ ${LLM_USAGE_GROUP}`
25749
+ ),
25750
+ [sessionId]
25751
+ );
25752
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25532
25753
  return Promise.resolve(reports[0] ?? null);
25533
25754
  }
25534
25755
  /**
@@ -25552,42 +25773,6 @@ var SqliteActivityRepository = class {
25552
25773
  for (const row of rows) seen.add(toHarness(row.harness));
25553
25774
  return Promise.resolve([...seen]);
25554
25775
  }
25555
- /**
25556
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25557
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25558
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25559
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25560
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25561
- */
25562
- readLlmCallLeaves(opts = {}) {
25563
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25564
- const params = [];
25565
- if (opts.sessionId !== void 0) {
25566
- conditions.push("root_session_id = ?");
25567
- params.push(opts.sessionId);
25568
- }
25569
- if (opts.fromMs !== void 0) {
25570
- conditions.push("started_at >= ?");
25571
- params.push(opts.fromMs);
25572
- }
25573
- const rows = allRows(
25574
- this.db.prepare(
25575
- `SELECT root_session_id AS sessionId, attributes
25576
- FROM audit_events
25577
- WHERE ${conditions.join(" AND ")}`
25578
- ),
25579
- params
25580
- );
25581
- return mapRowsTolerant(
25582
- rows.filter(
25583
- (row) => row.sessionId !== null
25584
- ),
25585
- (row) => ({
25586
- sessionId: row.sessionId,
25587
- attributes: JSON.parse(row.attributes)
25588
- })
25589
- );
25590
- }
25591
25776
  /**
25592
25777
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25593
25778
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25602,20 +25787,23 @@ var SqliteActivityRepository = class {
25602
25787
  const inClause = placeholders(sessionIds.length);
25603
25788
  const lastActivityRows = allRows(
25604
25789
  this.db.prepare(
25605
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25606
- WHERE root_session_id IN (${inClause})
25607
- GROUP BY root_session_id`
25790
+ `SELECT ids.value AS id,
25791
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25792
+ (SELECT max(ended_at) FROM audit_events e
25793
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25794
+ FROM json_each(?) AS ids`
25608
25795
  ),
25609
- sessionIds
25796
+ [JSON.stringify(sessionIds)]
25610
25797
  );
25611
25798
  for (const row of lastActivityRows) {
25612
- if (row.id === null) continue;
25613
25799
  const entry = result.get(row.id);
25614
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25800
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25801
+ if (entry && last > 0) entry.lastActivityMs = last;
25615
25802
  }
25616
25803
  const turnsRows = allRows(
25617
25804
  this.db.prepare(
25618
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25805
+ `SELECT root_session_id AS id, count(*) AS n
25806
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25619
25807
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25620
25808
  GROUP BY root_session_id`
25621
25809
  ),
@@ -25630,7 +25818,7 @@ var SqliteActivityRepository = class {
25630
25818
  this.db.prepare(
25631
25819
  `SELECT root_session_id AS id,
25632
25820
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25633
- FROM audit_events
25821
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25634
25822
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25635
25823
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25636
25824
  GROUP BY root_session_id`
@@ -25660,7 +25848,7 @@ var SqliteActivityRepository = class {
25660
25848
  this.db.prepare(
25661
25849
  `SELECT root_session_id AS id,
25662
25850
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25663
- FROM audit_events
25851
+ FROM audit_events INDEXED BY idx_audit_session_share
25664
25852
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25665
25853
  GROUP BY root_session_id`
25666
25854
  ),
@@ -26689,7 +26877,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26689
26877
 
26690
26878
  // ../../packages/persistence/src/repositories/findings.ts
26691
26879
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26692
- var SCAN_BATCH_ROWS = 1e3;
26693
26880
  var DEFAULT_LOCATIONS_LIMIT = 100;
26694
26881
  var LOCATION_RULE_IDS_CAP = 20;
26695
26882
  function compareLocationOrder(a, b) {
@@ -26718,6 +26905,25 @@ function deriveInstanceStatus(row) {
26718
26905
  latestResolutionStatus: row.latest_status
26719
26906
  });
26720
26907
  }
26908
+ function toFlatFindingRow(r) {
26909
+ return {
26910
+ id: r.id,
26911
+ ruleId: r.rule_id,
26912
+ category: r.category,
26913
+ severity: r.severity,
26914
+ maskedMatch: r.masked_match,
26915
+ actionTaken: r.action_taken,
26916
+ confidence: r.confidence,
26917
+ occurredAt: epochMillisToIso(r.occurred_at),
26918
+ sourceTool: r.source_tool,
26919
+ repo: r.repo ?? "",
26920
+ file: r.file ?? "",
26921
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26922
+ eventId: r.event_id,
26923
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26924
+ status: deriveInstanceStatus(r)
26925
+ };
26926
+ }
26721
26927
  function encodeGroupCursor(group) {
26722
26928
  const payload = {
26723
26929
  sev: group.severity,
@@ -26793,7 +26999,7 @@ var SqliteFindingsRepository = class {
26793
26999
  this.db.prepare(
26794
27000
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26795
27001
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26796
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27002
+ e.source_tool AS source_tool,
26797
27003
  e.event_type AS kind
26798
27004
  FROM audit_events e
26799
27005
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26901,56 +27107,11 @@ var SqliteFindingsRepository = class {
26901
27107
  predicate,
26902
27108
  params: sessionParams
26903
27109
  });
26904
- const rows = allRows(
26905
- this.db.prepare(
26906
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26907
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26908
- kind, finding_key, latest_status
26909
- FROM (
26910
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26911
- d.severity AS severity, f.masked_match AS masked_match,
26912
- f.action_taken AS action_taken, f.confidence AS confidence,
26913
- e.started_at AS occurred_at,
26914
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26915
- json_extract(e.attributes, '$.repo') AS repo,
26916
- json_extract(e.attributes, '$.file_path') AS file,
26917
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26918
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26919
- e.event_type AS kind, f.finding_key AS finding_key,
26920
- latest.status AS latest_status,
26921
- ROW_NUMBER() OVER (
26922
- PARTITION BY d.rule_id
26923
- ORDER BY e.started_at DESC, f.id DESC
26924
- ) AS rn
26925
- FROM inspection_findings f
26926
- JOIN audit_events e ON e.id = f.audit_event_id
26927
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26928
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26929
- ON latest.finding_key = f.finding_key
26930
- ${predicate}
26931
- )
26932
- WHERE rn <= :cap
26933
- ORDER BY occurred_at DESC, id DESC`
26934
- ),
26935
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26936
- );
26937
- const groupable = rows.map((r) => ({
26938
- id: r.id,
26939
- ruleId: r.rule_id,
26940
- category: r.category,
26941
- severity: r.severity,
26942
- maskedMatch: r.masked_match,
26943
- actionTaken: r.action_taken,
26944
- confidence: r.confidence,
26945
- occurredAt: epochMillisToIso(r.occurred_at),
26946
- sourceTool: r.source_tool,
26947
- repo: r.repo ?? "",
26948
- file: r.file ?? "",
26949
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26950
- eventId: r.event_id,
26951
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26952
- status: deriveInstanceStatus(r)
26953
- }));
27110
+ const rows = this.previewRows(aggregates, {
27111
+ sessionId: query.sessionId,
27112
+ from: query.from
27113
+ });
27114
+ const groupable = rows.map(toFlatFindingRow);
26954
27115
  const allGroups = buildFindingGroups(groupable, { aggregates });
26955
27116
  const filterOpts = {
26956
27117
  severity: query.severity,
@@ -27036,8 +27197,10 @@ var SqliteFindingsRepository = class {
27036
27197
  *
27037
27198
  * The scan runs from the top of the scope on every request, not from the
27038
27199
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
27039
- * move as the caller pages. Rows are pulled in batches so memory stays flat
27040
- * while the counting runs, and only the page itself is retained.
27200
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27201
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27202
+ * counting runs — a generator streaming the index order, not a sequence of
27203
+ * fetched batches; only the page itself is retained.
27041
27204
  */
27042
27205
  listFindingInstances(query) {
27043
27206
  const opts = {
@@ -27053,6 +27216,10 @@ var SqliteFindingsRepository = class {
27053
27216
  };
27054
27217
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27055
27218
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27219
+ const isPastCursor = cursor === null ? () => true : (row) => {
27220
+ const rowMs = isoToEpochMillis(row.occurredAt);
27221
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27222
+ };
27056
27223
  const accumulator = createInstanceFacetAccumulator(opts);
27057
27224
  const items = [];
27058
27225
  let total = 0;
@@ -27065,6 +27232,7 @@ var SqliteFindingsRepository = class {
27065
27232
  accumulator.add(row);
27066
27233
  if (!matchesInstanceFilters(row, opts)) continue;
27067
27234
  total += 1;
27235
+ if (!isPastCursor(row)) continue;
27068
27236
  if (items.length < limit) {
27069
27237
  items.push(toInstanceDetail(row));
27070
27238
  last = row;
@@ -27073,15 +27241,6 @@ var SqliteFindingsRepository = class {
27073
27241
  }
27074
27242
  }
27075
27243
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27076
- if (cursor !== null) {
27077
- const resumed = this.pageAfter(cursor, opts, limit, query);
27078
- return Promise.resolve({
27079
- totals: { findings: total },
27080
- facets: accumulator.facets(),
27081
- items: resumed.items,
27082
- nextCursor: resumed.nextCursor
27083
- });
27084
- }
27085
27244
  return Promise.resolve({
27086
27245
  totals: { findings: total },
27087
27246
  facets: accumulator.facets(),
@@ -27089,35 +27248,6 @@ var SqliteFindingsRepository = class {
27089
27248
  nextCursor
27090
27249
  });
27091
27250
  }
27092
- /**
27093
- * The page of matching rows strictly after `cursor`. Separate from the
27094
- * counting pass because that one starts at the top of the scope by design;
27095
- * this one narrows the scan with the same keyset predicate the activity list
27096
- * uses, so a later page costs less than the first rather than more.
27097
- */
27098
- pageAfter(cursor, opts, limit, query) {
27099
- const items = [];
27100
- let last;
27101
- let hasMore = false;
27102
- for (const row of this.scanFindingRows({
27103
- sessionId: query.sessionId,
27104
- from: query.from,
27105
- after: cursor
27106
- })) {
27107
- if (!matchesInstanceFilters(row, opts)) continue;
27108
- if (items.length < limit) {
27109
- items.push(toInstanceDetail(row));
27110
- last = row;
27111
- } else {
27112
- hasMore = true;
27113
- break;
27114
- }
27115
- }
27116
- return {
27117
- items,
27118
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27119
- };
27120
- }
27121
27251
  /**
27122
27252
  * The same findings folded by location: repository, then file within it.
27123
27253
  *
@@ -27200,25 +27330,111 @@ var SqliteFindingsRepository = class {
27200
27330
  });
27201
27331
  }
27202
27332
  /**
27203
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27333
+ * Each group's newest instances, for the table's expanded rows.
27334
+ *
27335
+ * ONE index-ordered scan with early termination, and the shape is the point.
27336
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27337
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27338
+ * through a temp B-tree to keep a bounded preview of each group, and then
27339
+ * sorts the survivors again for the page order. Both sorts grow with the
27340
+ * store while the answer does not.
27341
+ *
27342
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27343
+ * (or the session or window index the scope names — see `findingScanSql`),
27344
+ * which is already the order the page wants, and keeps rows per rule until
27345
+ * each rule has as many as it can show. The aggregate the caller already holds
27346
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27347
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27348
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27349
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27350
+ * store with many firing rules widens it. The bound that DOES hold
27351
+ * unconditionally is the sorted form's floor: this scan visits at most as
27352
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27353
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27354
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27355
+ * wanted instances sitting at the tail of the scope — is one pass over
27356
+ * everything in scope with a block sort of the id tie-break only, never a
27357
+ * sort of the scope, which is still that floor.
27358
+ *
27359
+ * A row whose rule the aggregate did not see is skipped: the two statements
27360
+ * run without a shared snapshot, so a capture landing between them can add a
27361
+ * rule here that has no counts there, and the counts are what the group is
27362
+ * built from.
27363
+ */
27364
+ previewRows(aggregates, scope) {
27365
+ const wanted = /* @__PURE__ */ new Map();
27366
+ let remaining = 0;
27367
+ for (const [ruleId, agg] of aggregates) {
27368
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27369
+ wanted.set(ruleId, n);
27370
+ remaining += n;
27371
+ }
27372
+ const rows = [];
27373
+ if (remaining === 0) return rows;
27374
+ const { sql, params } = this.findingScanSql(scope);
27375
+ const taken = /* @__PURE__ */ new Map();
27376
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27377
+ const want = wanted.get(r.rule_id);
27378
+ if (want === void 0) continue;
27379
+ const have = taken.get(r.rule_id) ?? 0;
27380
+ if (have >= want) continue;
27381
+ taken.set(r.rule_id, have + 1);
27382
+ rows.push(r);
27383
+ remaining -= 1;
27384
+ if (remaining === 0) break;
27385
+ }
27386
+ return rows;
27387
+ }
27388
+ /**
27389
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27204
27390
  *
27205
27391
  * A generator so a caller streams the scope without it ever being an array:
27206
27392
  * the flat list counts and facets the whole filtered scope, which on a large
27207
- * store is far more rows than any page. Each batch advances the same keyset
27208
- * predicate the page read uses, so the scan is a sequence of bounded reads
27209
- * rather than one unbounded result set.
27210
- *
27211
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27212
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27213
- * makes it a point lookup per row, and the derived table would re-materialize
27214
- * a window over the whole resolution table once per batch.
27393
+ * store is far more rows than any page. The rows come off ONE statement,
27394
+ * iterated rather than materialized, in the index order `findingScanSql`
27395
+ * arranges so the scan is a single pass with a block sort of the id
27396
+ * tie-break only, never a sort of the scope, where a sequence of
27397
+ * keyset-bounded batches re-sorted everything below the cursor on every
27398
+ * batch and cost the square of the scope.
27215
27399
  *
27216
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27217
- * would be missing from its own facet, which is computed by excluding that
27218
- * dimension see listFindingInstances.
27400
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27401
+ * dimension narrowed here would be missing from its own facet, which is
27402
+ * computed by excluding that dimension (see listFindingInstances). There is
27403
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27404
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27405
+ * narrower statement, since the counting pass already visits every row a
27406
+ * page-2+ request would otherwise re-seek for.
27219
27407
  */
27220
27408
  *scanFindingRows(scope) {
27221
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27409
+ const { sql, params } = this.findingScanSql(scope);
27410
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27411
+ yield toFlatFindingRow(r);
27412
+ }
27413
+ }
27414
+ /**
27415
+ * The one statement both instance-level scans run: every finding in scope,
27416
+ * joined to its event and definition, newest first.
27417
+ *
27418
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27419
+ * the same two `recentFindings` documents at length, for the same reason:
27420
+ *
27421
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27422
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27423
+ * yields `started_at` order per event type, not across the four, so
27424
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27425
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27426
+ * `idx_audit_session` for a session scope, which is also `started_at`
27427
+ * ordered within the session — and the order falls out of the index.
27428
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27429
+ * JOINs the planner drives from the findings and sorts everything.
27430
+ *
27431
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27432
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27433
+ * index probe per keyed row, and a derived table over the whole resolution
27434
+ * table would be materialized before the first row streamed.
27435
+ */
27436
+ findingScanSql(scope) {
27437
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27222
27438
  const params = [];
27223
27439
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27224
27440
  conditions.push("e.root_session_id = ?");
@@ -27232,58 +27448,24 @@ var SqliteFindingsRepository = class {
27232
27448
  d.severity AS severity, f.masked_match AS masked_match,
27233
27449
  f.action_taken AS action_taken, f.confidence AS confidence,
27234
27450
  e.started_at AS occurred_at,
27235
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27236
- json_extract(e.attributes, '$.repo') AS repo,
27237
- json_extract(e.attributes, '$.file_path') AS file,
27238
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27451
+ e.source_tool AS source_tool,
27452
+ e.repo AS repo,
27453
+ e.file_path AS file,
27454
+ e.tool_name AS tool_name,
27239
27455
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27240
27456
  e.event_type AS kind, f.finding_key AS finding_key,
27241
27457
  ${latestResolutionStatusSql("f")} AS latest_status
27242
- FROM inspection_findings f
27243
- JOIN audit_events e ON e.id = f.audit_event_id
27244
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27458
+ FROM audit_events e
27459
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27460
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27245
27461
  WHERE ${conditions.join(" AND ")}
27246
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27247
- ORDER BY e.started_at DESC, f.id DESC
27248
- LIMIT ?`;
27249
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27250
- for (; ; ) {
27251
- const rows = allRows(this.db.prepare(sql), [
27252
- ...params,
27253
- after.startedAtMs,
27254
- after.startedAtMs,
27255
- after.id,
27256
- SCAN_BATCH_ROWS
27257
- ]);
27258
- for (const r of rows) {
27259
- yield {
27260
- id: r.id,
27261
- ruleId: r.rule_id,
27262
- category: r.category,
27263
- severity: r.severity,
27264
- maskedMatch: r.masked_match,
27265
- actionTaken: r.action_taken,
27266
- confidence: r.confidence,
27267
- occurredAt: epochMillisToIso(r.occurred_at),
27268
- sourceTool: r.source_tool,
27269
- repo: r.repo ?? "",
27270
- file: r.file ?? "",
27271
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27272
- eventId: r.event_id,
27273
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27274
- status: deriveInstanceStatus(r)
27275
- };
27276
- }
27277
- if (rows.length < SCAN_BATCH_ROWS) return;
27278
- const lastRow = rows[rows.length - 1];
27279
- if (lastRow === void 0) return;
27280
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27281
- }
27462
+ ORDER BY e.started_at DESC, f.id DESC`;
27463
+ return { sql, params };
27282
27464
  }
27283
27465
  groupAggregates(withSearchText, scope) {
27284
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27285
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27286
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27466
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27467
+ group_concat(DISTINCT e.file_path) AS files,
27468
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27287
27469
  const rows = this.db.prepare(
27288
27470
  `SELECT rule_id,
27289
27471
  sum(tuple_count) AS instance_count,
@@ -27301,7 +27483,7 @@ var SqliteFindingsRepository = class {
27301
27483
  coalesce(latest.status, '') AS status_tuple,
27302
27484
  count(*) AS tuple_count,
27303
27485
  max(e.started_at) AS latest_at,
27304
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27486
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27305
27487
  group_concat(DISTINCT f.action_taken) AS actions_taken
27306
27488
  ${innerSearchColumns}
27307
27489
  FROM inspection_findings f
@@ -27432,6 +27614,8 @@ function isoDay(ms) {
27432
27614
  // ../../packages/persistence/src/repositories/history-sync.ts
27433
27615
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27434
27616
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27617
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27618
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27435
27619
  var SKIPPED = -1;
27436
27620
  var ROW_COLUMNS = `id,
27437
27621
  parent_id AS parentId,
@@ -27471,6 +27655,20 @@ var SqliteHistorySyncRepository = class {
27471
27655
  ORDER BY (event_type = 'session') DESC, started_at
27472
27656
  LIMIT :limit`
27473
27657
  );
27658
+ this.captureRowsStmt = db.prepare(
27659
+ `SELECT ${ROW_COLUMNS}
27660
+ FROM audit_events
27661
+ WHERE synced_at IS NULL
27662
+ AND sync_claimed_at IS NULL
27663
+ AND outbox_owed = 1
27664
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27665
+ AND started_at < :before
27666
+ ORDER BY started_at
27667
+ LIMIT :limit`
27668
+ );
27669
+ this.markOwedStmt = db.prepare(
27670
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27671
+ );
27474
27672
  this.stampStmt = db.prepare(
27475
27673
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27476
27674
  );
@@ -27502,6 +27700,12 @@ var SqliteHistorySyncRepository = class {
27502
27700
  FROM audit_events
27503
27701
  WHERE event_type IN (${TYPE_LIST})`
27504
27702
  );
27703
+ this.captureSkipCountStmt = db.prepare(
27704
+ `SELECT COUNT(*) AS skipped
27705
+ FROM audit_events
27706
+ WHERE synced_at = ${String(SKIPPED)}
27707
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27708
+ );
27505
27709
  this.fingerprintStmt = db.prepare(
27506
27710
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27507
27711
  FROM history_sync WHERE id = 1`
@@ -27511,6 +27715,10 @@ var SqliteHistorySyncRepository = class {
27511
27715
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27512
27716
  WHERE id = 1`
27513
27717
  );
27718
+ this.disownCapturesStmt = db.prepare(
27719
+ `UPDATE audit_events SET outbox_owed = NULL
27720
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27721
+ );
27514
27722
  this.rearmStmt = db.prepare(
27515
27723
  `UPDATE audit_events SET synced_at = NULL
27516
27724
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27583,6 +27791,10 @@ var SqliteHistorySyncRepository = class {
27583
27791
  closeWindowStmt;
27584
27792
  releaseBoundaryStmt;
27585
27793
  freezeBoundaryStmt;
27794
+ captureRowsStmt;
27795
+ markOwedStmt;
27796
+ captureSkipCountStmt;
27797
+ disownCapturesStmt;
27586
27798
  partitionStmt;
27587
27799
  claimRowStmt;
27588
27800
  releaseRowStmt;
@@ -27616,6 +27828,34 @@ var SqliteHistorySyncRepository = class {
27616
27828
  pendingRows(sessionId, limit, before) {
27617
27829
  return allRows(this.rowsStmt, { sessionId, limit, before });
27618
27830
  }
27831
+ /**
27832
+ * Captures this machine still owes the deployment, oldest first.
27833
+ *
27834
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27835
+ * by a time window — see captureRowsStmt for why a window could not express
27836
+ * this. `before` is the grace window that leaves a just-recorded capture to
27837
+ * the live path.
27838
+ */
27839
+ pendingCaptureRows(limit, before) {
27840
+ return allRows(this.captureRowsStmt, { limit, before });
27841
+ }
27842
+ /**
27843
+ * Record that a capture is OWED to the deployment.
27844
+ *
27845
+ * Written by the attached forward path when a live send did not confirm
27846
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27847
+ * a fact rather than an inference: the machine was attached, the send did not
27848
+ * land, so the row is owed — which no time window can state, because the same
27849
+ * window that holds the rows a past attachment left owed also holds every
27850
+ * capture recorded while the machine was DETACHED, and those were never
27851
+ * offered to anyone.
27852
+ *
27853
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27854
+ * out of the drain's read.
27855
+ */
27856
+ markCaptureOwed(id) {
27857
+ this.markOwedStmt.run({ id });
27858
+ }
27619
27859
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27620
27860
  markSynced(ids, atMs) {
27621
27861
  this.stampAll(ids, atMs);
@@ -27699,10 +27939,12 @@ var SqliteHistorySyncRepository = class {
27699
27939
  this.countsStmt,
27700
27940
  { before }
27701
27941
  );
27942
+ const captures = getRow(this.captureSkipCountStmt);
27702
27943
  return {
27703
27944
  pending: row?.pending ?? 0,
27704
27945
  sent: row?.sent ?? 0,
27705
- skipped: row?.skipped ?? 0
27946
+ skipped: row?.skipped ?? 0,
27947
+ capturesSkipped: captures?.skipped ?? 0
27706
27948
  };
27707
27949
  }
27708
27950
  /**
@@ -27743,7 +27985,11 @@ var SqliteHistorySyncRepository = class {
27743
27985
  withTransaction(
27744
27986
  this.db,
27745
27987
  () => {
27988
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27746
27989
  this.rearmStmt.run();
27990
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27991
+ this.disownCapturesStmt.run();
27992
+ }
27747
27993
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27748
27994
  },
27749
27995
  "IMMEDIATE"
@@ -27940,7 +28186,256 @@ var SqliteInspectionFindingsRepository = class {
27940
28186
  };
27941
28187
 
27942
28188
  // ../../packages/persistence/src/repositories/installed-packs.ts
27943
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28189
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28190
+
28191
+ // ../../packages/persistence/src/policy-floor.ts
28192
+ import { readFileSync as readFileSync5 } from "fs";
28193
+ import { join as join6 } from "path";
28194
+
28195
+ // ../../packages/persistence/src/local-layout.ts
28196
+ import { renameSync as renameSync3 } from "fs";
28197
+ import { mkdir } from "fs/promises";
28198
+ import { homedir } from "os";
28199
+ import { join as join4 } from "path";
28200
+ function defaultDataDir() {
28201
+ return join4(homedir(), ".aka");
28202
+ }
28203
+ function settingsDir(base = defaultDataDir()) {
28204
+ return join4(base, "settings");
28205
+ }
28206
+ function dataDir(base = defaultDataDir()) {
28207
+ return join4(base, "data");
28208
+ }
28209
+ function dbPath(base = defaultDataDir()) {
28210
+ return join4(dataDir(base), "aka.db");
28211
+ }
28212
+ async function ensureDataDir(dir = defaultDataDir()) {
28213
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
28214
+ tightenDir(dir);
28215
+ }
28216
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28217
+ ensureDataDirSync(dir);
28218
+ }
28219
+ function migrateLegacyLayout(base = defaultDataDir()) {
28220
+ const moves = [
28221
+ { name: "config.json", dest: settingsDir(base) },
28222
+ { name: "policy-cache.json", dest: dataDir(base) }
28223
+ ];
28224
+ for (const { name, dest } of moves) {
28225
+ try {
28226
+ ensureDataDirSync(dest);
28227
+ const moved = join4(dest, name);
28228
+ renameSync3(join4(base, name), moved);
28229
+ tightenFile(moved);
28230
+ } catch {
28231
+ }
28232
+ }
28233
+ }
28234
+
28235
+ // ../../packages/persistence/src/settings.ts
28236
+ import { readFileSync as readFileSync4 } from "fs";
28237
+ import { join as join5 } from "path";
28238
+
28239
+ // ../../packages/persistence/src/file-lock.ts
28240
+ import { randomUUID as randomUUID3 } from "crypto";
28241
+ import {
28242
+ closeSync,
28243
+ existsSync as existsSync2,
28244
+ openSync,
28245
+ readFileSync as readFileSync2,
28246
+ rmSync as rmSync5,
28247
+ statSync as statSync3,
28248
+ writeFileSync as writeFileSync2
28249
+ } from "fs";
28250
+ import { hostname as hostname3 } from "os";
28251
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28252
+
28253
+ // ../../packages/persistence/src/managed-settings.ts
28254
+ import { readFileSync as readFileSync3 } from "fs";
28255
+ import { posix, win32 } from "path";
28256
+ function managedSettingsPaths(platform2 = process.platform) {
28257
+ if (platform2 === "darwin") {
28258
+ return [
28259
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28260
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28261
+ ];
28262
+ }
28263
+ if (platform2 === "win32") {
28264
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28265
+ }
28266
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28267
+ }
28268
+ function readManagedSettings(paths = managedSettingsPaths()) {
28269
+ for (const path of paths) {
28270
+ let text;
28271
+ try {
28272
+ text = readFileSync3(path, "utf8");
28273
+ } catch {
28274
+ continue;
28275
+ }
28276
+ const record2 = parseJsonObject(text);
28277
+ if (!record2) continue;
28278
+ const parsed2 = ManagedSettings.safeParse(record2);
28279
+ if (parsed2.success) return parsed2.data;
28280
+ }
28281
+ return null;
28282
+ }
28283
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28284
+ if (!managed) return settings;
28285
+ const { values } = managed;
28286
+ const merged = { ...settings };
28287
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28288
+ if (values.controlPlane !== void 0) {
28289
+ merged.controlPlane = {
28290
+ ...values.controlPlane,
28291
+ // The administrator pinned WHICH deployment, not WHEN this machine
28292
+ // joined it. Keep the user's own attach time when the endpoint is
28293
+ // unchanged, so a managed machine does not appear to re-attach on every
28294
+ // read; stamp a fresh one when the administrator moved it.
28295
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28296
+ };
28297
+ }
28298
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28299
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28300
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28301
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28302
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28303
+ if (values.vaultConsent !== void 0) {
28304
+ merged.vaultConsent = values.vaultConsent ? (
28305
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28306
+ // at the current version otherwise.
28307
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28308
+ ) : void 0;
28309
+ }
28310
+ if (values.modelJudgeConsent !== void 0) {
28311
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28312
+ acknowledgedAt: now().toISOString(),
28313
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28314
+ } : void 0;
28315
+ }
28316
+ return merged;
28317
+ }
28318
+
28319
+ // ../../packages/persistence/src/settings.ts
28320
+ var SETTINGS_FILENAME = "settings.json";
28321
+ function readWorkspaceSettings(base = defaultDataDir()) {
28322
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28323
+ }
28324
+ function readUserSettings(base) {
28325
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28326
+ if (!record2) return defaultWorkspaceSettings();
28327
+ try {
28328
+ return WorkspaceSettings.parse(record2);
28329
+ } catch {
28330
+ return defaultWorkspaceSettings();
28331
+ }
28332
+ }
28333
+ function readJson(file2) {
28334
+ let text;
28335
+ try {
28336
+ text = readFileSync4(file2, "utf8");
28337
+ } catch {
28338
+ return null;
28339
+ }
28340
+ return parseJsonObject(text) ?? null;
28341
+ }
28342
+
28343
+ // ../../packages/persistence/src/policy-floor.ts
28344
+ function refusalMessage(pack, attempted, floor, refusal) {
28345
+ switch (refusal) {
28346
+ case "lock":
28347
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28348
+ case "disable":
28349
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28350
+ case "floor":
28351
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28352
+ }
28353
+ }
28354
+ var PolicyFloorError = class extends Error {
28355
+ /** `namespace/packId` of the detection whose write was refused. */
28356
+ pack;
28357
+ /**
28358
+ * The archetype the caller asked for, or null when the write named none —
28359
+ * clearing the assignment, or switching the detection off.
28360
+ */
28361
+ attempted;
28362
+ /** The weakest archetype the control plane permits for this pack. */
28363
+ floor;
28364
+ refusal;
28365
+ constructor(pack, attempted, floor, refusal) {
28366
+ super(refusalMessage(pack, attempted, floor, refusal));
28367
+ this.name = "PolicyFloorError";
28368
+ this.pack = pack;
28369
+ this.attempted = attempted;
28370
+ this.floor = floor;
28371
+ this.refusal = refusal;
28372
+ }
28373
+ };
28374
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28375
+ try {
28376
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28377
+ const parsed2 = JSON.parse(raw);
28378
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28379
+ return PolicyBundle.parse(parsed2.bundle);
28380
+ } catch {
28381
+ return null;
28382
+ }
28383
+ }
28384
+ function indexEnabled(policies) {
28385
+ const byRuleId = /* @__PURE__ */ new Map();
28386
+ const byCategory = /* @__PURE__ */ new Map();
28387
+ for (const policy of policies) {
28388
+ if (!policy.enabled) continue;
28389
+ if ("ruleId" in policy.target) {
28390
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28391
+ } else if (!byCategory.has(policy.target.category)) {
28392
+ byCategory.set(policy.target.category, policy.action);
28393
+ }
28394
+ }
28395
+ return { byRuleId, byCategory };
28396
+ }
28397
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28398
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28399
+ const categories = new Set(rules.map((rule) => rule.category));
28400
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28401
+ return policies.some((policy) => {
28402
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28403
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28404
+ });
28405
+ }
28406
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28407
+ const floors = openControlPlaneFloors(base);
28408
+ return floors === null ? null : floors.floorFor(rules);
28409
+ }
28410
+ function openControlPlaneFloors(base = defaultDataDir()) {
28411
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28412
+ const bundle = readCachedPolicyBundle(base);
28413
+ if (bundle === null) return null;
28414
+ const indexes = indexEnabled(bundle.policies);
28415
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28416
+ }
28417
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28418
+ let action = null;
28419
+ for (const rule of rules) {
28420
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28421
+ if (resolved === void 0) continue;
28422
+ action = action === null ? resolved : strongerAction(action, resolved);
28423
+ }
28424
+ if (action === null) return null;
28425
+ return {
28426
+ floor: weakestBuiltinAtLeast(action),
28427
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28428
+ };
28429
+ }
28430
+ function policyAssignmentRefusal(policyId, floor) {
28431
+ if (floor.locked) return "lock";
28432
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28433
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28434
+ }
28435
+ function packEnablementRefusal(enabled, floor) {
28436
+ if (floor === null || enabled) return null;
28437
+ return "disable";
28438
+ }
27944
28439
 
27945
28440
  // ../../packages/persistence/src/semver.ts
27946
28441
  function parse3(version2) {
@@ -28034,8 +28529,19 @@ function ruleIdsOf(rulesJson) {
28034
28529
  return ids;
28035
28530
  }
28036
28531
  var SqliteInstalledPacksRepository = class {
28037
- constructor(db) {
28532
+ /**
28533
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28534
+ * floor needs both halves of it (settings/ says whether this machine is
28535
+ * attached, data/ holds the cached bundle). It is optional because a caller
28536
+ * holding only a DatabaseSync — every test construction site, and any embedder
28537
+ * that opens the store itself — has no layout to point at, and such a caller
28538
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28539
+ * from `openLocalDatabase`, which is the single construction site that owns a
28540
+ * real `~/.aka`.
28541
+ */
28542
+ constructor(db, baseDir) {
28038
28543
  this.db = db;
28544
+ this.baseDir = baseDir;
28039
28545
  this.insertMissingStmt = db.prepare(
28040
28546
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
28041
28547
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28057,11 +28563,17 @@ var SqliteInstalledPacksRepository = class {
28057
28563
  this.signatureStmt = db.prepare(
28058
28564
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28059
28565
  );
28566
+ this.packRulesStmt = db.prepare(
28567
+ `SELECT rules_json AS rulesJson FROM installed_packs
28568
+ WHERE namespace = ? AND pack_id = ?`
28569
+ );
28060
28570
  }
28061
28571
  db;
28572
+ baseDir;
28062
28573
  insertMissingStmt;
28063
28574
  upsertAvailableStmt;
28064
28575
  signatureStmt;
28576
+ packRulesStmt;
28065
28577
  /**
28066
28578
  * Record the running binary's detection inventory. Refreshes the
28067
28579
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28103,7 +28615,7 @@ var SqliteInstalledPacksRepository = class {
28103
28615
  let behind = false;
28104
28616
  for (const row of rows) {
28105
28617
  const params = {
28106
- id: randomUUID3(),
28618
+ id: randomUUID4(),
28107
28619
  namespace: row.namespace,
28108
28620
  packId: row.packId,
28109
28621
  version: row.version,
@@ -28115,7 +28627,7 @@ var SqliteInstalledPacksRepository = class {
28115
28627
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28116
28628
  this.upsertAvailableStmt.run({
28117
28629
  ...params,
28118
- id: randomUUID3(),
28630
+ id: randomUUID4(),
28119
28631
  recordedBy: meta4?.recordedBy ?? null
28120
28632
  });
28121
28633
  } else {
@@ -28361,9 +28873,65 @@ var SqliteInstalledPacksRepository = class {
28361
28873
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28362
28874
  // caller rather than swallowing them. Each returns whether a row matched, so the
28363
28875
  // caller can tell an edit from a no-such-detection.
28876
+ /**
28877
+ * The rules one installed pack owns, reduced to what a floor computation
28878
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28879
+ * unreadable contributes no rules to a scan either, so it is not a detection
28880
+ * the control plane can be governing, and an empty list correctly imposes no
28881
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28882
+ * the user can re-enable, and its assignment stays governed meanwhile.
28883
+ */
28884
+ packFloorRules(namespace, packId) {
28885
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28886
+ if (!row) return [];
28887
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28888
+ }
28889
+ /**
28890
+ * What the connected control plane imposes on one installed pack, or null on a
28891
+ * machine that is its own authority (standalone, no cached bundle, or a
28892
+ * repository constructed without a layout base).
28893
+ *
28894
+ * Exposed as a READ so a surface can render the constraint — grey out the
28895
+ * choices below the floor, mark a locked detection as locked — rather than
28896
+ * offer the user a picker whose selections it will then be told it may not
28897
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28898
+ */
28899
+ policyFloor(namespace, packId) {
28900
+ if (this.baseDir === void 0) return null;
28901
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28902
+ }
28903
+ /**
28904
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28905
+ * entry only for a pack the control plane actually governs.
28906
+ *
28907
+ * A surface listing every detection asks per pack, and asking through
28908
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28909
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28910
+ * answer, repeated for each row, on every render. This reads all of that once.
28911
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28912
+ * exactly as the single-pack read returns null for them.
28913
+ */
28914
+ policyFloors(packs) {
28915
+ const floors = /* @__PURE__ */ new Map();
28916
+ if (this.baseDir === void 0) return floors;
28917
+ const source = openControlPlaneFloors(this.baseDir);
28918
+ if (source === null) return floors;
28919
+ for (const pack of packs) {
28920
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28921
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28922
+ }
28923
+ return floors;
28924
+ }
28364
28925
  /**
28365
28926
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28366
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28927
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28928
+ *
28929
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28930
+ * write below, and a detection the organization has authored a policy for is
28931
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28932
+ * a throw rather than a silently substituted value. This is the one device-local
28933
+ * write path for the assignment, so the check belongs here rather than on any
28934
+ * surface that offers the choice.
28367
28935
  */
28368
28936
  setPolicy(namespace, packId, policyId) {
28369
28937
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28371,14 +28939,38 @@ var SqliteInstalledPacksRepository = class {
28371
28939
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28372
28940
  );
28373
28941
  }
28942
+ const requested = policyId;
28943
+ const floor = this.policyFloor(namespace, packId);
28944
+ if (floor !== null) {
28945
+ const refusal = policyAssignmentRefusal(requested, floor);
28946
+ if (refusal !== null) {
28947
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28948
+ }
28949
+ }
28374
28950
  const res = this.db.prepare(
28375
28951
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28376
28952
  WHERE namespace = :namespace AND pack_id = :packId`
28377
28953
  ).run({ policyId, now: Date.now(), namespace, packId });
28378
28954
  return Number(res.changes) > 0;
28379
28955
  }
28380
- /** Enable or disable one installed pack. */
28956
+ /**
28957
+ * Enable or disable one installed pack.
28958
+ *
28959
+ * On an ATTACHED machine a detection the organization's bundle governs at all
28960
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
28961
+ * merely another point below the floor, and why re-enabling stays open. Like
28962
+ * the assignment above, the check belongs at this write path rather than on a
28963
+ * surface: this is the one device-local writer of the column, and a refusal
28964
+ * that lived in a page would leave the CLI free.
28965
+ */
28381
28966
  setEnabled(namespace, packId, enabled) {
28967
+ const floor = this.policyFloor(namespace, packId);
28968
+ if (floor !== null) {
28969
+ const refusal = packEnablementRefusal(enabled, floor);
28970
+ if (refusal !== null) {
28971
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
28972
+ }
28973
+ }
28382
28974
  const res = this.db.prepare(
28383
28975
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28384
28976
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28464,7 +29056,7 @@ var SqliteInventoryRepository = class {
28464
29056
  };
28465
29057
 
28466
29058
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28467
- import { randomUUID as randomUUID4 } from "crypto";
29059
+ import { randomUUID as randomUUID5 } from "crypto";
28468
29060
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28469
29061
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28470
29062
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28953,7 +29545,7 @@ var SqliteInventoryAssetsRepository = class {
28953
29545
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28954
29546
  VALUES (:id, :projectId, :path, :access, :now, :now)
28955
29547
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28956
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29548
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28957
29549
  }
28958
29550
  return true;
28959
29551
  }
@@ -28974,7 +29566,7 @@ var SqliteInventoryAssetsRepository = class {
28974
29566
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28975
29567
  VALUES (:id, :assetId, :trust, :now, :now)
28976
29568
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28977
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29569
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28978
29570
  }
28979
29571
  this.configRowsCache = void 0;
28980
29572
  return "ok";
@@ -29271,7 +29863,7 @@ var SqliteInventoryAssetsRepository = class {
29271
29863
  };
29272
29864
 
29273
29865
  // ../../packages/persistence/src/repositories/policies.ts
29274
- import { randomUUID as randomUUID5 } from "crypto";
29866
+ import { randomUUID as randomUUID6 } from "crypto";
29275
29867
  var SqlitePoliciesRepository = class {
29276
29868
  constructor(db) {
29277
29869
  this.db = db;
@@ -29306,7 +29898,7 @@ var SqlitePoliciesRepository = class {
29306
29898
  failOpenTransaction(this.db, () => {
29307
29899
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29308
29900
  stmt.run({
29309
- id: randomUUID5(),
29901
+ id: randomUUID6(),
29310
29902
  target: JSON.stringify({ category }),
29311
29903
  action,
29312
29904
  now: Date.now()
@@ -29326,7 +29918,7 @@ var SqlitePoliciesRepository = class {
29326
29918
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29327
29919
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29328
29920
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29329
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29921
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29330
29922
  }
29331
29923
  // Caps every global per-category policy currently set to block/redact down
29332
29924
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29394,7 +29986,7 @@ var SqlitePolicyCatalogRepository = class {
29394
29986
  };
29395
29987
 
29396
29988
  // ../../packages/persistence/src/repositories/project-files.ts
29397
- import { randomUUID as randomUUID6 } from "crypto";
29989
+ import { randomUUID as randomUUID7 } from "crypto";
29398
29990
  var SqliteProjectFilesRepository = class {
29399
29991
  constructor(db) {
29400
29992
  this.db = db;
@@ -29426,7 +30018,7 @@ var SqliteProjectFilesRepository = class {
29426
30018
  const stamp = Math.max(now, maxStamp + 1);
29427
30019
  for (const file2 of scan2.files) {
29428
30020
  this.upsertStmt.run({
29429
- id: randomUUID6(),
30021
+ id: randomUUID7(),
29430
30022
  projectId,
29431
30023
  path: file2.path,
29432
30024
  name: file2.name,
@@ -29440,9 +30032,9 @@ var SqliteProjectFilesRepository = class {
29440
30032
  };
29441
30033
 
29442
30034
  // ../../packages/persistence/src/repositories/resolutions.ts
29443
- import { randomUUID as randomUUID7 } from "crypto";
30035
+ import { randomUUID as randomUUID8 } from "crypto";
29444
30036
  var SqliteResolutionsRepository = class {
29445
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30037
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29446
30038
  this.db = db;
29447
30039
  this.now = now;
29448
30040
  this.newId = newId;
@@ -29655,7 +30247,7 @@ var SqliteScanLedgerRepository = class {
29655
30247
  };
29656
30248
 
29657
30249
  // ../../packages/persistence/src/repositories/secret-vault.ts
29658
- import { randomUUID as randomUUID8 } from "crypto";
30250
+ import { randomUUID as randomUUID9 } from "crypto";
29659
30251
  function pageLimit(requested, fallback) {
29660
30252
  if (requested === void 0) return fallback;
29661
30253
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29701,12 +30293,14 @@ var SELECT_COLUMNS = `
29701
30293
  ciphertext,
29702
30294
  nonce,
29703
30295
  auth_tag AS authTag,
30296
+ user_authorized AS userAuthorized,
29704
30297
  occurrence_count AS occurrenceCount,
29705
30298
  first_seen AS firstSeen,
29706
30299
  last_seen AS lastSeen`;
29707
30300
  function toRow(raw) {
29708
- const { provider, ...rest } = raw;
29709
- return provider === null ? rest : { ...rest, provider };
30301
+ const { provider, userAuthorized, ...rest } = raw;
30302
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30303
+ return provider === null ? row : { ...row, provider };
29710
30304
  }
29711
30305
  var SqliteSecretVaultRepository = class {
29712
30306
  constructor(db) {
@@ -29716,17 +30310,18 @@ var SqliteSecretVaultRepository = class {
29716
30310
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29717
30311
  format_version, category, rule_id, masked_match, provider,
29718
30312
  ciphertext, nonce, auth_tag,
29719
- occurrence_count, first_seen, last_seen
30313
+ user_authorized, occurrence_count, first_seen, last_seen
29720
30314
  ) VALUES (
29721
30315
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29722
30316
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29723
30317
  :ciphertext, :nonce, :authTag,
29724
- 1, :now, :now
30318
+ :userAuthorized, 1, :now, :now
29725
30319
  )`
29726
30320
  );
29727
30321
  this.bumpStmt = db.prepare(
29728
30322
  `UPDATE secret_vault
29729
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30323
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30324
+ user_authorized = max(user_authorized, :userAuthorized)
29730
30325
  WHERE value_fingerprint = :valueFingerprint`
29731
30326
  );
29732
30327
  this.byPointerStmt = db.prepare(
@@ -29746,6 +30341,7 @@ var SqliteSecretVaultRepository = class {
29746
30341
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29747
30342
  WHERE pointer_id = :pointerId`
29748
30343
  );
30344
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29749
30345
  this.derefStmt = db.prepare(
29750
30346
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29751
30347
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29759,6 +30355,7 @@ var SqliteSecretVaultRepository = class {
29759
30355
  listStmt;
29760
30356
  replaceCiphertextStmt;
29761
30357
  refreshFingerprintStmt;
30358
+ deleteByPointerStmt;
29762
30359
  derefStmt;
29763
30360
  /**
29764
30361
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29767,6 +30364,11 @@ var SqliteSecretVaultRepository = class {
29767
30364
  * pointer, category and ciphertext, so the same secret always resolves to one
29768
30365
  * wire token. `minted` is true only when this call created the row.
29769
30366
  *
30367
+ * `userAuthorized` is the one field a repeat call may still change, and only
30368
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30369
+ * the row is shared with every automatic path that vaults the same value. See
30370
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30371
+ *
29770
30372
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29771
30373
  * writers cannot both decide they are minting.
29772
30374
  */
@@ -29793,13 +30395,18 @@ var SqliteSecretVaultRepository = class {
29793
30395
  ciphertext: input2.ciphertext,
29794
30396
  nonce: input2.nonce,
29795
30397
  authTag: input2.authTag,
30398
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29796
30399
  now
29797
30400
  })
29798
30401
  );
29799
30402
  minted = true;
29800
30403
  return;
29801
30404
  }
29802
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30405
+ this.bumpStmt.run({
30406
+ valueFingerprint: input2.valueFingerprint,
30407
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30408
+ now
30409
+ });
29803
30410
  },
29804
30411
  "IMMEDIATE"
29805
30412
  );
@@ -29859,6 +30466,42 @@ var SqliteSecretVaultRepository = class {
29859
30466
  );
29860
30467
  return destroyed;
29861
30468
  }
30469
+ /**
30470
+ * Destroy the named entries and report WHICH ones went — the scoped
30471
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30472
+ * values back where they came from. Ids the store does not hold are absent
30473
+ * from the answer rather than an error, so a set assembled from a stale read
30474
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30475
+ * it.
30476
+ *
30477
+ * The ids come back rather than a count because the caller's next act is to
30478
+ * write a purge row per destroyed entry, and a record of destruction has to
30479
+ * be a record of what was really destroyed: a selection is a claim about a
30480
+ * read that has since gone stale, and auditing from it invents a purge for an
30481
+ * entry still sitting in the vault.
30482
+ *
30483
+ * One transaction over the whole set rather than a statement per id: the
30484
+ * caller hands this the result of a restore pass it has completed, and a
30485
+ * fault partway through must leave the vault as it was found rather than
30486
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30487
+ * stands for, so half a delete is not a state anything can recover from.
30488
+ */
30489
+ deleteByPointerIds(pointerIds) {
30490
+ if (pointerIds.length === 0) return [];
30491
+ const deleted = [];
30492
+ withTransaction(
30493
+ this.db,
30494
+ () => {
30495
+ for (const pointerId of pointerIds) {
30496
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30497
+ deleted.push(pointerId);
30498
+ }
30499
+ }
30500
+ },
30501
+ "IMMEDIATE"
30502
+ );
30503
+ return deleted;
30504
+ }
29862
30505
  /**
29863
30506
  * Record (or re-stamp) one place a pointer has been written. One row per
29864
30507
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29871,7 +30514,7 @@ var SqliteSecretVaultRepository = class {
29871
30514
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29872
30515
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29873
30516
  ).run({
29874
- id: randomUUID8(),
30517
+ id: randomUUID9(),
29875
30518
  pointerId: entry.pointerId,
29876
30519
  location: entry.location,
29877
30520
  kind: entry.kind,
@@ -30384,15 +31027,15 @@ var SqliteSecurityRepository = class {
30384
31027
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30385
31028
  const rows = allRows(
30386
31029
  this.db.prepare(
30387
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31030
+ `SELECT e.repo AS repo, count(*) AS c
30388
31031
  FROM inspection_findings f
30389
31032
  JOIN audit_events e ON e.id = f.audit_event_id
30390
31033
  WHERE e.started_at >= :from AND e.started_at < :to
30391
31034
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30392
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30393
- AND json_extract(e.attributes, '$.repo') != ''
30394
- GROUP BY repo
30395
- ORDER BY c DESC, repo
31035
+ AND e.repo IS NOT NULL
31036
+ AND e.repo != ''
31037
+ GROUP BY e.repo
31038
+ ORDER BY c DESC, e.repo
30396
31039
  LIMIT :limit`
30397
31040
  ),
30398
31041
  { from, to: now, limit }
@@ -30454,7 +31097,7 @@ var SqliteSecurityRepository = class {
30454
31097
  `SELECT f.finding_key AS finding_key,
30455
31098
  d.rule_id AS rule_id,
30456
31099
  d.severity AS severity,
30457
- json_extract(e.attributes, '$.file_path') AS path,
31100
+ e.file_path AS path,
30458
31101
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30459
31102
  latest.resolved_at AS latest_resolved_at
30460
31103
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30507,7 +31150,7 @@ var SqliteSecurityRepository = class {
30507
31150
  };
30508
31151
 
30509
31152
  // ../../packages/persistence/src/repositories/shares.ts
30510
- import { randomUUID as randomUUID9 } from "crypto";
31153
+ import { randomUUID as randomUUID10 } from "crypto";
30511
31154
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30512
31155
  var IN_CHUNK = 500;
30513
31156
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30595,7 +31238,7 @@ function buildSummary(dest, endpoints) {
30595
31238
  callSiteCount,
30596
31239
  transports: distinctTransports(transports),
30597
31240
  dataClasses: distinctDataClasses(dataClasses),
30598
- review: buildReviewInfo(dest.trust, transports),
31241
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30599
31242
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30600
31243
  endpoints: endpoints.map(toEndpointSummary)
30601
31244
  };
@@ -30622,7 +31265,7 @@ function buildDetail(dest, endpoints, callSites) {
30622
31265
  lastSeen: new Date(lastSeenMs).toISOString(),
30623
31266
  transports: distinctTransports(transports),
30624
31267
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30625
- review: buildReviewInfo(dest.trust, transports),
31268
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30626
31269
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30627
31270
  note: dest.note,
30628
31271
  endpoints: endpoints.map((ep) => ({
@@ -30651,7 +31294,11 @@ var SqliteSharesRepository = class {
30651
31294
  FROM share_destination d
30652
31295
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30653
31296
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30654
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31297
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31298
+ AND NOT EXISTS (
31299
+ SELECT 1 FROM egress_decision_override o
31300
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31301
+ )`
30655
31302
  );
30656
31303
  const kindCounts = countBy(
30657
31304
  this.db,
@@ -30763,7 +31410,7 @@ var SqliteSharesRepository = class {
30763
31410
  (id, destination_id, host, decision, created_at, updated_at)
30764
31411
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30765
31412
  ).run({
30766
- id: randomUUID9(),
31413
+ id: randomUUID10(),
30767
31414
  destinationId,
30768
31415
  host: dest.host,
30769
31416
  decision,
@@ -30912,7 +31559,7 @@ var SqliteSharesRepository = class {
30912
31559
  let destinationId = destIds.get(hit.host);
30913
31560
  if (destinationId === void 0) {
30914
31561
  destStmt.run({
30915
- id: randomUUID9(),
31562
+ id: randomUUID10(),
30916
31563
  kind: hit.kind,
30917
31564
  name: hit.name,
30918
31565
  host: hit.host,
@@ -30928,7 +31575,7 @@ var SqliteSharesRepository = class {
30928
31575
  let endpointId = endpointIds.get(endpointKey);
30929
31576
  if (endpointId === void 0) {
30930
31577
  endpointStmt.run({
30931
- id: randomUUID9(),
31578
+ id: randomUUID10(),
30932
31579
  destinationId,
30933
31580
  method: hit.method,
30934
31581
  transport: hit.transport,
@@ -30941,7 +31588,7 @@ var SqliteSharesRepository = class {
30941
31588
  endpointIds.set(endpointKey, endpointId);
30942
31589
  }
30943
31590
  siteStmt.run({
30944
- id: randomUUID9(),
31591
+ id: randomUUID10(),
30945
31592
  endpointId,
30946
31593
  project: input2.project,
30947
31594
  projectKey: input2.projectKey,
@@ -31306,6 +31953,7 @@ function purgeSampleData(db) {
31306
31953
  }
31307
31954
 
31308
31955
  // ../../packages/persistence/src/database.ts
31956
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31309
31957
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31310
31958
  "aka.persistence.unsafeTestOnlyRawHandle"
31311
31959
  );
@@ -31353,7 +32001,7 @@ function backupLegacyStore(db, file2) {
31353
32001
  discardStore(file2, backup);
31354
32002
  return backup;
31355
32003
  }
31356
- function openAndInitialize(file2) {
32004
+ function openAndInitialize(file2, base) {
31357
32005
  let db = openWithPragmas(file2);
31358
32006
  try {
31359
32007
  if (isForeignSqliteLineage(db)) {
@@ -31366,7 +32014,7 @@ function openAndInitialize(file2) {
31366
32014
  applyMigrations(db, file2);
31367
32015
  tightenPerms(file2);
31368
32016
  const policies = new SqlitePoliciesRepository(db);
31369
- const installedPacks = new SqliteInstalledPacksRepository(db);
32017
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31370
32018
  const repositories = {
31371
32019
  events: new SqliteEventsRepository(db),
31372
32020
  findings: new SqliteFindingsRepository(db),
@@ -31402,7 +32050,7 @@ function openAndInitialize(file2) {
31402
32050
  }
31403
32051
  function openLocalDatabase(dir) {
31404
32052
  ensureDataDirSync(dir);
31405
- const file2 = join4(dir, DB_FILENAME);
32053
+ const file2 = join7(dir, DB_FILENAME);
31406
32054
  reapStalePartials(file2);
31407
32055
  const {
31408
32056
  db,
@@ -31430,7 +32078,13 @@ function openLocalDatabase(dir) {
31430
32078
  inspectionDefinitions,
31431
32079
  inspectionFindings,
31432
32080
  configInventory
31433
- } = openAndInitialize(file2);
32081
+ } = openAndInitialize(
32082
+ file2,
32083
+ // `dir` is always `<base>/data` — every caller resolves it through
32084
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32085
+ // settings/ and data/, and the pack-policy floor needs both halves.
32086
+ dirname2(dir)
32087
+ );
31434
32088
  function captureRowId(event) {
31435
32089
  return captureId(
31436
32090
  event.metadata?.sessionId ?? null,
@@ -31443,6 +32097,21 @@ function openLocalDatabase(dir) {
31443
32097
  historySync.markSynced([captureRowId(event)], atMs);
31444
32098
  });
31445
32099
  }
32100
+ function markCaptureOwed(event) {
32101
+ failOpenTransaction(db, () => {
32102
+ historySync.markCaptureOwed(captureRowId(event));
32103
+ });
32104
+ }
32105
+ function markAuditEventsDelivered(events2, atMs) {
32106
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32107
+ if (stampable.length === 0) return;
32108
+ failOpenTransaction(db, () => {
32109
+ historySync.markSynced(
32110
+ stampable.map((event) => event.id),
32111
+ atMs
32112
+ );
32113
+ });
32114
+ }
31446
32115
  function recordCapture(event, detected) {
31447
32116
  failOpenTransaction(db, () => {
31448
32117
  const sessionId = event.metadata?.sessionId;
@@ -31529,7 +32198,7 @@ function openLocalDatabase(dir) {
31529
32198
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31530
32199
  if (!definitionId) continue;
31531
32200
  inspectionFindings.insertFinding({
31532
- id: randomUUID10(),
32201
+ id: randomUUID11(),
31533
32202
  auditEventId: record2.scanEvent.id,
31534
32203
  inspectionDefinitionId: definitionId,
31535
32204
  span: finding.span,
@@ -31625,6 +32294,8 @@ function openLocalDatabase(dir) {
31625
32294
  inspectionFindings,
31626
32295
  recordCapture,
31627
32296
  markCaptureDelivered,
32297
+ markCaptureOwed,
32298
+ markAuditEventsDelivered,
31628
32299
  ensureInventory,
31629
32300
  recordConfigScan,
31630
32301
  recordProjectFiles,
@@ -31643,32 +32314,18 @@ function openLocalDatabase(dir) {
31643
32314
  };
31644
32315
  }
31645
32316
 
31646
- // ../../packages/persistence/src/file-lock.ts
31647
- import { randomUUID as randomUUID11 } from "crypto";
31648
- import {
31649
- closeSync,
31650
- existsSync as existsSync2,
31651
- openSync,
31652
- readFileSync as readFileSync2,
31653
- rmSync as rmSync5,
31654
- statSync as statSync3,
31655
- writeFileSync as writeFileSync2
31656
- } from "fs";
31657
- import { hostname as hostname3 } from "os";
31658
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31659
-
31660
32317
  // ../../packages/persistence/src/finding-key.ts
31661
32318
  import { createHash as createHash3 } from "crypto";
31662
32319
 
31663
32320
  // ../../packages/persistence/src/fingerprint.ts
31664
32321
  import { createHmac, randomBytes } from "crypto";
31665
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31666
- import { join as join5 } from "path";
32322
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32323
+ import { join as join8 } from "path";
31667
32324
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31668
32325
  var EXCEPTION_KEY_FILENAME = "exception.key";
31669
32326
  var KEY_MATERIAL_BYTES = 32;
31670
32327
  function keyFilePath(dataDir2) {
31671
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32328
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31672
32329
  }
31673
32330
  function parseKeyFile(raw) {
31674
32331
  const parsed2 = JSON.parse(raw);
@@ -31691,7 +32348,7 @@ function parseKeyFile(raw) {
31691
32348
  function readFingerprintKey(dataDir2) {
31692
32349
  let raw;
31693
32350
  try {
31694
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32351
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31695
32352
  } catch (err) {
31696
32353
  if (err.code === "ENOENT") return null;
31697
32354
  throw err instanceof Error ? err : new Error(String(err));
@@ -31701,143 +32358,12 @@ function readFingerprintKey(dataDir2) {
31701
32358
 
31702
32359
  // ../../packages/persistence/src/history-preview.ts
31703
32360
  import { existsSync as existsSync4 } from "fs";
31704
- import { join as join6 } from "path";
32361
+ import { join as join9 } from "path";
31705
32362
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31706
32363
 
31707
- // ../../packages/persistence/src/local-layout.ts
31708
- import { renameSync as renameSync3 } from "fs";
31709
- import { mkdir } from "fs/promises";
31710
- import { homedir } from "os";
31711
- import { join as join7 } from "path";
31712
- function defaultDataDir() {
31713
- return join7(homedir(), ".aka");
31714
- }
31715
- function settingsDir(base = defaultDataDir()) {
31716
- return join7(base, "settings");
31717
- }
31718
- function dataDir(base = defaultDataDir()) {
31719
- return join7(base, "data");
31720
- }
31721
- function dbPath(base = defaultDataDir()) {
31722
- return join7(dataDir(base), "aka.db");
31723
- }
31724
- async function ensureDataDir(dir = defaultDataDir()) {
31725
- await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
31726
- tightenDir(dir);
31727
- }
31728
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31729
- ensureDataDirSync(dir);
31730
- }
31731
- function migrateLegacyLayout(base = defaultDataDir()) {
31732
- const moves = [
31733
- { name: "config.json", dest: settingsDir(base) },
31734
- { name: "policy-cache.json", dest: dataDir(base) }
31735
- ];
31736
- for (const { name, dest } of moves) {
31737
- try {
31738
- ensureDataDirSync(dest);
31739
- const moved = join7(dest, name);
31740
- renameSync3(join7(base, name), moved);
31741
- tightenFile(moved);
31742
- } catch {
31743
- }
31744
- }
31745
- }
31746
-
31747
- // ../../packages/persistence/src/managed-settings.ts
31748
- import { readFileSync as readFileSync4 } from "fs";
31749
- import { posix, win32 } from "path";
31750
- function managedSettingsPaths(platform2 = process.platform) {
31751
- if (platform2 === "darwin") {
31752
- return [
31753
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31754
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31755
- ];
31756
- }
31757
- if (platform2 === "win32") {
31758
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31759
- }
31760
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31761
- }
31762
- function readManagedSettings(paths = managedSettingsPaths()) {
31763
- for (const path of paths) {
31764
- let text;
31765
- try {
31766
- text = readFileSync4(path, "utf8");
31767
- } catch {
31768
- continue;
31769
- }
31770
- const record2 = parseJsonObject(text);
31771
- if (!record2) continue;
31772
- const parsed2 = ManagedSettings.safeParse(record2);
31773
- if (parsed2.success) return parsed2.data;
31774
- }
31775
- return null;
31776
- }
31777
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31778
- if (!managed) return settings;
31779
- const { values } = managed;
31780
- const merged = { ...settings };
31781
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31782
- if (values.controlPlane !== void 0) {
31783
- merged.controlPlane = {
31784
- ...values.controlPlane,
31785
- // The administrator pinned WHICH deployment, not WHEN this machine
31786
- // joined it. Keep the user's own attach time when the endpoint is
31787
- // unchanged, so a managed machine does not appear to re-attach on every
31788
- // read; stamp a fresh one when the administrator moved it.
31789
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31790
- };
31791
- }
31792
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31793
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31794
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31795
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31796
- if (values.vaultConsent !== void 0) {
31797
- merged.vaultConsent = values.vaultConsent ? (
31798
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31799
- // at the current version otherwise.
31800
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31801
- ) : void 0;
31802
- }
31803
- if (values.modelJudgeConsent !== void 0) {
31804
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31805
- acknowledgedAt: now().toISOString(),
31806
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31807
- } : void 0;
31808
- }
31809
- return merged;
31810
- }
31811
-
31812
- // ../../packages/persistence/src/settings.ts
31813
- import { readFileSync as readFileSync5 } from "fs";
31814
- import { join as join8 } from "path";
31815
- var SETTINGS_FILENAME = "settings.json";
31816
- function readWorkspaceSettings(base = defaultDataDir()) {
31817
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31818
- }
31819
- function readUserSettings(base) {
31820
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31821
- if (!record2) return defaultWorkspaceSettings();
31822
- try {
31823
- return WorkspaceSettings.parse(record2);
31824
- } catch {
31825
- return defaultWorkspaceSettings();
31826
- }
31827
- }
31828
- function readJson(file2) {
31829
- let text;
31830
- try {
31831
- text = readFileSync5(file2, "utf8");
31832
- } catch {
31833
- return null;
31834
- }
31835
- return parseJsonObject(text) ?? null;
31836
- }
31837
-
31838
32364
  // ../../packages/persistence/src/store-symlinks.ts
31839
32365
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31840
- import { dirname as dirname2, join as join9, resolve } from "path";
32366
+ import { dirname as dirname3, join as join10, resolve } from "path";
31841
32367
 
31842
32368
  // ../../packages/persistence/src/vault/crypto.ts
31843
32369
  import {
@@ -31851,19 +32377,19 @@ import {
31851
32377
  // ../../packages/persistence/src/vault/key-provider.ts
31852
32378
  import { execFileSync } from "child_process";
31853
32379
  import { randomBytes as randomBytes2 } from "crypto";
31854
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
31855
- import { join as join10 } from "path";
32380
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32381
+ import { join as join11 } from "path";
31856
32382
 
31857
32383
  // ../../packages/persistence/src/vault/vault.ts
31858
32384
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
31859
32385
 
31860
32386
  // ../../packages/persistence/src/warn-era-cap.ts
31861
32387
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
31862
- import { join as join11 } from "path";
32388
+ import { join as join12 } from "path";
31863
32389
  var MARKER = "warn-era-capped";
31864
32390
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
31865
32391
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
31866
- const marker = join11(dataDir2, MARKER);
32392
+ const marker = join12(dataDir2, MARKER);
31867
32393
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
31868
32394
  const capped = db.policies.capCategoryActions();
31869
32395
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -31908,6 +32434,307 @@ function toEgressIngestRequest(input2) {
31908
32434
  };
31909
32435
  }
31910
32436
 
32437
+ // ../../packages/remote/src/http.ts
32438
+ import { request as httpRequest } from "http";
32439
+ import { request as httpsRequest } from "https";
32440
+ var DEFAULT_TIMEOUT_MS = 1e4;
32441
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
32442
+ var RemoteRequestError = class extends Error {
32443
+ constructor(status) {
32444
+ super(`control-plane request failed with status ${String(status)}`);
32445
+ this.status = status;
32446
+ this.name = "RemoteRequestError";
32447
+ }
32448
+ status;
32449
+ };
32450
+ var RemoteRouteAbsent = class extends Error {
32451
+ constructor(route) {
32452
+ super(`control plane does not serve ${route}`);
32453
+ this.route = route;
32454
+ this.name = "RemoteRouteAbsent";
32455
+ }
32456
+ route;
32457
+ };
32458
+ var RemoteRequestInvalid = class extends Error {
32459
+ constructor(route, cause) {
32460
+ super(`refusing to send a malformed body to ${route}`);
32461
+ this.cause = cause;
32462
+ this.name = "RemoteRequestInvalid";
32463
+ }
32464
+ cause;
32465
+ };
32466
+ var RemoteResponseInvalid = class extends Error {
32467
+ constructor(route, detail) {
32468
+ super(`control plane answered ${route} with ${detail}`);
32469
+ this.name = "RemoteResponseInvalid";
32470
+ }
32471
+ };
32472
+ var RemoteTransportError = class extends Error {
32473
+ /**
32474
+ * The status the peer sent, when headers arrived and only the BODY was
32475
+ * refused.
32476
+ *
32477
+ * Undefined for the ordinary case this class was written for — no answer at
32478
+ * all. It exists because two paths reject after a status has already been
32479
+ * delivered: an oversized body and an aborted response. Discarding it there
32480
+ * reported a deployment answering 401 with a verbose body as a network
32481
+ * outage, which sends the reader to look at their network instead of their
32482
+ * credential.
32483
+ */
32484
+ constructor(reason, status) {
32485
+ super(`control-plane request did not complete: ${reason}`);
32486
+ this.status = status;
32487
+ this.name = "RemoteTransportError";
32488
+ }
32489
+ status;
32490
+ };
32491
+ async function send(options) {
32492
+ const url2 = new URL(options.url);
32493
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32494
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
32495
+ const requestOptions = {
32496
+ method: options.method,
32497
+ headers: {
32498
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
32499
+ // last they win, and two of the values below are ones no caller may
32500
+ // replace: `x-api-key` is the credential, and `content-length` is the
32501
+ // byte count that stops a multi-byte body being truncated by the
32502
+ // receiver. `SendOptions.headers` is a free-form record on an exported
32503
+ // function, so "no caller does that today" is not the guarantee to rely
32504
+ // on. The one header any caller actually passes — `if-none-match` on the
32505
+ // conditional GET — is untouched by this order.
32506
+ ...options.headers,
32507
+ // The credential. One header, matching what the deployment authenticates
32508
+ // on; a second copy in an `Authorization` header would be one more place
32509
+ // it can be logged by an intermediary for no gain.
32510
+ //
32511
+ // Spread conditionally rather than assigned as `undefined`: Node's header
32512
+ // handling and `content-length` bookkeeping treat a present-but-undefined
32513
+ // key differently from an absent one, and "the header is not there" is
32514
+ // the property the attach flow needs.
32515
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
32516
+ accept: "application/json",
32517
+ ...options.body === void 0 ? {} : {
32518
+ "content-type": "application/json",
32519
+ // Byte length, not string length: a multi-byte body sent with a
32520
+ // character count is truncated by the receiver.
32521
+ "content-length": String(Buffer.byteLength(options.body))
32522
+ }
32523
+ }
32524
+ };
32525
+ return new Promise((resolve2, reject) => {
32526
+ let settled = false;
32527
+ const fail = (reason, status) => {
32528
+ if (settled) return;
32529
+ settled = true;
32530
+ reject(new RemoteTransportError(reason, status));
32531
+ };
32532
+ const req = send_(url2, requestOptions, (res) => {
32533
+ const chunks = [];
32534
+ let size = 0;
32535
+ res.on("data", (chunk) => {
32536
+ size += chunk.length;
32537
+ if (size > MAX_RESPONSE_BYTES) {
32538
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
32539
+ res.destroy();
32540
+ req.destroy();
32541
+ return;
32542
+ }
32543
+ chunks.push(chunk);
32544
+ });
32545
+ res.on("aborted", () => {
32546
+ fail("the response was aborted", res.statusCode);
32547
+ });
32548
+ res.on("end", () => {
32549
+ if (settled) return;
32550
+ settled = true;
32551
+ resolve2({
32552
+ status: res.statusCode ?? 0,
32553
+ headers: res.headers,
32554
+ body: Buffer.concat(chunks).toString("utf8")
32555
+ });
32556
+ });
32557
+ });
32558
+ const deadline = setTimeout(() => {
32559
+ fail(`no response within ${String(timeoutMs)}ms`);
32560
+ req.destroy();
32561
+ }, timeoutMs);
32562
+ deadline.unref();
32563
+ req.on("upgrade", (_res, socket) => {
32564
+ fail("the deployment answered with a protocol upgrade");
32565
+ socket.destroy();
32566
+ });
32567
+ req.on("close", () => {
32568
+ fail("the connection closed before a response was read");
32569
+ clearTimeout(deadline);
32570
+ });
32571
+ req.on("error", (err) => {
32572
+ fail(err.message);
32573
+ });
32574
+ if (options.body !== void 0) req.write(options.body);
32575
+ req.end();
32576
+ });
32577
+ }
32578
+
32579
+ // ../../packages/remote/src/client.ts
32580
+ var ROUTES = {
32581
+ events: "/v1/events",
32582
+ auditEvents: "/v1/audit-events",
32583
+ auditEventsBatch: "/v1/audit-events/batch",
32584
+ inventory: "/v1/inventory",
32585
+ storePosture: "/v1/store-posture",
32586
+ policyBundle: "/v1/policy-bundle",
32587
+ whoami: "/v1/plugin/whoami",
32588
+ shares: "/v1/shares",
32589
+ commands: "/v1/plugin/commands"
32590
+ };
32591
+ function ackRoute(id) {
32592
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
32593
+ }
32594
+ function headerValue(response, name) {
32595
+ const raw = response.headers[name];
32596
+ if (raw === void 0) return void 0;
32597
+ return Array.isArray(raw) ? raw[0] : raw;
32598
+ }
32599
+ function okBody(response) {
32600
+ if (response.status < 200 || response.status >= 300) {
32601
+ throw new RemoteRequestError(response.status);
32602
+ }
32603
+ return response.body;
32604
+ }
32605
+ function parsed(schema, body, route) {
32606
+ let json2;
32607
+ try {
32608
+ json2 = JSON.parse(body);
32609
+ } catch {
32610
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
32611
+ }
32612
+ const result = schema.safeParse(json2);
32613
+ if (!result.success) {
32614
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
32615
+ }
32616
+ return result.data;
32617
+ }
32618
+ function withoutTrailingSlashes(endpoint) {
32619
+ let end = endpoint.length;
32620
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
32621
+ return endpoint.slice(0, end);
32622
+ }
32623
+ var SLASH = "/".charCodeAt(0);
32624
+ function createRemoteClient(options) {
32625
+ const base = withoutTrailingSlashes(options.endpoint);
32626
+ const url2 = (route) => `${base}${route}`;
32627
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
32628
+ const sendOne = async (event) => {
32629
+ const validated = RecordAuditEventRequest.safeParse(event);
32630
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
32631
+ const response = await send({
32632
+ ...common,
32633
+ method: "POST",
32634
+ url: url2(ROUTES.auditEvents),
32635
+ body: JSON.stringify(validated.data)
32636
+ });
32637
+ okBody(response);
32638
+ };
32639
+ return {
32640
+ async ingestEvents(batch) {
32641
+ const response = await send({
32642
+ ...common,
32643
+ method: "POST",
32644
+ url: url2(ROUTES.events),
32645
+ body: JSON.stringify(batch)
32646
+ });
32647
+ return parsed(IngestAck, okBody(response), ROUTES.events);
32648
+ },
32649
+ async ingestInventory(context) {
32650
+ const response = await send({
32651
+ ...common,
32652
+ method: "POST",
32653
+ url: url2(ROUTES.inventory),
32654
+ body: JSON.stringify(context)
32655
+ });
32656
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
32657
+ },
32658
+ async recordAuditEvent(event) {
32659
+ await sendOne(event);
32660
+ },
32661
+ async recordAuditEvents(events, opts) {
32662
+ const validated = RecordAuditEventBatch.safeParse({ events });
32663
+ if (!validated.success) {
32664
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
32665
+ }
32666
+ const response = await send({
32667
+ ...common,
32668
+ method: "POST",
32669
+ url: url2(ROUTES.auditEventsBatch),
32670
+ body: JSON.stringify(validated.data)
32671
+ });
32672
+ if (response.status === 404) {
32673
+ if (opts?.fallbackToSingleEvents !== true) {
32674
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
32675
+ }
32676
+ for (const event of validated.data.events) await sendOne(event);
32677
+ return { accepted: validated.data.events.length };
32678
+ }
32679
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
32680
+ },
32681
+ async reportStorePosture(snapshot) {
32682
+ const response = await send({
32683
+ ...common,
32684
+ method: "POST",
32685
+ url: url2(ROUTES.storePosture),
32686
+ body: JSON.stringify(snapshot)
32687
+ });
32688
+ okBody(response);
32689
+ },
32690
+ async getPolicyBundle(etag) {
32691
+ const response = await send({
32692
+ ...common,
32693
+ method: "GET",
32694
+ url: url2(ROUTES.policyBundle),
32695
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
32696
+ });
32697
+ if (response.status === 304) {
32698
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
32699
+ }
32700
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
32701
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
32702
+ },
32703
+ async whoami() {
32704
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
32705
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
32706
+ },
32707
+ async recordProjectEgress(request) {
32708
+ const validated = EgressIngestRequest.safeParse(request);
32709
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
32710
+ const response = await send({
32711
+ ...common,
32712
+ method: "POST",
32713
+ url: url2(ROUTES.shares),
32714
+ body: JSON.stringify(validated.data)
32715
+ });
32716
+ okBody(response);
32717
+ },
32718
+ async pollCommand() {
32719
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
32720
+ if (response.status === 404) return null;
32721
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
32722
+ },
32723
+ async ackCommand(id, body) {
32724
+ const validated = DeviceCommandAckBody.safeParse(body);
32725
+ const route = ackRoute(id);
32726
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
32727
+ const response = await send({
32728
+ ...common,
32729
+ method: "POST",
32730
+ url: url2(route),
32731
+ body: JSON.stringify(validated.data)
32732
+ });
32733
+ okBody(response);
32734
+ }
32735
+ };
32736
+ }
32737
+
31911
32738
  // ../../packages/plugin-runtime/src/attached/failure.ts
31912
32739
  function statusOf(err) {
31913
32740
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -31926,12 +32753,27 @@ function classifyFailure(err) {
31926
32753
  }
31927
32754
  }
31928
32755
 
32756
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
32757
+ var REQUEST_TIMEOUT_MS = 2e3;
32758
+ function withTimeout(promise2, ms) {
32759
+ let timer;
32760
+ const timeout = new Promise((_, reject) => {
32761
+ timer = setTimeout(() => {
32762
+ reject(new Error("attached gateway request timed out"));
32763
+ }, ms);
32764
+ });
32765
+ promise2.catch(() => void 0);
32766
+ return Promise.race([promise2, timeout]).finally(() => {
32767
+ clearTimeout(timer);
32768
+ });
32769
+ }
32770
+
31929
32771
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
31930
- import { readFileSync as readFileSync7 } from "fs";
31931
- import { join as join12 } from "path";
32772
+ import { readFileSync as readFileSync8 } from "fs";
32773
+ import { join as join13 } from "path";
31932
32774
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
31933
32775
  function forwardDropsPath(dataDir2) {
31934
- return join12(dataDir2, FORWARD_DROPS_FILENAME);
32776
+ return join13(dataDir2, FORWARD_DROPS_FILENAME);
31935
32777
  }
31936
32778
  function recordForwardDrops(dataDir2, count, nowMs) {
31937
32779
  if (count <= 0) return;
@@ -31949,7 +32791,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
31949
32791
  }
31950
32792
  function readForwardDrops(dataDir2) {
31951
32793
  try {
31952
- const parsed2 = JSON.parse(readFileSync7(forwardDropsPath(dataDir2), "utf8"));
32794
+ const parsed2 = JSON.parse(readFileSync8(forwardDropsPath(dataDir2), "utf8"));
31953
32795
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
31954
32796
  const record2 = parsed2;
31955
32797
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -31967,13 +32809,13 @@ function readForwardDrops(dataDir2) {
31967
32809
 
31968
32810
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
31969
32811
  import { randomUUID as randomUUID15 } from "crypto";
31970
- import { readFileSync as readFileSync13 } from "fs";
32812
+ import { readFileSync as readFileSync14 } from "fs";
31971
32813
  import { readFile, rename, writeFile } from "fs/promises";
31972
- import { join as join21 } from "path";
32814
+ import { join as join22 } from "path";
31973
32815
 
31974
32816
  // ../../packages/plugin-sdk/src/config.ts
31975
32817
  import { existsSync as existsSync7 } from "fs";
31976
- import { join as join13 } from "path";
32818
+ import { join as join14 } from "path";
31977
32819
 
31978
32820
  // ../../packages/plugin-sdk/src/provider-env.ts
31979
32821
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32027,7 +32869,7 @@ function resolveProvider() {
32027
32869
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32028
32870
  try {
32029
32871
  ensureLayoutDirSync(base);
32030
- const settingsFile = join13(settingsDir(base), "settings.json");
32872
+ const settingsFile = join14(settingsDir(base), "settings.json");
32031
32873
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
32032
32874
  } catch {
32033
32875
  }
@@ -32051,9 +32893,9 @@ function resolveProviderSafe(resolveProviderFn) {
32051
32893
  }
32052
32894
 
32053
32895
  // ../../packages/plugin-sdk/src/config-inventory.ts
32054
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32896
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32055
32897
  import { homedir as homedir2 } from "os";
32056
- import { basename as basename3, join as join15 } from "path";
32898
+ import { basename as basename3, join as join16 } from "path";
32057
32899
 
32058
32900
  // ../../packages/detections/src/egress/registry.ts
32059
32901
  var EXTRACTOR_VERSION = "1";
@@ -34836,8 +35678,8 @@ function bundledDetections() {
34836
35678
  }
34837
35679
 
34838
35680
  // ../../packages/plugin-sdk/src/repo.ts
34839
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
34840
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join14, sep as sep2 } from "path";
35681
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35682
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
34841
35683
 
34842
35684
  // ../../packages/plugin-sdk/src/events.ts
34843
35685
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
@@ -34849,8 +35691,8 @@ import { Worker } from "worker_threads";
34849
35691
 
34850
35692
  // ../../packages/plugin-sdk/src/ignore-layers.ts
34851
35693
  var import_ignore = __toESM(require_ignore(), 1);
34852
- import { readFileSync as readFileSync10 } from "fs";
34853
- import { join as join16 } from "path";
35694
+ import { readFileSync as readFileSync11 } from "fs";
35695
+ import { join as join17 } from "path";
34854
35696
 
34855
35697
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
34856
35698
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -34861,24 +35703,24 @@ import {
34861
35703
  fstatSync,
34862
35704
  mkdirSync as mkdirSync2,
34863
35705
  openSync as openSync2,
34864
- readFileSync as readFileSync11,
35706
+ readFileSync as readFileSync12,
34865
35707
  readSync,
34866
35708
  writeFileSync as writeFileSync5
34867
35709
  } from "fs";
34868
- import { join as join17 } from "path";
35710
+ import { join as join18 } from "path";
34869
35711
  var TAIL_BYTES = 256 * 1024;
34870
35712
 
34871
35713
  // ../../packages/plugin-sdk/src/nudge.ts
34872
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
34873
- import { join as join18 } from "path";
35714
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
35715
+ import { join as join19 } from "path";
34874
35716
 
34875
35717
  // ../../packages/plugin-sdk/src/paths.ts
34876
35718
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
34877
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
35719
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
34878
35720
 
34879
35721
  // ../../packages/plugin-sdk/src/project-files.ts
34880
35722
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
34881
- import { basename as basename5, join as join19 } from "path";
35723
+ import { basename as basename5, join as join20 } from "path";
34882
35724
 
34883
35725
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
34884
35726
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -34914,27 +35756,19 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
34914
35756
 
34915
35757
  // ../../packages/plugin-sdk/src/throttle.ts
34916
35758
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
34917
- import { join as join20 } from "path";
34918
-
34919
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
34920
- var REQUEST_TIMEOUT_MS = 2e3;
34921
- function withTimeout(promise2, ms) {
34922
- let timer;
34923
- const timeout = new Promise((_, reject) => {
34924
- timer = setTimeout(() => {
34925
- reject(new Error("attached gateway request timed out"));
34926
- }, ms);
34927
- });
34928
- promise2.catch(() => void 0);
34929
- return Promise.race([promise2, timeout]).finally(() => {
34930
- clearTimeout(timer);
34931
- });
34932
- }
35759
+ import { join as join21 } from "path";
34933
35760
 
34934
35761
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
34935
35762
  function isInvalidRequest(err) {
34936
35763
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
34937
35764
  }
35765
+ function isRouteAbsent(err) {
35766
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
35767
+ }
35768
+ function isServerRejection(err) {
35769
+ const status = statusOf(err);
35770
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
35771
+ }
34938
35772
  var FORWARD_BUDGET_MS = 1500;
34939
35773
  var DECISION_PATH_BUDGET_MS = 800;
34940
35774
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -34962,7 +35796,7 @@ function parseBreakerState(raw, nowMs) {
34962
35796
  }
34963
35797
  function createForwardPolicy(deps) {
34964
35798
  const now = deps.now ?? (() => Date.now());
34965
- const file2 = join21(deps.dir, STATE_FILENAME);
35799
+ const file2 = join22(deps.dir, STATE_FILENAME);
34966
35800
  let state = null;
34967
35801
  let loading = null;
34968
35802
  async function readState() {
@@ -35002,6 +35836,20 @@ function createForwardPolicy(deps) {
35002
35836
  } catch {
35003
35837
  current = { ...CLOSED };
35004
35838
  }
35839
+ const restoreOpenedAtMs = (openedAtMs) => persist({
35840
+ consecutiveFailures: current.consecutiveFailures,
35841
+ openedAtMs,
35842
+ lastFailure: current.lastFailure
35843
+ });
35844
+ const recordFailure = (cause) => {
35845
+ const failures = current.consecutiveFailures + 1;
35846
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
35847
+ return persist({
35848
+ consecutiveFailures: failures,
35849
+ openedAtMs: shouldOpen ? now() : null,
35850
+ lastFailure: cause
35851
+ });
35852
+ };
35005
35853
  const at = now();
35006
35854
  if (current.openedAtMs !== null) {
35007
35855
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -35020,15 +35868,20 @@ function createForwardPolicy(deps) {
35020
35868
  }
35021
35869
  return { ok: true, value };
35022
35870
  } catch (err) {
35023
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
35871
+ if (isInvalidRequest(err)) {
35872
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
35873
+ return { ok: false, reason: "invalid-request" };
35874
+ }
35875
+ if (isRouteAbsent(err)) {
35876
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
35877
+ return { ok: false, reason: "route-absent" };
35878
+ }
35879
+ if (isServerRejection(err)) {
35880
+ await recordFailure("unreachable");
35881
+ return { ok: false, reason: "rejected" };
35882
+ }
35024
35883
  const reason = classifyFailure(err);
35025
- const failures = current.consecutiveFailures + 1;
35026
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
35027
- await persist({
35028
- consecutiveFailures: failures,
35029
- openedAtMs: shouldOpen ? now() : null,
35030
- lastFailure: reason
35031
- });
35884
+ await recordFailure(reason);
35032
35885
  return { ok: false, reason };
35033
35886
  }
35034
35887
  }
@@ -35036,13 +35889,11 @@ function createForwardPolicy(deps) {
35036
35889
  }
35037
35890
 
35038
35891
  // ../../packages/plugin-runtime/src/attached/gateway.ts
35039
- var ACTION_STRENGTH = {
35040
- allow: 0,
35041
- log: 1,
35042
- warn: 2,
35043
- redact: 3,
35044
- block: 4
35045
- };
35892
+ function strongerOf(a, b) {
35893
+ if (a === null) return b;
35894
+ if (b === null) return a;
35895
+ return strongerAction(a, b);
35896
+ }
35046
35897
  function ruleCategoryMap(wireRules, localRules) {
35047
35898
  const map2 = /* @__PURE__ */ new Map();
35048
35899
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -35052,11 +35903,6 @@ function ruleCategoryMap(wireRules, localRules) {
35052
35903
  }
35053
35904
  return map2;
35054
35905
  }
35055
- function strongerOf(a, b) {
35056
- if (a === null) return b;
35057
- if (b === null) return a;
35058
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
35059
- }
35060
35906
  function policyKey(policy) {
35061
35907
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
35062
35908
  }
@@ -35075,7 +35921,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35075
35921
  const floor = floorFor(policy, categoryByRuleId);
35076
35922
  remoteCategoryAction.set(
35077
35923
  policy.target.category,
35078
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
35924
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
35079
35925
  );
35080
35926
  }
35081
35927
  for (const policy of localPolicies) {
@@ -35092,7 +35938,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35092
35938
  }
35093
35939
  merged.set(
35094
35940
  key,
35095
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
35941
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
35096
35942
  );
35097
35943
  }
35098
35944
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -35112,13 +35958,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35112
35958
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
35113
35959
  }
35114
35960
  const effectiveFloor = strongerOf(floor, localFloor);
35115
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
35961
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
35116
35962
  const existing = merged.get(key);
35117
35963
  if (existing === void 0) {
35118
35964
  merged.set(key, clamped);
35119
35965
  continue;
35120
35966
  }
35121
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
35967
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
35122
35968
  merged.set(key, clamped);
35123
35969
  }
35124
35970
  }
@@ -35151,6 +35997,8 @@ var AttachedDataGateway = class {
35151
35997
  );
35152
35998
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
35153
35999
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
36000
+ } else {
36001
+ this.deps.local.markCaptureOwed(record2.event);
35154
36002
  }
35155
36003
  }
35156
36004
  async ensureInventory(ctx) {
@@ -35187,9 +36035,10 @@ var AttachedDataGateway = class {
35187
36035
  // a retried tool_call, exactly this path — can never stomp a populated row.
35188
36036
  async recordAuditEvent(event) {
35189
36037
  await this.deps.local.recordAuditEvent(event);
35190
- await this.deps.forward.run(
36038
+ const forwarded = await this.deps.forward.run(
35191
36039
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
35192
36040
  );
36041
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
35193
36042
  }
35194
36043
  // Attached `llm_call` is written locally by the inner gateway, then routed to
35195
36044
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -35198,44 +36047,170 @@ var AttachedDataGateway = class {
35198
36047
  // which would write the event to the local store a second time.
35199
36048
  async recordLlmCall(input2) {
35200
36049
  await this.deps.local.recordLlmCall(input2);
35201
- await this.deps.forward.run(
35202
- () => this.deps.client.recordAuditEvent(
35203
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
35204
- )
36050
+ const event = llmAuditEvent(input2);
36051
+ const forwarded = await this.deps.forward.run(
36052
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
35205
36053
  );
36054
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
35206
36055
  }
35207
36056
  /**
35208
- * Forward one batch, item by item, under ONE aggregate deadline.
36057
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
36058
+ *
36059
+ * This used to send one HTTP request per event, which is what made the batch
36060
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
36061
+ * away everything after them. The same rows now cross 50 at a time over
36062
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
36063
+ * used — so the same budget admits ~750. The wire cap is the server's own
36064
+ * constant, sized against server cost, and the client REFUSES a longer array
36065
+ * client-side, so the chunking here is not a convention.
36066
+ *
36067
+ * Still serial, and still for the original reason: firing N requests at once
36068
+ * would trade a latency problem for a burst the plane's per-key rate limiting
36069
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
36070
+ * the fix; more concurrent ones is not.
35209
36071
  *
35210
- * Per-item budgets bound each request and nothing bounded their sum see
35211
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
35212
- * rather than sent: the local write has already succeeded, so every caller
35213
- * has a correct result to return, and a drop is the outcome this path is
35214
- * built to accept (G8) where a blown hook timeout is not.
36072
+ * When the deadline passes the remainder is dropped rather than sent: the
36073
+ * local write has already succeeded, so every caller has a correct result to
36074
+ * return. What is dropped is COUNTED, everywhere it can happen this path
36075
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
36076
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
36077
+ * the breaker closed, renders a healthy block, and discards the tail of every
36078
+ * batch indefinitely. The SAME tally also covers a single that fails inside
36079
+ * the per-item retry below — the breaker opening mid-retry is a failure the
36080
+ * breaker's own state DOES capture, but the events still in this chunk once
36081
+ * that happens are neither delivered nor otherwise counted anywhere, which is
36082
+ * the same invisibility with a different cause.
35215
36083
  *
35216
- * Serial rather than concurrent on purpose. Firing N requests at once would
35217
- * trade a latency problem for a burst the plane's own per-key rate limiting
35218
- * would answer with the refusals the breaker then counts.
36084
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
36085
+ * single-event ack and at fifty times the blast radius here:
36086
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
36087
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
36088
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
36089
+ * delivered and never re-offer the twenty the plane did not take. So success
36090
+ * is checked against `chunk.length`; anything short of it falls into the same
36091
+ * per-item pass as a refused chunk, which is the only way to recover the
36092
+ * rows that did not land, since the ack carries no per-row verdict to
36093
+ * resend by.
35219
36094
  *
35220
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
35221
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
35222
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
35223
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
35224
- * plane produces no failures, keeps the breaker closed, renders a healthy
35225
- * block, and discards the tail of every batch indefinitely.
36095
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
36096
+ * no-op rather than a second cost — an assumption this file cannot verify.
36097
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
36098
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
36099
+ * the batch size as the invariant `recordCapture` reads), so whether a
36100
+ * duplicate counts toward THIS route's `accepted` is not expressed
36101
+ * anywhere in this repo. If it follows its sibling's convention and does
36102
+ * NOT, a chunk containing even one already-delivered row — the ordinary
36103
+ * consequence of a lost stamp, which this file already treats as cheap —
36104
+ * answers short forever and enters the per-item pass on every pass it is
36105
+ * offered again. The cost of that is bounded rather than silent: the
36106
+ * pass converges (every row lands and stamps), so it is one wasted round
36107
+ * of singles rather than a stall, and it errs toward an extra resend
36108
+ * rather than toward the lost row the alternative risks.
36109
+ *
36110
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
36111
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
36112
+ * settles none — which is why the whole chunk is stamped together on a FULL
36113
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
36114
+ * treatment, alongside a short accept, and all are re-sent one event at a
36115
+ * time:
36116
+ *
36117
+ * `invalid-request` a chunk the client refused to send at all. One malformed
36118
+ * event would otherwise cost the 49 good ones beside it —
36119
+ * a new way to lose data introduced by the very change
36120
+ * meant to stop losing it.
36121
+ * `route-absent` a deployment that predates the batch route. The
36122
+ * single-event route is the one it serves, and re-sending
36123
+ * here rather than inside the client is what gives each
36124
+ * request its own budget instead of 50 inside one.
36125
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
36126
+ * a 4xx body refusal from schema drift on the other side
36127
+ * of the wire. Settlement is batch-atomic on this reason
36128
+ * exactly as on the others, so leaving it out would cost
36129
+ * the whole chunk for one event the DEPLOYMENT considers
36130
+ * malformed, where the per-item form cost only that one.
36131
+ *
36132
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
36133
+ * chunk, and re-sending it item by item would just spend the budget failing 50
36134
+ * more times — for those, the blast radius stays exactly what it was before
36135
+ * batching.
35226
36136
  */
35227
36137
  async forwardBatch(inputs, toEvent) {
35228
36138
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
35229
- for (let i = 0; i < inputs.length; i += 1) {
35230
- const now = Date.now();
35231
- if (now >= deadline) {
35232
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
35233
- return;
36139
+ const delivered = [];
36140
+ try {
36141
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
36142
+ const now = Date.now();
36143
+ if (now >= deadline) {
36144
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
36145
+ return;
36146
+ }
36147
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
36148
+ const forwarded = await this.deps.forward.run(
36149
+ () => this.deps.client.recordAuditEvents(
36150
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
36151
+ )
36152
+ );
36153
+ if (forwarded.ok) {
36154
+ if (forwarded.value.accepted === chunk.length) {
36155
+ delivered.push(...chunk);
36156
+ continue;
36157
+ }
36158
+ } else if (
36159
+ // THREE reasons are worth a second pass, one at a time, and they are
36160
+ // the three settled BEFORE the control plane refused anything, or
36161
+ // (for `rejected`) refused the BODY rather than the connection.
36162
+ //
36163
+ // `invalid-request` — the CLIENT refused the body before any request
36164
+ // went out: a defect in one event, not an outage. Re-sending singly
36165
+ // isolates the bad one instead of charging its 49 neighbours for it.
36166
+ //
36167
+ // `route-absent` — the deployment predates the batch route and serves
36168
+ // only the single-event one. The retry IS the compatibility path, and
36169
+ // it has to live HERE rather than inside the client: each single gets
36170
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
36171
+ // fallback would spend 50 sequential round trips inside the ONE
36172
+ // budget wrapping this call — turning a working older deployment into
36173
+ // a timeout, three of those into an open breaker, and every row into
36174
+ // a silent drop while the status surface called an answering
36175
+ // deployment down.
36176
+ //
36177
+ // `rejected` — the deployment's own 4xx refusal of the body, the
36178
+ // server-side twin of `invalid-request`: isolating it the same way
36179
+ // costs one event instead of the whole chunk for a defect the
36180
+ // deployment considers local to one row.
36181
+ //
36182
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
36183
+ // the whole chunk; re-sending it item by item would just spend the
36184
+ // budget failing 50 more times.
36185
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
36186
+ ) {
36187
+ continue;
36188
+ }
36189
+ for (const [j, event] of chunk.entries()) {
36190
+ const at = Date.now();
36191
+ if (at >= deadline) {
36192
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
36193
+ return;
36194
+ }
36195
+ const single = await this.deps.forward.run(
36196
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
36197
+ );
36198
+ if (single.ok) {
36199
+ delivered.push(event);
36200
+ continue;
36201
+ }
36202
+ if (single.reason === "breaker-open") {
36203
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
36204
+ return;
36205
+ }
36206
+ recordForwardDrops(this.deps.dataDir, 1, at);
36207
+ }
36208
+ }
36209
+ } finally {
36210
+ try {
36211
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
36212
+ } catch {
35234
36213
  }
35235
- const input2 = inputs[i];
35236
- await this.deps.forward.run(
35237
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
35238
- );
35239
36214
  }
35240
36215
  }
35241
36216
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -35278,9 +36253,10 @@ var AttachedDataGateway = class {
35278
36253
  // local store.
35279
36254
  async recordConfigScan(record2) {
35280
36255
  await this.deps.local.recordConfigScan(record2);
35281
- await this.deps.forward.run(
36256
+ const forwarded = await this.deps.forward.run(
35282
36257
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
35283
36258
  );
36259
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
35284
36260
  }
35285
36261
  async recordBlockedDetection(entry) {
35286
36262
  return this.deps.local.recordBlockedDetection(entry);
@@ -35414,6 +36390,18 @@ var AttachedDataGateway = class {
35414
36390
  // exactly what it did, leaving the whole control inert on every device
35415
36391
  // while every test around it stayed green.
35416
36392
  prohibitedModels: cached2.prohibitedModels
36393
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
36394
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
36395
+ // it emits, so an 'authored' policy arriving from the control plane
36396
+ // keeps that marker even where the clamp rebuilds it with a stronger
36397
+ // action. The device reads it in exactly one direction — the rules such a
36398
+ // policy targets are not locally re-assignable — so it sits on the
36399
+ // `prohibitedModels` side of the line for the same reason that field
36400
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
36401
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
36402
+ // would be the silent failure rather than the safe one — the action would
36403
+ // still be enforced while the local override the organization authored
36404
+ // away quietly came back.
35417
36405
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
35418
36406
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
35419
36407
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -35453,10 +36441,10 @@ var AttachedDataGateway = class {
35453
36441
  //
35454
36442
  // Implementing these is what actually closes the skipped-local-maintenance
35455
36443
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
35456
- // any object carrying all five, so the composite qualifies and SessionStart
36444
+ // any object carrying them all, so the composite qualifies and SessionStart
35457
36445
  // runs maintenance on the device's real store.
35458
36446
  //
35459
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
36447
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
35460
36448
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
35461
36449
  // return value directly; declaring them `async` here would hand those call
35462
36450
  // sites a Promise and silently break both.
@@ -35479,9 +36467,15 @@ var AttachedDataGateway = class {
35479
36467
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
35480
36468
  // gives: `recordCapture` calls it after the forward has already settled, on a
35481
36469
  // path that has nothing left to await.
36470
+ markCaptureOwed(event) {
36471
+ this.deps.local.markCaptureOwed(event);
36472
+ }
35482
36473
  markCaptureDelivered(event, atMs) {
35483
36474
  this.deps.local.markCaptureDelivered(event, atMs);
35484
36475
  }
36476
+ markAuditEventsDelivered(events, atMs) {
36477
+ this.deps.local.markAuditEventsDelivered(events, atMs);
36478
+ }
35485
36479
  };
35486
36480
  function reKeyForForward(event, remote) {
35487
36481
  if (remote === null) {
@@ -35524,281 +36518,17 @@ function toolAuditEvent(input2) {
35524
36518
  }
35525
36519
 
35526
36520
  // ../../packages/plugin-runtime/src/attached/history-state.ts
35527
- import { readFileSync as readFileSync14 } from "fs";
35528
- import { join as join22 } from "path";
36521
+ import { readFileSync as readFileSync15 } from "fs";
36522
+ import { join as join23 } from "path";
35529
36523
 
35530
36524
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
35531
36525
  import { createHash as createHash6 } from "crypto";
35532
36526
  import { hostname as hostname5 } from "os";
35533
36527
 
35534
- // ../../packages/remote/src/http.ts
35535
- import { request as httpRequest } from "http";
35536
- import { request as httpsRequest } from "https";
35537
- var DEFAULT_TIMEOUT_MS = 1e4;
35538
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
35539
- var RemoteRequestError = class extends Error {
35540
- constructor(status) {
35541
- super(`control-plane request failed with status ${String(status)}`);
35542
- this.status = status;
35543
- this.name = "RemoteRequestError";
35544
- }
35545
- status;
35546
- };
35547
- var RemoteRequestInvalid = class extends Error {
35548
- constructor(route, cause) {
35549
- super(`refusing to send a malformed body to ${route}`);
35550
- this.cause = cause;
35551
- this.name = "RemoteRequestInvalid";
35552
- }
35553
- cause;
35554
- };
35555
- var RemoteResponseInvalid = class extends Error {
35556
- constructor(route, detail) {
35557
- super(`control plane answered ${route} with ${detail}`);
35558
- this.name = "RemoteResponseInvalid";
35559
- }
35560
- };
35561
- var RemoteTransportError = class extends Error {
35562
- /**
35563
- * The status the peer sent, when headers arrived and only the BODY was
35564
- * refused.
35565
- *
35566
- * Undefined for the ordinary case this class was written for — no answer at
35567
- * all. It exists because two paths reject after a status has already been
35568
- * delivered: an oversized body and an aborted response. Discarding it there
35569
- * reported a deployment answering 401 with a verbose body as a network
35570
- * outage, which sends the reader to look at their network instead of their
35571
- * credential.
35572
- */
35573
- constructor(reason, status) {
35574
- super(`control-plane request did not complete: ${reason}`);
35575
- this.status = status;
35576
- this.name = "RemoteTransportError";
35577
- }
35578
- status;
35579
- };
35580
- async function send(options) {
35581
- const url2 = new URL(options.url);
35582
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35583
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
35584
- const requestOptions = {
35585
- method: options.method,
35586
- headers: {
35587
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
35588
- // last they win, and two of the values below are ones no caller may
35589
- // replace: `x-api-key` is the credential, and `content-length` is the
35590
- // byte count that stops a multi-byte body being truncated by the
35591
- // receiver. `SendOptions.headers` is a free-form record on an exported
35592
- // function, so "no caller does that today" is not the guarantee to rely
35593
- // on. The one header any caller actually passes — `if-none-match` on the
35594
- // conditional GET — is untouched by this order.
35595
- ...options.headers,
35596
- // The credential. One header, matching what the deployment authenticates
35597
- // on; a second copy in an `Authorization` header would be one more place
35598
- // it can be logged by an intermediary for no gain.
35599
- //
35600
- // Spread conditionally rather than assigned as `undefined`: Node's header
35601
- // handling and `content-length` bookkeeping treat a present-but-undefined
35602
- // key differently from an absent one, and "the header is not there" is
35603
- // the property the attach flow needs.
35604
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
35605
- accept: "application/json",
35606
- ...options.body === void 0 ? {} : {
35607
- "content-type": "application/json",
35608
- // Byte length, not string length: a multi-byte body sent with a
35609
- // character count is truncated by the receiver.
35610
- "content-length": String(Buffer.byteLength(options.body))
35611
- }
35612
- }
35613
- };
35614
- return new Promise((resolve2, reject) => {
35615
- let settled = false;
35616
- const fail = (reason, status) => {
35617
- if (settled) return;
35618
- settled = true;
35619
- reject(new RemoteTransportError(reason, status));
35620
- };
35621
- const req = send_(url2, requestOptions, (res) => {
35622
- const chunks = [];
35623
- let size = 0;
35624
- res.on("data", (chunk) => {
35625
- size += chunk.length;
35626
- if (size > MAX_RESPONSE_BYTES) {
35627
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
35628
- res.destroy();
35629
- req.destroy();
35630
- return;
35631
- }
35632
- chunks.push(chunk);
35633
- });
35634
- res.on("aborted", () => {
35635
- fail("the response was aborted", res.statusCode);
35636
- });
35637
- res.on("end", () => {
35638
- if (settled) return;
35639
- settled = true;
35640
- resolve2({
35641
- status: res.statusCode ?? 0,
35642
- headers: res.headers,
35643
- body: Buffer.concat(chunks).toString("utf8")
35644
- });
35645
- });
35646
- });
35647
- const deadline = setTimeout(() => {
35648
- fail(`no response within ${String(timeoutMs)}ms`);
35649
- req.destroy();
35650
- }, timeoutMs);
35651
- deadline.unref();
35652
- req.on("upgrade", (_res, socket) => {
35653
- fail("the deployment answered with a protocol upgrade");
35654
- socket.destroy();
35655
- });
35656
- req.on("close", () => {
35657
- fail("the connection closed before a response was read");
35658
- clearTimeout(deadline);
35659
- });
35660
- req.on("error", (err) => {
35661
- fail(err.message);
35662
- });
35663
- if (options.body !== void 0) req.write(options.body);
35664
- req.end();
35665
- });
35666
- }
35667
-
35668
- // ../../packages/remote/src/client.ts
35669
- var ROUTES = {
35670
- events: "/v1/events",
35671
- auditEvents: "/v1/audit-events",
35672
- auditEventsBatch: "/v1/audit-events/batch",
35673
- inventory: "/v1/inventory",
35674
- storePosture: "/v1/store-posture",
35675
- policyBundle: "/v1/policy-bundle",
35676
- whoami: "/v1/plugin/whoami",
35677
- shares: "/v1/shares"
35678
- };
35679
- function headerValue(response, name) {
35680
- const raw = response.headers[name];
35681
- if (raw === void 0) return void 0;
35682
- return Array.isArray(raw) ? raw[0] : raw;
35683
- }
35684
- function okBody(response) {
35685
- if (response.status < 200 || response.status >= 300) {
35686
- throw new RemoteRequestError(response.status);
35687
- }
35688
- return response.body;
35689
- }
35690
- function parsed(schema, body, route) {
35691
- let json2;
35692
- try {
35693
- json2 = JSON.parse(body);
35694
- } catch {
35695
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
35696
- }
35697
- const result = schema.safeParse(json2);
35698
- if (!result.success) {
35699
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
35700
- }
35701
- return result.data;
35702
- }
35703
- function withoutTrailingSlashes(endpoint) {
35704
- let end = endpoint.length;
35705
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
35706
- return endpoint.slice(0, end);
35707
- }
35708
- var SLASH = "/".charCodeAt(0);
35709
- function createRemoteClient(options) {
35710
- const base = withoutTrailingSlashes(options.endpoint);
35711
- const url2 = (route) => `${base}${route}`;
35712
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
35713
- const sendOne = async (event) => {
35714
- const validated = RecordAuditEventRequest.safeParse(event);
35715
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
35716
- const response = await send({
35717
- ...common,
35718
- method: "POST",
35719
- url: url2(ROUTES.auditEvents),
35720
- body: JSON.stringify(validated.data)
35721
- });
35722
- okBody(response);
35723
- };
35724
- return {
35725
- async ingestEvents(batch) {
35726
- const response = await send({
35727
- ...common,
35728
- method: "POST",
35729
- url: url2(ROUTES.events),
35730
- body: JSON.stringify(batch)
35731
- });
35732
- return parsed(IngestAck, okBody(response), ROUTES.events);
35733
- },
35734
- async ingestInventory(context) {
35735
- const response = await send({
35736
- ...common,
35737
- method: "POST",
35738
- url: url2(ROUTES.inventory),
35739
- body: JSON.stringify(context)
35740
- });
35741
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
35742
- },
35743
- async recordAuditEvent(event) {
35744
- await sendOne(event);
35745
- },
35746
- async recordAuditEvents(events) {
35747
- const validated = RecordAuditEventBatch.safeParse({ events });
35748
- if (!validated.success) {
35749
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
35750
- }
35751
- const response = await send({
35752
- ...common,
35753
- method: "POST",
35754
- url: url2(ROUTES.auditEventsBatch),
35755
- body: JSON.stringify(validated.data)
35756
- });
35757
- if (response.status === 404) {
35758
- for (const event of validated.data.events) await sendOne(event);
35759
- return { accepted: validated.data.events.length };
35760
- }
35761
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
35762
- },
35763
- async reportStorePosture(snapshot) {
35764
- const response = await send({
35765
- ...common,
35766
- method: "POST",
35767
- url: url2(ROUTES.storePosture),
35768
- body: JSON.stringify(snapshot)
35769
- });
35770
- okBody(response);
35771
- },
35772
- async getPolicyBundle(etag) {
35773
- const response = await send({
35774
- ...common,
35775
- method: "GET",
35776
- url: url2(ROUTES.policyBundle),
35777
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
35778
- });
35779
- if (response.status === 304) {
35780
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
35781
- }
35782
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
35783
- return { changed: true, bundle, etag: headerValue(response, "etag") };
35784
- },
35785
- async whoami() {
35786
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
35787
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
35788
- },
35789
- async recordProjectEgress(request) {
35790
- const validated = EgressIngestRequest.safeParse(request);
35791
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
35792
- const response = await send({
35793
- ...common,
35794
- method: "POST",
35795
- url: url2(ROUTES.shares),
35796
- body: JSON.stringify(validated.data)
35797
- });
35798
- okBody(response);
35799
- }
35800
- };
35801
- }
36528
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
36529
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
36530
+ var TRACE_ID = EventMetadata.shape.traceId;
36531
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
35802
36532
 
35803
36533
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
35804
36534
  import { spawn } from "child_process";
@@ -35806,7 +36536,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
35806
36536
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
35807
36537
 
35808
36538
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
35809
- import { readFileSync as readFileSync15 } from "fs";
36539
+ import { readFileSync as readFileSync16 } from "fs";
35810
36540
  function createPluginBlock(build, policyStore) {
35811
36541
  return async () => {
35812
36542
  const cached2 = await policyStore.read();
@@ -35825,7 +36555,7 @@ function createPluginBlock(build, policyStore) {
35825
36555
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
35826
36556
  import { randomUUID as randomUUID16 } from "crypto";
35827
36557
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
35828
- import { join as join23 } from "path";
36558
+ import { join as join24 } from "path";
35829
36559
 
35830
36560
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
35831
36561
  import { rename as rename2 } from "fs/promises";
@@ -35849,7 +36579,7 @@ async function publishByRename(tmp, file2, move = rename2) {
35849
36579
 
35850
36580
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
35851
36581
  function createPolicyStore(dir = dataDir()) {
35852
- const file2 = join23(dir, "policy-cache.json");
36582
+ const file2 = join24(dir, "policy-cache.json");
35853
36583
  async function read() {
35854
36584
  try {
35855
36585
  const raw = await readFile2(file2, "utf8");
@@ -35858,22 +36588,32 @@ function createPolicyStore(dir = dataDir()) {
35858
36588
  const record2 = parsed2;
35859
36589
  const bundle = PolicyBundle.parse(record2.bundle);
35860
36590
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
35861
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
36591
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
36592
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
36593
+ const etag = replayable ? stored : void 0;
35862
36594
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
35863
36595
  } catch {
35864
36596
  return null;
35865
36597
  }
35866
36598
  }
35867
- async function write(bundle, etag) {
35868
- await ensureDataDir(dir);
35869
- const stored = {
35870
- bundle,
35871
- fetchedAtMs: Date.now(),
35872
- ...etag === void 0 ? {} : { etag }
35873
- };
36599
+ function knowsMoreThanThisBuild(shapeId) {
36600
+ if (typeof shapeId !== "string" || shapeId === "") return false;
36601
+ const theirs = new Set(shapeId.split(","));
36602
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
36603
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
36604
+ }
36605
+ async function priorRecord() {
36606
+ try {
36607
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
36608
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
36609
+ } catch {
36610
+ return null;
36611
+ }
36612
+ }
36613
+ async function publishRecord(record2) {
35874
36614
  const tmp = `${file2}.${randomUUID16()}.tmp`;
35875
36615
  try {
35876
- await writeFile2(tmp, JSON.stringify(stored), {
36616
+ await writeFile2(tmp, JSON.stringify(record2), {
35877
36617
  encoding: "utf8",
35878
36618
  mode: DATA_FILE_MODE,
35879
36619
  flag: "wx"
@@ -35884,6 +36624,27 @@ function createPolicyStore(dir = dataDir()) {
35884
36624
  throw err;
35885
36625
  }
35886
36626
  }
36627
+ async function write(bundle, etag) {
36628
+ await ensureDataDir(dir);
36629
+ const prior = await priorRecord();
36630
+ const priorVersion = prior?.bundle?.version;
36631
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
36632
+ await publishRecord({
36633
+ ...prior,
36634
+ fetchedAtMs: Date.now()
36635
+ });
36636
+ return;
36637
+ }
36638
+ await publishRecord({
36639
+ bundle,
36640
+ fetchedAtMs: Date.now(),
36641
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
36642
+ // bundle it already holds, and the point of the stamp is to describe the
36643
+ // build that last narrowed those bytes, which is this one.
36644
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
36645
+ ...etag === void 0 ? {} : { etag }
36646
+ });
36647
+ }
35887
36648
  return { read, write, file: file2 };
35888
36649
  }
35889
36650
 
@@ -36049,11 +36810,11 @@ function readStorePosture(dbPath2) {
36049
36810
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
36050
36811
  import { randomUUID as randomUUID17 } from "crypto";
36051
36812
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
36052
- import { join as join24 } from "path";
36813
+ import { join as join25 } from "path";
36053
36814
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
36054
36815
  function createPostureStore(dir = settingsDir(), legacyDir) {
36055
- const file2 = join24(dir, "posture-state.json");
36056
- const legacyFile = legacyDir === void 0 ? null : join24(legacyDir, "posture-state.json");
36816
+ const file2 = join25(dir, "posture-state.json");
36817
+ const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
36057
36818
  async function persist(state) {
36058
36819
  await ensureDataDir(dir);
36059
36820
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -36121,8 +36882,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
36121
36882
  }
36122
36883
 
36123
36884
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
36124
- import { readFileSync as readFileSync16 } from "fs";
36125
- import { join as join25 } from "path";
36885
+ import { readFileSync as readFileSync17 } from "fs";
36886
+ import { join as join26 } from "path";
36126
36887
 
36127
36888
  // ../../packages/plugin-runtime/src/attached/status.ts
36128
36889
  var REFUSAL_LINES = {
@@ -36440,9 +37201,21 @@ var StandaloneDataGateway = class {
36440
37201
  // for the whole of it, so a member that threw would make that answer a lie
36441
37202
  // the moment a composite delegated to it. A store-level no-op is the honest
36442
37203
  // shape — a standalone machine has nothing delivered to record.
37204
+ markCaptureOwed(event) {
37205
+ this.db.markCaptureOwed(event);
37206
+ }
36443
37207
  markCaptureDelivered(event, atMs) {
36444
37208
  this.db.markCaptureDelivered(event, atMs);
36445
37209
  }
37210
+ // Implemented, not stubbed, for the same reason its sibling above is: the
37211
+ // attached gateway is a DECORATOR over an instance of this class
37212
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
37213
+ // stamp the live forward makes lands here with a non-empty array. This is the
37214
+ // production write path for that feature, not a shape-satisfying no-op — a
37215
+ // machine that is merely standalone simply never calls it.
37216
+ markAuditEventsDelivered(events, atMs) {
37217
+ this.db.markAuditEventsDelivered(events, atMs);
37218
+ }
36446
37219
  staleBinaryNotice(currentVersion) {
36447
37220
  try {
36448
37221
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -36694,7 +37467,7 @@ function show(body) {
36694
37467
 
36695
37468
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
36696
37469
  import { writeFileSync as writeFileSync8 } from "fs";
36697
- import { join as join26 } from "path";
37470
+ import { join as join27 } from "path";
36698
37471
 
36699
37472
  // ../../packages/setup-wizard/src/triage/merge.ts
36700
37473
  var RANK = Object.fromEntries(
@@ -36702,9 +37475,9 @@ var RANK = Object.fromEntries(
36702
37475
  );
36703
37476
 
36704
37477
  // ../../packages/setup-wizard/src/triage/plan-file.ts
36705
- import { mkdtempSync, readFileSync as readFileSync17, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37478
+ import { mkdtempSync, readFileSync as readFileSync18, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
36706
37479
  import { tmpdir } from "os";
36707
- import { basename as basename6, dirname as dirname5, join as join27 } from "path";
37480
+ import { basename as basename6, dirname as dirname6, join as join28 } from "path";
36708
37481
  var SuppressionEntrySchema = external_exports.object({
36709
37482
  ruleId: external_exports.string(),
36710
37483
  category: DetectionCategory,