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

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