@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.
@@ -493,11 +493,12 @@ var require_ignore = __commonJS({
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
495
  import { existsSync as existsSync7 } from "fs";
496
- import { join as join12 } from "path";
496
+ import { join as join13 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/attached-derived.ts
499
499
  import { rmSync } from "fs";
500
500
  import { join } from "path";
501
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
501
502
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
502
503
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
503
504
 
@@ -598,6 +599,30 @@ var SQLITE_MIGRATIONS = [
598
599
  {
599
600
  tag: "0022_audit_inspection_ms",
600
601
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
602
+ },
603
+ {
604
+ tag: "0023_secret_vault_user_authorized",
605
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
606
+ },
607
+ {
608
+ tag: "0024_finding_resolution_key_created_index",
609
+ sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
610
+ },
611
+ {
612
+ tag: "0025_audit_capture_attribute_columns",
613
+ sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
614
+ },
615
+ {
616
+ tag: "0026_audit_llm_call_usage_columns",
617
+ sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
618
+ },
619
+ {
620
+ tag: "0027_audit_llm_usage_index",
621
+ sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
622
+ },
623
+ {
624
+ tag: "0028_activity_session_probe_indexes",
625
+ sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
601
626
  }
602
627
  ];
603
628
 
@@ -22137,6 +22162,26 @@ var AttachTokenResponse = external_exports.union([
22137
22162
  AttachTokenExpired,
22138
22163
  external_exports.object({ status: printable(64) })
22139
22164
  ]);
22165
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22166
+ var DeviceCommand = external_exports.object({
22167
+ id: printable(128).min(1),
22168
+ kind: DeviceCommandKind,
22169
+ issuedAt: printable(64).min(1),
22170
+ expiresAt: printable(64).min(1)
22171
+ }).strict();
22172
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22173
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22174
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22175
+ external_exports.object({
22176
+ outcome: external_exports.literal("reported"),
22177
+ projectsScanned: external_exports.number().int().nonnegative()
22178
+ }).strict(),
22179
+ external_exports.object({
22180
+ outcome: external_exports.literal("failed"),
22181
+ reason: DeviceCommandFailureReason,
22182
+ projectsScanned: external_exports.number().int().nonnegative()
22183
+ }).strict()
22184
+ ]);
22140
22185
 
22141
22186
  // ../../packages/schema/src/zod/registry.ts
22142
22187
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22303,7 +22348,7 @@ var PackManifest = external_exports.object({
22303
22348
  }).meta({ id: "PackManifest" });
22304
22349
 
22305
22350
  // ../../packages/schema/src/zod/detection.ts
22306
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22351
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22307
22352
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22308
22353
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22309
22354
  var DetectionCounts = external_exports.object({
@@ -22440,14 +22485,17 @@ function optional2(key, parsed2, raw) {
22440
22485
  function isStringArray(value) {
22441
22486
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22442
22487
  }
22488
+ var ORIGIN_VALUES = { library: true, custom: true };
22489
+ function resolveOrigin(origin) {
22490
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22491
+ }
22443
22492
  function summaryToDetectionListItem(s) {
22444
22493
  return {
22445
22494
  id: `${s.namespace}/${s.packId}`,
22446
22495
  name: s.name,
22447
22496
  version: s.version,
22448
22497
  enabled: s.enabled,
22449
- origin: "library",
22450
- // v1: every installed pack is library origin
22498
+ origin: resolveOrigin(s.origin),
22451
22499
  namespace: s.namespace,
22452
22500
  packId: s.packId,
22453
22501
  ruleCount: s.ruleCount,
@@ -22499,7 +22547,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22499
22547
  name: row.name,
22500
22548
  version: row.version,
22501
22549
  enabled: row.enabled,
22502
- origin: "library",
22550
+ origin: resolveOrigin(row.origin),
22503
22551
  namespace: row.namespace,
22504
22552
  packId: row.packId,
22505
22553
  ruleCount: row.rules.length,
@@ -22519,16 +22567,20 @@ function splitDetectionId(id) {
22519
22567
  }
22520
22568
  function buildDetectionsList(summaries, query) {
22521
22569
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22570
+ const originOf = (s) => resolveOrigin(s.origin);
22522
22571
  const counts = {
22523
22572
  all: summaries.length,
22524
- library: summaries.length,
22525
- // all origin=library in v1
22526
- custom: 0,
22573
+ library: summaries.filter((s) => originOf(s) === "library").length,
22574
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22575
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22576
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22577
+ // place, and that state does not exist — editing a library pack forks it. See
22578
+ // OriginEnum.
22527
22579
  customized: 0,
22528
22580
  updates: withUpdate.length
22529
22581
  };
22530
22582
  const filter = query.filter;
22531
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22583
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22532
22584
  if (query.q) {
22533
22585
  const q = query.q.toLowerCase();
22534
22586
  filtered = filtered.filter(
@@ -22608,8 +22660,9 @@ var Event = external_exports.object({
22608
22660
  metadata: EventMetadata.optional()
22609
22661
  }).meta({ id: "Event" });
22610
22662
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22663
+ var INGEST_BATCH_MAX = 100;
22611
22664
  var IngestBatch = external_exports.object({
22612
- events: external_exports.array(IngestEvent).min(1).max(100),
22665
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22613
22666
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22614
22667
  // additionally rejects any event whose contentHash the store has already
22615
22668
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23165,372 +23218,11 @@ var PatchInstalledPackRequest = external_exports.object({
23165
23218
  message: "At least one field must be provided"
23166
23219
  }).meta({ id: "PatchInstalledPackRequest" });
23167
23220
 
23168
- // ../../packages/schema/src/zod/vault.ts
23169
- var POINTER_FORMAT_VERSION = 2;
23170
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23171
- var POINTER_TOKEN_PATTERN = new RegExp(
23172
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23173
- );
23174
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23175
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23176
- var ParsedPointer = external_exports.object({
23177
- category: DetectionCategory,
23178
- keyVersion: external_exports.number().int().positive(),
23179
- pointerId: external_exports.string(),
23180
- tag: external_exports.string()
23181
- });
23182
- var VaultEntry = external_exports.object({
23183
- pointerId: external_exports.string(),
23184
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23185
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23186
- // independently of the vault encryption key below.
23187
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23188
- fingerprintKeyVersion: external_exports.number().int().positive(),
23189
- // The vault-key epoch this row's ciphertext was sealed under.
23190
- keyVersion: external_exports.number().int().positive(),
23191
- // Fixed at first mint and never updated: the same value detected later under a
23192
- // different rule's category keeps the category it was minted with, so one
23193
- // value always produces exactly one wire token.
23194
- category: DetectionCategory,
23195
- ruleId: external_exports.string(),
23196
- // Partial-reveal preview for badges and listings. Never the raw value.
23197
- maskedMatch: external_exports.string(),
23198
- provider: external_exports.string().optional(),
23199
- ciphertext: external_exports.string(),
23200
- nonce: external_exports.string(),
23201
- authTag: external_exports.string(),
23202
- // How many times this value has been detected on this machine — the reuse
23203
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23204
- occurrenceCount: external_exports.number().int().nonnegative(),
23205
- firstSeen: external_exports.string(),
23206
- lastSeen: external_exports.string()
23207
- });
23208
- var PointerDescriptor = external_exports.object({
23209
- category: DetectionCategory,
23210
- provider: external_exports.string().optional(),
23211
- maskedMatch: external_exports.string(),
23212
- occurrences: external_exports.number().int().nonnegative(),
23213
- firstSeen: external_exports.string(),
23214
- lastSeen: external_exports.string()
23215
- });
23216
- var PointerIdentity = external_exports.object({
23217
- ruleId: external_exports.string(),
23218
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23219
- fingerprintKeyVersion: external_exports.number().int().positive()
23220
- });
23221
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23222
- var VaultDerefReason = external_exports.enum([
23223
- "display",
23224
- "explicit-reveal",
23225
- "view-render",
23226
- "model-input",
23227
- "remediation",
23228
- "purge"
23229
- ]);
23230
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23231
- var VaultDeref = external_exports.object({
23232
- id: external_exports.guid(),
23233
- pointerId: external_exports.string(),
23234
- at: external_exports.string(),
23235
- target: DetokenizeTarget,
23236
- reason: VaultDerefReason,
23237
- outcome: VaultDerefOutcome,
23238
- // Present only on a model-target crossing that a reveal grant authorized.
23239
- grantId: external_exports.string().optional(),
23240
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23241
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23242
- pointerCount: external_exports.number().int().positive().default(1)
23243
- });
23244
- var VaultSightingKind = external_exports.enum([
23245
- "prompt",
23246
- "tool-input",
23247
- "tool-output",
23248
- "file",
23249
- "transcript"
23250
- ]);
23251
- var VaultSighting = external_exports.object({
23252
- location: external_exports.string(),
23253
- kind: VaultSightingKind,
23254
- firstSeen: external_exports.string(),
23255
- lastSeen: external_exports.string()
23256
- });
23257
- var VaultInventoryEntry = external_exports.object({
23258
- pointerId: external_exports.string(),
23259
- category: DetectionCategory,
23260
- provider: external_exports.string().optional(),
23261
- maskedMatch: external_exports.string(),
23262
- occurrences: external_exports.number().int().nonnegative(),
23263
- firstSeen: external_exports.string(),
23264
- lastSeen: external_exports.string(),
23265
- // The active reveal-to-model grant covering this value, when one exists —
23266
- // the inventory badges it, the row links to revocation.
23267
- revealGrantId: external_exports.string().nullable(),
23268
- sightings: external_exports.array(VaultSighting)
23269
- });
23270
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23271
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23272
- var MAX_VAULT_PAGE_LIMIT = 200;
23273
- var ListVaultInventoryQuery = external_exports.object({
23274
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23275
- // Opaque; names the last row of the page just served.
23276
- cursor: external_exports.string().optional()
23277
- });
23278
- var ListVaultInventoryResponse = external_exports.object({
23279
- // Vaulted values across the whole store, not just this page — cursor-
23280
- // independent, so paging never changes what the count claims.
23281
- totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23282
- items: external_exports.array(VaultInventoryEntry),
23283
- // `null` once the last page is reached.
23284
- nextCursor: external_exports.string().nullable()
23285
- });
23286
- var ListVaultReuseQuery = external_exports.object({
23287
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23288
- cursor: external_exports.string().optional()
23289
- });
23290
- var ListVaultReuseResponse = external_exports.object({
23291
- // Reused values across the whole store — the number the section's claim
23292
- // ("values detected in more than one place") is about.
23293
- totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23294
- items: external_exports.array(VaultInventoryEntry),
23295
- nextCursor: external_exports.string().nullable()
23296
- });
23297
- var ListVaultDerefsQuery = external_exports.object({
23298
- // Include the batched, high-volume reasons (display, view-render). Omitted
23299
- // hides them and counts them into `hiddenBatched` instead, so the model
23300
- // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23301
- // over a Server Action, which preserves the type, never as a URL param.
23302
- includeBatched: external_exports.boolean().optional(),
23303
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23304
- cursor: external_exports.string().optional()
23305
- });
23306
- var ListVaultDerefsResponse = external_exports.object({
23307
- items: external_exports.array(VaultDeref),
23308
- nextCursor: external_exports.string().nullable(),
23309
- // Display/view-render rows the query hid, over the WHOLE trail rather than
23310
- // this page — it is the count the "N hidden" line and its toggle speak for.
23311
- // Always 0 when `includeBatched` was set, since nothing was hidden.
23312
- hiddenBatched: external_exports.number().int().nonnegative()
23313
- });
23314
- var VaultKeyCustody = external_exports.string();
23315
- var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23316
- var VAULT_CONSENT_VERSION = 1;
23317
- var VaultConsent = external_exports.object({
23318
- acknowledgedAt: external_exports.iso.datetime(),
23319
- version: external_exports.number().int().positive()
23320
- });
23321
-
23322
- // ../../packages/schema/src/zod/local.ts
23323
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23324
- var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23325
- var RunMode = external_exports.enum(["standalone", "attached"]);
23326
- var ControlPlaneConnection = external_exports.object({
23327
- endpoint: external_exports.string().min(1),
23328
- // Display name for the deployment, shown instead of the raw endpoint.
23329
- label: external_exports.string().min(1).optional(),
23330
- attachedAt: external_exports.iso.datetime()
23331
- }).meta({ id: "ControlPlaneConnection" });
23332
- var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23333
- var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23334
- var ModelJudgeConsent = external_exports.object({
23335
- acknowledgedAt: external_exports.iso.datetime(),
23336
- payloadVersion: external_exports.number().int().positive()
23337
- });
23338
- var HistorySyncConsent = external_exports.object({
23339
- acknowledgedAt: external_exports.iso.datetime(),
23340
- payloadVersion: external_exports.number().int().positive(),
23341
- endpoint: external_exports.string()
23342
- });
23343
- var WorkspaceSettings = external_exports.object({
23344
- specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23345
- runMode: RunMode.default("standalone"),
23346
- // Present only while attached; a detach clears it. Its presence is what makes
23347
- // `runMode: 'attached'` mean anything — see isAttached.
23348
- controlPlane: ControlPlaneConnection.optional(),
23349
- policy: SimpleDetectionPolicy.default("redact"),
23350
- // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23351
- historicalAccess: HistoricalAccess.default("session-only"),
23352
- // In-place egress extraction on the scan paths; disable to stop all Data
23353
- // Shares writes.
23354
- dataSharesInPlace: external_exports.boolean().default(true),
23355
- // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23356
- // vault, instead of destroying them. Absent by default: this is a custody
23357
- // change from one-way redaction, so it is never an assumed grant on upgrade.
23358
- // Revoking stops future vaulting; it does not erase what is already stored —
23359
- // purging the vault is the eraser.
23360
- vaultConsent: VaultConsent.optional(),
23361
- // Where the vault master key lives.
23362
- vaultKeyCustody: VaultKeyCustody.default("file"),
23363
- // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23364
- vaultInlineReveal: VaultInlineReveal.default("masked"),
23365
- // Absent until /aka:setup completes; its presence is what "onboarded" means.
23366
- onboardedAt: external_exports.iso.datetime().optional(),
23367
- // Records that the user consented to sending findings to the model API for
23368
- // the /aka:setup judge, along with the payload-shape version they agreed to.
23369
- // Absent until granted; a stale payloadVersion means the consent no longer
23370
- // covers the current payload and must be re-granted.
23371
- modelJudgeConsent: ModelJudgeConsent.optional(),
23372
- // Records that the user consented to sending the activity already recorded on
23373
- // this machine to the deployment it is attached to, along with the payload
23374
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23375
- // a different endpoint or an older payload no longer counts.
23376
- historySyncConsent: HistorySyncConsent.optional()
23377
- });
23378
- function defaultWorkspaceSettings() {
23379
- return WorkspaceSettings.parse({});
23380
- }
23381
- function isAttached(settings) {
23382
- return settings.runMode === "attached" && settings.controlPlane !== void 0;
23383
- }
23384
- function toInventoryRow(input2, id, now) {
23385
- return {
23386
- id,
23387
- objectType: input2.objectType,
23388
- location: input2.location ?? null,
23389
- title: input2.title ?? null,
23390
- hostId: input2.hostId ?? null,
23391
- attributes: JSON.stringify(input2.attributes),
23392
- firstSeen: now,
23393
- lastSeen: now
23394
- };
23395
- }
23396
- function toSourceProjectRow(input2, id, now) {
23397
- return {
23398
- id,
23399
- url: input2.url,
23400
- name: input2.name ?? null,
23401
- attributes: JSON.stringify(input2.attributes),
23402
- firstSeen: now,
23403
- lastSeen: now
23404
- };
23405
- }
23406
- function toAuditEventRow(input2) {
23407
- return {
23408
- id: input2.id,
23409
- parentId: input2.parentId ?? null,
23410
- rootSessionId: input2.rootSessionId ?? null,
23411
- eventType: input2.eventType,
23412
- hostId: input2.hostId ?? null,
23413
- harnessId: input2.harnessId ?? null,
23414
- sourceProjectId: input2.sourceProjectId ?? null,
23415
- startedAt: isoToEpochMillis(input2.startedAt),
23416
- endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23417
- severity: input2.severity ?? null,
23418
- priority: input2.priority ?? null,
23419
- content: input2.content ?? null,
23420
- contentHash: input2.contentHash ?? null,
23421
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23422
- };
23423
- }
23424
- function toClassifiedDataRow(input2, id) {
23425
- return {
23426
- id,
23427
- class: input2.class,
23428
- label: input2.label ?? null,
23429
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23430
- };
23431
- }
23432
- function toInspectionDefinitionRow(input2, id) {
23433
- return {
23434
- id,
23435
- ruleId: input2.ruleId,
23436
- name: input2.name,
23437
- category: input2.category,
23438
- severity: input2.severity,
23439
- definition: input2.definition,
23440
- version: input2.version
23441
- };
23442
- }
23443
- function toInspectionFindingRow(input2) {
23444
- return {
23445
- id: input2.id,
23446
- auditEventId: input2.auditEventId,
23447
- inspectionDefinitionId: input2.inspectionDefinitionId,
23448
- classifiedDataId: input2.classifiedDataId ?? null,
23449
- spanStart: input2.span.start,
23450
- spanEnd: input2.span.end,
23451
- maskedMatch: input2.maskedMatch,
23452
- actionTaken: input2.actionTaken,
23453
- confidence: input2.confidence,
23454
- findingKey: input2.findingKey ?? null,
23455
- firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23456
- };
23457
- }
23458
- function toCaptureAttributes(event) {
23459
- const metadata = event.metadata;
23460
- return {
23461
- source_tool: event.sourceTool,
23462
- ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23463
- ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23464
- ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23465
- ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23466
- ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23467
- ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23468
- ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23469
- ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23470
- ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23471
- // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23472
- // has ever populated either), but every legacy metadata key still rides
23473
- // the bag rather than being silently dropped — CaptureAttributes'
23474
- // `.catchall(z.unknown())` carries the long tail.
23475
- ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23476
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23477
- };
23478
- }
23479
- function captureDefinitionVersion(finding) {
23480
- return `capture/${finding.category}/${finding.severity}`;
23481
- }
23482
- function toCaptureDefinitionInput(finding) {
23483
- return {
23484
- ruleId: finding.ruleId,
23485
- version: captureDefinitionVersion(finding),
23486
- name: finding.ruleId,
23487
- category: finding.category,
23488
- severity: finding.severity,
23489
- definition: JSON.stringify({ ruleId: finding.ruleId })
23490
- };
23491
- }
23492
-
23493
- // ../../packages/schema/src/zod/managed.ts
23494
- var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23495
- var MANAGED_SETTINGS_SPEC_VERSION = 1;
23496
- var ManagedSettingKey = external_exports.enum([
23497
- "runMode",
23498
- "historicalAccess",
23499
- "vaultConsent",
23500
- "vaultKeyCustody",
23501
- "vaultInlineReveal",
23502
- "modelJudgeConsent",
23503
- "dataSharesInPlace"
23504
- ]).meta({ id: "ManagedSettingKey" });
23505
- var ManagedSettingsValues = external_exports.object({
23506
- runMode: external_exports.enum(["standalone", "attached"]).optional(),
23507
- controlPlane: external_exports.object({
23508
- endpoint: external_exports.string().min(1),
23509
- label: external_exports.string().min(1).optional()
23510
- }).optional(),
23511
- historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23512
- vaultConsent: external_exports.boolean().optional(),
23513
- vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23514
- vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23515
- modelJudgeConsent: external_exports.boolean().optional(),
23516
- dataSharesInPlace: external_exports.boolean().optional()
23517
- }).meta({ id: "ManagedSettingsValues" });
23518
- var ManagedSettings = external_exports.object({
23519
- specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23520
- // Shown on every locked control, so the user can tell an administrative
23521
- // decision from a bug. Absent renders as a generic "your organization".
23522
- organization: external_exports.string().min(1).optional(),
23523
- // What the administrator pinned.
23524
- values: ManagedSettingsValues.default({}),
23525
- // Which of those the user may not change. A key here with no matching value
23526
- // freezes whatever the user last chose; a value with no lock is a DEFAULT
23527
- // the user may still override. The two are separable on purpose.
23528
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23529
- }).meta({ id: "ManagedSettings" });
23530
-
23531
23221
  // ../../packages/schema/src/zod/policy.ts
23532
23222
  var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23533
23223
  var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23224
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23225
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23534
23226
  var Policy = external_exports.object({
23535
23227
  id: external_exports.guid(),
23536
23228
  scope: PolicyScope,
@@ -23540,7 +23232,27 @@ var Policy = external_exports.object({
23540
23232
  customKeywords: external_exports.array(external_exports.string()).optional(),
23541
23233
  // Display name — optional so older policy rows without name still parse.
23542
23234
  // Added for the findings API (policy.name column migration).
23543
- name: external_exports.string().optional()
23235
+ name: external_exports.string().optional(),
23236
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23237
+ // which row this is. A producer that collapses several rows onto one target
23238
+ // must carry the marker onto whichever row survives, or the collapse decides
23239
+ // the answer; a survivor may therefore be a built-in expansion still marked
23240
+ // 'authored' because an authored sibling targeted the same thing.
23241
+ // Optional so an older producer — and an older on-disk cache — still parses;
23242
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23243
+ //
23244
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23245
+ // built-in archetype catalog entry a policy is, which every catalog surface
23246
+ // reads and which a caller may state. This one is a statement the PRODUCER
23247
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23248
+ // — the CRUD routes neither accept nor set it.
23249
+ //
23250
+ // A device consumes this in exactly one direction: an 'authored' policy
23251
+ // arriving from a control plane marks the rules it targets as not
23252
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23253
+ // which is what makes it safe to honour from an unsigned cache — the same
23254
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23255
+ provenance: PolicyProvenance.optional()
23544
23256
  }).meta({ id: "Policy" });
23545
23257
  var PolicyBundle = external_exports.object({
23546
23258
  version: external_exports.string(),
@@ -23592,6 +23304,12 @@ var PolicyBundle = external_exports.object({
23592
23304
  customKeywords: external_exports.array(external_exports.string()),
23593
23305
  fetchedAt: external_exports.iso.datetime()
23594
23306
  }).meta({ id: "PolicyBundle" });
23307
+ var POLICY_BUNDLE_SHAPE_ID = [
23308
+ ...Object.keys(PolicyBundle.shape),
23309
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23310
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23311
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23312
+ ].sort().join(",");
23595
23313
  var OBSERVE_ONLY_CATEGORIES = ["config"];
23596
23314
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23597
23315
  var CATEGORY_PEAK_SEVERITY = {
@@ -23612,9 +23330,11 @@ function severityFloorPolicy(category) {
23612
23330
  const peak = CATEGORY_PEAK_SEVERITY[category];
23613
23331
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23614
23332
  }
23615
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23616
23333
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23617
23334
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23335
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23336
+ id: "RedactFallback"
23337
+ });
23618
23338
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23619
23339
  var BUILTIN_POLICY_SPECS = {
23620
23340
  monitor: {
@@ -23651,6 +23371,42 @@ var BUILTIN_POLICY_SPECS = {
23651
23371
  function builtinPolicyToAction(id) {
23652
23372
  return BUILTIN_POLICY_SPECS[id].action;
23653
23373
  }
23374
+ var PALETTE_WEAKEST_FIRST = [
23375
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23376
+ ];
23377
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23378
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23379
+ );
23380
+ var ACTION_STRENGTH_ORDER = [
23381
+ ...BELOW_PALETTE,
23382
+ ...PALETTE_WEAKEST_FIRST
23383
+ ];
23384
+ function actionRank(action) {
23385
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23386
+ }
23387
+ function isActionAtLeast(action, floor) {
23388
+ return actionRank(action) >= actionRank(floor);
23389
+ }
23390
+ function strongerAction(a, b) {
23391
+ return actionRank(a) >= actionRank(b) ? a : b;
23392
+ }
23393
+ function weakestBuiltinAtLeast(floor) {
23394
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23395
+ }
23396
+ var PackPolicyFloor = external_exports.object({
23397
+ /**
23398
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23399
+ * rather than a raw ActionTaken because that is the vocabulary the user
23400
+ * picks from — a floor a UI cannot name is one it cannot explain.
23401
+ */
23402
+ floor: BuiltinPolicyId,
23403
+ /**
23404
+ * True when the organization AUTHORED a policy governing this pack rather
23405
+ * than stating a minimum: it gave the answer, so the pack is not
23406
+ * re-assignable locally in either direction.
23407
+ */
23408
+ locked: external_exports.boolean()
23409
+ }).describe("PackPolicyFloor");
23654
23410
  var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23655
23411
  (id) => !BUILTIN_POLICY_SPECS[id].reversible
23656
23412
  );
@@ -23708,6 +23464,394 @@ var PolicyStatsResponse = external_exports.object({
23708
23464
  detectionsGoverned: external_exports.number().int().nonnegative()
23709
23465
  }).meta({ id: "PolicyStatsResponse" });
23710
23466
 
23467
+ // ../../packages/schema/src/zod/vault.ts
23468
+ var POINTER_FORMAT_VERSION = 2;
23469
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23470
+ var POINTER_TOKEN_PATTERN = new RegExp(
23471
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23472
+ );
23473
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23474
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23475
+ var ParsedPointer = external_exports.object({
23476
+ category: DetectionCategory,
23477
+ keyVersion: external_exports.number().int().positive(),
23478
+ pointerId: external_exports.string(),
23479
+ tag: external_exports.string()
23480
+ });
23481
+ var VaultEntry = external_exports.object({
23482
+ pointerId: external_exports.string(),
23483
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23484
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23485
+ // independently of the vault encryption key below.
23486
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23487
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23488
+ // The vault-key epoch this row's ciphertext was sealed under.
23489
+ keyVersion: external_exports.number().int().positive(),
23490
+ // Fixed at first mint and never updated: the same value detected later under a
23491
+ // different rule's category keeps the category it was minted with, so one
23492
+ // value always produces exactly one wire token.
23493
+ category: DetectionCategory,
23494
+ ruleId: external_exports.string(),
23495
+ // Partial-reveal preview for badges and listings. Never the raw value.
23496
+ maskedMatch: external_exports.string(),
23497
+ provider: external_exports.string().optional(),
23498
+ ciphertext: external_exports.string(),
23499
+ nonce: external_exports.string(),
23500
+ authTag: external_exports.string(),
23501
+ // How many times this value has been detected on this machine — the reuse
23502
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23503
+ occurrenceCount: external_exports.number().int().nonnegative(),
23504
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23505
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23506
+ // one row however many paths vault it, so this is what tells a policy sweep
23507
+ // that the row carries somebody's own instruction and not just an assignment
23508
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23509
+ // vaulting of the same value must never clear it — what the user said about
23510
+ // the value does not expire.
23511
+ userAuthorized: external_exports.boolean(),
23512
+ firstSeen: external_exports.string(),
23513
+ lastSeen: external_exports.string()
23514
+ });
23515
+ var PointerDescriptor = external_exports.object({
23516
+ category: DetectionCategory,
23517
+ provider: external_exports.string().optional(),
23518
+ maskedMatch: external_exports.string(),
23519
+ occurrences: external_exports.number().int().nonnegative(),
23520
+ firstSeen: external_exports.string(),
23521
+ lastSeen: external_exports.string()
23522
+ });
23523
+ var PointerIdentity = external_exports.object({
23524
+ ruleId: external_exports.string(),
23525
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23526
+ fingerprintKeyVersion: external_exports.number().int().positive()
23527
+ });
23528
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23529
+ var VaultDerefReason = external_exports.enum([
23530
+ "display",
23531
+ "explicit-reveal",
23532
+ "view-render",
23533
+ "model-input",
23534
+ "remediation",
23535
+ "purge"
23536
+ ]);
23537
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23538
+ var VaultDeref = external_exports.object({
23539
+ id: external_exports.guid(),
23540
+ pointerId: external_exports.string(),
23541
+ at: external_exports.string(),
23542
+ target: DetokenizeTarget,
23543
+ reason: VaultDerefReason,
23544
+ outcome: VaultDerefOutcome,
23545
+ // Present only on a model-target crossing that a reveal grant authorized.
23546
+ grantId: external_exports.string().optional(),
23547
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23548
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23549
+ pointerCount: external_exports.number().int().positive().default(1)
23550
+ });
23551
+ var VaultSightingKind = external_exports.enum([
23552
+ "prompt",
23553
+ "tool-input",
23554
+ "tool-output",
23555
+ "file",
23556
+ "transcript"
23557
+ ]);
23558
+ var VaultSighting = external_exports.object({
23559
+ location: external_exports.string(),
23560
+ kind: VaultSightingKind,
23561
+ firstSeen: external_exports.string(),
23562
+ lastSeen: external_exports.string()
23563
+ });
23564
+ var VaultInventoryEntry = external_exports.object({
23565
+ pointerId: external_exports.string(),
23566
+ category: DetectionCategory,
23567
+ provider: external_exports.string().optional(),
23568
+ maskedMatch: external_exports.string(),
23569
+ occurrences: external_exports.number().int().nonnegative(),
23570
+ firstSeen: external_exports.string(),
23571
+ lastSeen: external_exports.string(),
23572
+ // The active reveal-to-model grant covering this value, when one exists —
23573
+ // the inventory badges it, the row links to revocation.
23574
+ revealGrantId: external_exports.string().nullable(),
23575
+ sightings: external_exports.array(VaultSighting)
23576
+ });
23577
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23578
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23579
+ var MAX_VAULT_PAGE_LIMIT = 200;
23580
+ var ListVaultInventoryQuery = external_exports.object({
23581
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23582
+ // Opaque; names the last row of the page just served.
23583
+ cursor: external_exports.string().optional()
23584
+ });
23585
+ var ListVaultInventoryResponse = external_exports.object({
23586
+ // Vaulted values across the whole store, not just this page — cursor-
23587
+ // independent, so paging never changes what the count claims.
23588
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23589
+ items: external_exports.array(VaultInventoryEntry),
23590
+ // `null` once the last page is reached.
23591
+ nextCursor: external_exports.string().nullable()
23592
+ });
23593
+ var ListVaultReuseQuery = external_exports.object({
23594
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23595
+ cursor: external_exports.string().optional()
23596
+ });
23597
+ var ListVaultReuseResponse = external_exports.object({
23598
+ // Reused values across the whole store — the number the section's claim
23599
+ // ("values detected in more than one place") is about.
23600
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23601
+ items: external_exports.array(VaultInventoryEntry),
23602
+ nextCursor: external_exports.string().nullable()
23603
+ });
23604
+ var ListVaultDerefsQuery = external_exports.object({
23605
+ // Include the batched, high-volume reasons (display, view-render). Omitted
23606
+ // hides them and counts them into `hiddenBatched` instead, so the model
23607
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23608
+ // over a Server Action, which preserves the type, never as a URL param.
23609
+ includeBatched: external_exports.boolean().optional(),
23610
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23611
+ cursor: external_exports.string().optional()
23612
+ });
23613
+ var ListVaultDerefsResponse = external_exports.object({
23614
+ items: external_exports.array(VaultDeref),
23615
+ nextCursor: external_exports.string().nullable(),
23616
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
23617
+ // this page — it is the count the "N hidden" line and its toggle speak for.
23618
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
23619
+ hiddenBatched: external_exports.number().int().nonnegative()
23620
+ });
23621
+ var VaultKeyCustody = external_exports.string();
23622
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23623
+ var VAULT_CONSENT_VERSION = 1;
23624
+ var VaultConsent = external_exports.object({
23625
+ acknowledgedAt: external_exports.iso.datetime(),
23626
+ version: external_exports.number().int().positive()
23627
+ });
23628
+
23629
+ // ../../packages/schema/src/zod/local.ts
23630
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23631
+ var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23632
+ var RunMode = external_exports.enum(["standalone", "attached"]);
23633
+ var ControlPlaneConnection = external_exports.object({
23634
+ endpoint: external_exports.string().min(1),
23635
+ // Display name for the deployment, shown instead of the raw endpoint.
23636
+ label: external_exports.string().min(1).optional(),
23637
+ attachedAt: external_exports.iso.datetime()
23638
+ }).meta({ id: "ControlPlaneConnection" });
23639
+ var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23640
+ var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23641
+ var ModelJudgeConsent = external_exports.object({
23642
+ acknowledgedAt: external_exports.iso.datetime(),
23643
+ payloadVersion: external_exports.number().int().positive()
23644
+ });
23645
+ var HistorySyncConsent = external_exports.object({
23646
+ acknowledgedAt: external_exports.iso.datetime(),
23647
+ payloadVersion: external_exports.number().int().positive(),
23648
+ endpoint: external_exports.string()
23649
+ });
23650
+ var WorkspaceSettings = external_exports.object({
23651
+ specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23652
+ runMode: RunMode.default("standalone"),
23653
+ // Present only while attached; a detach clears it. Its presence is what makes
23654
+ // `runMode: 'attached'` mean anything — see isAttached.
23655
+ controlPlane: ControlPlaneConnection.optional(),
23656
+ policy: SimpleDetectionPolicy.default("redact"),
23657
+ // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23658
+ historicalAccess: HistoricalAccess.default("session-only"),
23659
+ // In-place egress extraction on the scan paths; disable to stop all Data
23660
+ // Shares writes.
23661
+ dataSharesInPlace: external_exports.boolean().default(true),
23662
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23663
+ // vault, instead of destroying them. Absent by default: this is a custody
23664
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
23665
+ // Revoking stops future vaulting; it does not erase what is already stored —
23666
+ // purging the vault is the eraser.
23667
+ vaultConsent: VaultConsent.optional(),
23668
+ // Where the vault master key lives.
23669
+ vaultKeyCustody: VaultKeyCustody.default("file"),
23670
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23671
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
23672
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23673
+ // place. Not a handling policy: the policy has already resolved to redact,
23674
+ // and this only says what happens when the host offers no channel to carry it
23675
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23676
+ // Claude Code decline to mask a field that EXECUTES because masking would
23677
+ // change what runs. Per FIELD rather than per host, so a host that can
23678
+ // rewrite some inputs keeps true redaction on those.
23679
+ //
23680
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23681
+ // an attached machine's merge is `strongerAction` over the one action ladder
23682
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23683
+ // word and stays out of the stored value.
23684
+ redactFallback: RedactFallback.default("warn"),
23685
+ // Absent until /aka:setup completes; its presence is what "onboarded" means.
23686
+ onboardedAt: external_exports.iso.datetime().optional(),
23687
+ // Records that the user consented to sending findings to the model API for
23688
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
23689
+ // Absent until granted; a stale payloadVersion means the consent no longer
23690
+ // covers the current payload and must be re-granted.
23691
+ modelJudgeConsent: ModelJudgeConsent.optional(),
23692
+ // Records that the user consented to the DEFERRED send — the outbox — along
23693
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23694
+ // that covers both the pre-attach backlog and undelivered captures (which
23695
+ // carry prompt/reply text in `content`); the key name predates the widening.
23696
+ // Absent until granted, and a grant for a different endpoint or an older
23697
+ // payload no longer counts.
23698
+ historySyncConsent: HistorySyncConsent.optional()
23699
+ });
23700
+ function defaultWorkspaceSettings() {
23701
+ return WorkspaceSettings.parse({});
23702
+ }
23703
+ function isAttached(settings) {
23704
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23705
+ }
23706
+ function toInventoryRow(input2, id, now) {
23707
+ return {
23708
+ id,
23709
+ objectType: input2.objectType,
23710
+ location: input2.location ?? null,
23711
+ title: input2.title ?? null,
23712
+ hostId: input2.hostId ?? null,
23713
+ attributes: JSON.stringify(input2.attributes),
23714
+ firstSeen: now,
23715
+ lastSeen: now
23716
+ };
23717
+ }
23718
+ function toSourceProjectRow(input2, id, now) {
23719
+ return {
23720
+ id,
23721
+ url: input2.url,
23722
+ name: input2.name ?? null,
23723
+ attributes: JSON.stringify(input2.attributes),
23724
+ firstSeen: now,
23725
+ lastSeen: now
23726
+ };
23727
+ }
23728
+ function toAuditEventRow(input2) {
23729
+ return {
23730
+ id: input2.id,
23731
+ parentId: input2.parentId ?? null,
23732
+ rootSessionId: input2.rootSessionId ?? null,
23733
+ eventType: input2.eventType,
23734
+ hostId: input2.hostId ?? null,
23735
+ harnessId: input2.harnessId ?? null,
23736
+ sourceProjectId: input2.sourceProjectId ?? null,
23737
+ startedAt: isoToEpochMillis(input2.startedAt),
23738
+ endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23739
+ severity: input2.severity ?? null,
23740
+ priority: input2.priority ?? null,
23741
+ content: input2.content ?? null,
23742
+ contentHash: input2.contentHash ?? null,
23743
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23744
+ };
23745
+ }
23746
+ function toClassifiedDataRow(input2, id) {
23747
+ return {
23748
+ id,
23749
+ class: input2.class,
23750
+ label: input2.label ?? null,
23751
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23752
+ };
23753
+ }
23754
+ function toInspectionDefinitionRow(input2, id) {
23755
+ return {
23756
+ id,
23757
+ ruleId: input2.ruleId,
23758
+ name: input2.name,
23759
+ category: input2.category,
23760
+ severity: input2.severity,
23761
+ definition: input2.definition,
23762
+ version: input2.version
23763
+ };
23764
+ }
23765
+ function toInspectionFindingRow(input2) {
23766
+ return {
23767
+ id: input2.id,
23768
+ auditEventId: input2.auditEventId,
23769
+ inspectionDefinitionId: input2.inspectionDefinitionId,
23770
+ classifiedDataId: input2.classifiedDataId ?? null,
23771
+ spanStart: input2.span.start,
23772
+ spanEnd: input2.span.end,
23773
+ maskedMatch: input2.maskedMatch,
23774
+ actionTaken: input2.actionTaken,
23775
+ confidence: input2.confidence,
23776
+ findingKey: input2.findingKey ?? null,
23777
+ firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23778
+ };
23779
+ }
23780
+ function toCaptureAttributes(event) {
23781
+ const metadata = event.metadata;
23782
+ return {
23783
+ source_tool: event.sourceTool,
23784
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23785
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23786
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23787
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23788
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23789
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23790
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23791
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23792
+ ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23793
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23794
+ // has ever populated either), but every legacy metadata key still rides
23795
+ // the bag rather than being silently dropped — CaptureAttributes'
23796
+ // `.catchall(z.unknown())` carries the long tail.
23797
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23798
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23799
+ };
23800
+ }
23801
+ function captureDefinitionVersion(finding) {
23802
+ return `capture/${finding.category}/${finding.severity}`;
23803
+ }
23804
+ function toCaptureDefinitionInput(finding) {
23805
+ return {
23806
+ ruleId: finding.ruleId,
23807
+ version: captureDefinitionVersion(finding),
23808
+ name: finding.ruleId,
23809
+ category: finding.category,
23810
+ severity: finding.severity,
23811
+ definition: JSON.stringify({ ruleId: finding.ruleId })
23812
+ };
23813
+ }
23814
+
23815
+ // ../../packages/schema/src/zod/managed.ts
23816
+ var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23817
+ var MANAGED_SETTINGS_SPEC_VERSION = 1;
23818
+ var ManagedSettingKey = external_exports.enum([
23819
+ "runMode",
23820
+ "historicalAccess",
23821
+ "vaultConsent",
23822
+ "vaultKeyCustody",
23823
+ "vaultInlineReveal",
23824
+ "modelJudgeConsent",
23825
+ "dataSharesInPlace",
23826
+ "redactFallback"
23827
+ ]).meta({ id: "ManagedSettingKey" });
23828
+ var ManagedSettingsValues = external_exports.object({
23829
+ runMode: external_exports.enum(["standalone", "attached"]).optional(),
23830
+ controlPlane: external_exports.object({
23831
+ endpoint: external_exports.string().min(1),
23832
+ label: external_exports.string().min(1).optional()
23833
+ }).optional(),
23834
+ historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23835
+ vaultConsent: external_exports.boolean().optional(),
23836
+ vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23837
+ vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23838
+ modelJudgeConsent: external_exports.boolean().optional(),
23839
+ dataSharesInPlace: external_exports.boolean().optional(),
23840
+ redactFallback: RedactFallback.optional()
23841
+ }).meta({ id: "ManagedSettingsValues" });
23842
+ var ManagedSettings = external_exports.object({
23843
+ specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23844
+ // Shown on every locked control, so the user can tell an administrative
23845
+ // decision from a bug. Absent renders as a generic "your organization".
23846
+ organization: external_exports.string().min(1).optional(),
23847
+ // What the administrator pinned.
23848
+ values: ManagedSettingsValues.default({}),
23849
+ // Which of those the user may not change. A key here with no matching value
23850
+ // freezes whatever the user last chose; a value with no lock is a DEFAULT
23851
+ // the user may still override. The two are separable on purpose.
23852
+ lockedFields: external_exports.array(ManagedSettingKey).default([])
23853
+ }).meta({ id: "ManagedSettings" });
23854
+
23711
23855
  // ../../packages/schema/src/zod/project-files.ts
23712
23856
  var ProjectFileInput = external_exports.object({
23713
23857
  path: external_exports.string().min(1),
@@ -23953,10 +24097,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23953
24097
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23954
24098
 
23955
24099
  // ../../packages/schema/src/zod/settings-action.ts
24100
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24101
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23956
24102
  var SaveSettingsInput = external_exports.object({
23957
24103
  historicalAccess: external_exports.string(),
23958
- modelJudgeConsent: external_exports.boolean(),
23959
- historySyncConsent: external_exports.boolean(),
24104
+ modelJudgeConsent: ModelJudgeConsentChoice,
24105
+ historySyncConsent: HistorySyncConsentChoice,
23960
24106
  vaultConsent: external_exports.string(),
23961
24107
  vaultInlineReveal: external_exports.string()
23962
24108
  });
@@ -24106,9 +24252,9 @@ function deriveReviewReasons(trust, transports) {
24106
24252
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24107
24253
  return reasons;
24108
24254
  }
24109
- function buildReviewInfo(trust, transports) {
24255
+ function buildReviewInfo(trust, transports, decided) {
24110
24256
  const reasons = deriveReviewReasons(trust, transports);
24111
- return { needsReview: reasons.length > 0, reasons };
24257
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24112
24258
  }
24113
24259
  function distinctTransports(transports) {
24114
24260
  return Array.from(new Set(transports));
@@ -24306,8 +24452,8 @@ function readControlPlaneCredential(settingsDir2, connection) {
24306
24452
  }
24307
24453
 
24308
24454
  // ../../packages/persistence/src/database.ts
24309
- import { randomUUID as randomUUID10 } from "crypto";
24310
- import { join as join4, sep } from "path";
24455
+ import { randomUUID as randomUUID11 } from "crypto";
24456
+ import { dirname as dirname2, join as join7, sep } from "path";
24311
24457
  import { DatabaseSync } from "node:sqlite";
24312
24458
 
24313
24459
  // ../../packages/persistence/src/ids.ts
@@ -24562,6 +24708,10 @@ function allRows(stmt, params) {
24562
24708
  if (Array.isArray(params)) return stmt.all(...params);
24563
24709
  return stmt.all(params);
24564
24710
  }
24711
+ function* iterateRows(stmt, params) {
24712
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24713
+ for (const row of rows) yield row;
24714
+ }
24565
24715
  function getRow(stmt, params) {
24566
24716
  if (params === void 0) return stmt.get();
24567
24717
  if (Array.isArray(params)) return stmt.get(...params);
@@ -25030,10 +25180,17 @@ function ensureSyncedAtColumn(db, table) {
25030
25180
  if (!columns.includes("sync_claimed_at")) {
25031
25181
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
25032
25182
  }
25183
+ if (!columns.includes("outbox_owed")) {
25184
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25185
+ }
25033
25186
  db.exec(
25034
25187
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25035
25188
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
25036
25189
  );
25190
+ db.exec(
25191
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25192
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25193
+ );
25037
25194
  db.exec(
25038
25195
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
25039
25196
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25138,7 +25295,6 @@ function decodeKeysetCursor(cursor) {
25138
25295
  // ../../packages/persistence/src/repositories/activity.ts
25139
25296
  var DAY_MS = 864e5;
25140
25297
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25141
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25142
25298
  function defaultTimeZone() {
25143
25299
  try {
25144
25300
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25193,6 +25349,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25193
25349
  error: "error",
25194
25350
  active: "active"
25195
25351
  };
25352
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25196
25353
  function safeParseStringArray(raw) {
25197
25354
  if (!raw) return [];
25198
25355
  const parsed2 = safeJson(raw, null);
@@ -25266,6 +25423,37 @@ var TIMELINE_COLUMNS = `
25266
25423
  json_extract(attributes, '$.targetId') AS target_id,
25267
25424
  json_extract(attributes, '$.internal') AS internal,
25268
25425
  json_extract(attributes, '$.flagged') AS flagged`;
25426
+ var LLM_USAGE_SELECT = `
25427
+ SELECT root_session_id AS sessionId,
25428
+ provider,
25429
+ model,
25430
+ service_tier AS serviceTier,
25431
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25432
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25433
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25434
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25435
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25436
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25437
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25438
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25439
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25440
+ function usageLeaves(rows) {
25441
+ return rows.map((row) => {
25442
+ const attributes = {
25443
+ input_tokens: row.inputTokens,
25444
+ output_tokens: row.outputTokens,
25445
+ cache_creation_input_tokens: row.cacheCreationTokens,
25446
+ cache_read_input_tokens: row.cacheReadTokens,
25447
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25448
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25449
+ web_search_requests: row.webSearchRequests
25450
+ };
25451
+ if (row.provider !== null) attributes.provider = row.provider;
25452
+ if (row.model !== null) attributes.model = row.model;
25453
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25454
+ return { sessionId: row.sessionId, attributes };
25455
+ });
25456
+ }
25269
25457
  var SESSION_ROOT = `event_type = 'session'`;
25270
25458
  var HAS_ACTIVITY = `EXISTS (
25271
25459
  SELECT 1 FROM audit_events c
@@ -25291,16 +25479,17 @@ var SqliteActivityRepository = class {
25291
25479
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25292
25480
  const liveNow = countScalar(
25293
25481
  this.db,
25294
- `SELECT count(*) AS n FROM audit_events s
25482
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25295
25483
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25296
- AND max(
25297
- s.started_at,
25298
- coalesce(
25299
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25300
- s.started_at
25301
- )
25302
- ) >= ?`,
25303
- [liveThreshold]
25484
+ AND s.id IN (
25485
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25486
+ UNION
25487
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25488
+ WHERE started_at >= ?
25489
+ UNION
25490
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25491
+ WHERE ended_at >= ?)`,
25492
+ [liveThreshold, liveThreshold, liveThreshold]
25304
25493
  );
25305
25494
  const toolCallsToday = countScalar(
25306
25495
  this.db,
@@ -25430,7 +25619,7 @@ var SqliteActivityRepository = class {
25430
25619
  this.db.prepare(
25431
25620
  `SELECT ${TIMELINE_COLUMNS}
25432
25621
  FROM audit_events
25433
- WHERE id = ? OR root_session_id = ?
25622
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25434
25623
  ORDER BY started_at ASC, id ASC`
25435
25624
  ),
25436
25625
  [sessionId, sessionId]
@@ -25443,14 +25632,14 @@ var SqliteActivityRepository = class {
25443
25632
  coalesce(sum(output_tokens), 0) AS output,
25444
25633
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25445
25634
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25446
- FROM audit_events
25635
+ FROM audit_events INDEXED BY idx_audit_session_type
25447
25636
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25448
25637
  ),
25449
25638
  [sessionId]
25450
25639
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25451
25640
  const primaryModel = getRow(
25452
25641
  this.db.prepare(
25453
- `SELECT model, provider FROM audit_events
25642
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25454
25643
  WHERE root_session_id = ? AND event_type = 'llm_call'
25455
25644
  ORDER BY started_at ASC, id ASC
25456
25645
  LIMIT 1`
@@ -25461,7 +25650,7 @@ var SqliteActivityRepository = class {
25461
25650
  this.db.prepare(
25462
25651
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25463
25652
  count(*) AS n
25464
- FROM audit_events
25653
+ FROM audit_events INDEXED BY idx_audit_session
25465
25654
  WHERE root_session_id = ? AND event_type = 'tool_call'
25466
25655
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25467
25656
  ),
@@ -25469,7 +25658,7 @@ var SqliteActivityRepository = class {
25469
25658
  );
25470
25659
  const modelRows = allRows(
25471
25660
  this.db.prepare(
25472
- `SELECT DISTINCT model FROM audit_events
25661
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25473
25662
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25474
25663
  ORDER BY model`
25475
25664
  ),
@@ -25478,7 +25667,7 @@ var SqliteActivityRepository = class {
25478
25667
  const derivedModels = modelRows.map((r) => r.model);
25479
25668
  const commits = countScalar(
25480
25669
  this.db,
25481
- `SELECT count(*) AS n FROM audit_events
25670
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25482
25671
  WHERE root_session_id = ? AND event_type = 'commit'`,
25483
25672
  [sessionId]
25484
25673
  );
@@ -25514,25 +25703,57 @@ var SqliteActivityRepository = class {
25514
25703
  return Promise.resolve(session);
25515
25704
  }
25516
25705
  /**
25517
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25518
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25519
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25520
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25521
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25522
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25706
+ * Cross-session token report — every `llm_call` in the store (or in a
25707
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25708
+ * session, with USD cost DERIVED at read time via the shared
25709
+ * `defaultCostModel` (never stored). The caller collapses these onto
25710
+ * per-model rows with `aggregateTokenUsage`.
25711
+ *
25712
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25713
+ * the members the rollup sums — and priced once per group, which is exact
25714
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25715
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25716
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25717
+ * the index stores the values once, at write, and answers the same window in
25718
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25719
+ * planner prefers the general event-type index and fetches every row to
25720
+ * recompute the columns it could have read. The index is one every open
25721
+ * store carries, since opening runs the migrations, so the hard requirement
25722
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25723
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25724
+ * per call, no bag parsed.
25523
25725
  */
25524
25726
  tokenReports(fromMs) {
25525
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25526
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25727
+ const rows = allRows(
25728
+ this.db.prepare(
25729
+ `${LLM_USAGE_SELECT}
25730
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25731
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25732
+ ${LLM_USAGE_GROUP}`
25733
+ ),
25734
+ fromMs === void 0 ? void 0 : [fromMs]
25735
+ );
25736
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25527
25737
  }
25528
25738
  /**
25529
- * One session's token report — its `llm_call` leaves grouped per (provider,
25530
- * model) with derived cost, or `null` when the session made no `llm_call`s
25531
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25532
- * breakdown + estimated cost.
25739
+ * One session's token report — its `llm_call`s grouped per (provider,
25740
+ * model, tier) with derived cost, or `null` when the session made no
25741
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25742
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25743
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25744
+ * it replaces walked every `llm_call` in the store to find one session's.
25533
25745
  */
25534
25746
  tokenReportForSession(sessionId) {
25535
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25747
+ const rows = allRows(
25748
+ this.db.prepare(
25749
+ `${LLM_USAGE_SELECT}
25750
+ FROM audit_events
25751
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25752
+ ${LLM_USAGE_GROUP}`
25753
+ ),
25754
+ [sessionId]
25755
+ );
25756
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25536
25757
  return Promise.resolve(reports[0] ?? null);
25537
25758
  }
25538
25759
  /**
@@ -25556,42 +25777,6 @@ var SqliteActivityRepository = class {
25556
25777
  for (const row of rows) seen.add(toHarness(row.harness));
25557
25778
  return Promise.resolve([...seen]);
25558
25779
  }
25559
- /**
25560
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25561
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25562
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25563
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25564
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25565
- */
25566
- readLlmCallLeaves(opts = {}) {
25567
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25568
- const params = [];
25569
- if (opts.sessionId !== void 0) {
25570
- conditions.push("root_session_id = ?");
25571
- params.push(opts.sessionId);
25572
- }
25573
- if (opts.fromMs !== void 0) {
25574
- conditions.push("started_at >= ?");
25575
- params.push(opts.fromMs);
25576
- }
25577
- const rows = allRows(
25578
- this.db.prepare(
25579
- `SELECT root_session_id AS sessionId, attributes
25580
- FROM audit_events
25581
- WHERE ${conditions.join(" AND ")}`
25582
- ),
25583
- params
25584
- );
25585
- return mapRowsTolerant(
25586
- rows.filter(
25587
- (row) => row.sessionId !== null
25588
- ),
25589
- (row) => ({
25590
- sessionId: row.sessionId,
25591
- attributes: JSON.parse(row.attributes)
25592
- })
25593
- );
25594
- }
25595
25780
  /**
25596
25781
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25597
25782
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25606,20 +25791,23 @@ var SqliteActivityRepository = class {
25606
25791
  const inClause = placeholders(sessionIds.length);
25607
25792
  const lastActivityRows = allRows(
25608
25793
  this.db.prepare(
25609
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25610
- WHERE root_session_id IN (${inClause})
25611
- GROUP BY root_session_id`
25794
+ `SELECT ids.value AS id,
25795
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25796
+ (SELECT max(ended_at) FROM audit_events e
25797
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25798
+ FROM json_each(?) AS ids`
25612
25799
  ),
25613
- sessionIds
25800
+ [JSON.stringify(sessionIds)]
25614
25801
  );
25615
25802
  for (const row of lastActivityRows) {
25616
- if (row.id === null) continue;
25617
25803
  const entry = result.get(row.id);
25618
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25804
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25805
+ if (entry && last > 0) entry.lastActivityMs = last;
25619
25806
  }
25620
25807
  const turnsRows = allRows(
25621
25808
  this.db.prepare(
25622
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25809
+ `SELECT root_session_id AS id, count(*) AS n
25810
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25623
25811
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25624
25812
  GROUP BY root_session_id`
25625
25813
  ),
@@ -25634,7 +25822,7 @@ var SqliteActivityRepository = class {
25634
25822
  this.db.prepare(
25635
25823
  `SELECT root_session_id AS id,
25636
25824
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25637
- FROM audit_events
25825
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25638
25826
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25639
25827
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25640
25828
  GROUP BY root_session_id`
@@ -25664,7 +25852,7 @@ var SqliteActivityRepository = class {
25664
25852
  this.db.prepare(
25665
25853
  `SELECT root_session_id AS id,
25666
25854
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25667
- FROM audit_events
25855
+ FROM audit_events INDEXED BY idx_audit_session_share
25668
25856
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25669
25857
  GROUP BY root_session_id`
25670
25858
  ),
@@ -26693,7 +26881,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26693
26881
 
26694
26882
  // ../../packages/persistence/src/repositories/findings.ts
26695
26883
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26696
- var SCAN_BATCH_ROWS = 1e3;
26697
26884
  var DEFAULT_LOCATIONS_LIMIT = 100;
26698
26885
  var LOCATION_RULE_IDS_CAP = 20;
26699
26886
  function compareLocationOrder(a, b) {
@@ -26722,6 +26909,25 @@ function deriveInstanceStatus(row) {
26722
26909
  latestResolutionStatus: row.latest_status
26723
26910
  });
26724
26911
  }
26912
+ function toFlatFindingRow(r) {
26913
+ return {
26914
+ id: r.id,
26915
+ ruleId: r.rule_id,
26916
+ category: r.category,
26917
+ severity: r.severity,
26918
+ maskedMatch: r.masked_match,
26919
+ actionTaken: r.action_taken,
26920
+ confidence: r.confidence,
26921
+ occurredAt: epochMillisToIso(r.occurred_at),
26922
+ sourceTool: r.source_tool,
26923
+ repo: r.repo ?? "",
26924
+ file: r.file ?? "",
26925
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26926
+ eventId: r.event_id,
26927
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26928
+ status: deriveInstanceStatus(r)
26929
+ };
26930
+ }
26725
26931
  function encodeGroupCursor(group) {
26726
26932
  const payload = {
26727
26933
  sev: group.severity,
@@ -26797,7 +27003,7 @@ var SqliteFindingsRepository = class {
26797
27003
  this.db.prepare(
26798
27004
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26799
27005
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26800
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27006
+ e.source_tool AS source_tool,
26801
27007
  e.event_type AS kind
26802
27008
  FROM audit_events e
26803
27009
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26905,56 +27111,11 @@ var SqliteFindingsRepository = class {
26905
27111
  predicate,
26906
27112
  params: sessionParams
26907
27113
  });
26908
- const rows = allRows(
26909
- this.db.prepare(
26910
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26911
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26912
- kind, finding_key, latest_status
26913
- FROM (
26914
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26915
- d.severity AS severity, f.masked_match AS masked_match,
26916
- f.action_taken AS action_taken, f.confidence AS confidence,
26917
- e.started_at AS occurred_at,
26918
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26919
- json_extract(e.attributes, '$.repo') AS repo,
26920
- json_extract(e.attributes, '$.file_path') AS file,
26921
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26922
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26923
- e.event_type AS kind, f.finding_key AS finding_key,
26924
- latest.status AS latest_status,
26925
- ROW_NUMBER() OVER (
26926
- PARTITION BY d.rule_id
26927
- ORDER BY e.started_at DESC, f.id DESC
26928
- ) AS rn
26929
- FROM inspection_findings f
26930
- JOIN audit_events e ON e.id = f.audit_event_id
26931
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26932
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26933
- ON latest.finding_key = f.finding_key
26934
- ${predicate}
26935
- )
26936
- WHERE rn <= :cap
26937
- ORDER BY occurred_at DESC, id DESC`
26938
- ),
26939
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26940
- );
26941
- const groupable = rows.map((r) => ({
26942
- id: r.id,
26943
- ruleId: r.rule_id,
26944
- category: r.category,
26945
- severity: r.severity,
26946
- maskedMatch: r.masked_match,
26947
- actionTaken: r.action_taken,
26948
- confidence: r.confidence,
26949
- occurredAt: epochMillisToIso(r.occurred_at),
26950
- sourceTool: r.source_tool,
26951
- repo: r.repo ?? "",
26952
- file: r.file ?? "",
26953
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26954
- eventId: r.event_id,
26955
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26956
- status: deriveInstanceStatus(r)
26957
- }));
27114
+ const rows = this.previewRows(aggregates, {
27115
+ sessionId: query.sessionId,
27116
+ from: query.from
27117
+ });
27118
+ const groupable = rows.map(toFlatFindingRow);
26958
27119
  const allGroups = buildFindingGroups(groupable, { aggregates });
26959
27120
  const filterOpts = {
26960
27121
  severity: query.severity,
@@ -27040,8 +27201,10 @@ var SqliteFindingsRepository = class {
27040
27201
  *
27041
27202
  * The scan runs from the top of the scope on every request, not from the
27042
27203
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
27043
- * move as the caller pages. Rows are pulled in batches so memory stays flat
27044
- * while the counting runs, and only the page itself is retained.
27204
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27205
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27206
+ * counting runs — a generator streaming the index order, not a sequence of
27207
+ * fetched batches; only the page itself is retained.
27045
27208
  */
27046
27209
  listFindingInstances(query) {
27047
27210
  const opts = {
@@ -27057,6 +27220,10 @@ var SqliteFindingsRepository = class {
27057
27220
  };
27058
27221
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27059
27222
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27223
+ const isPastCursor = cursor === null ? () => true : (row) => {
27224
+ const rowMs = isoToEpochMillis(row.occurredAt);
27225
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27226
+ };
27060
27227
  const accumulator = createInstanceFacetAccumulator(opts);
27061
27228
  const items = [];
27062
27229
  let total = 0;
@@ -27069,6 +27236,7 @@ var SqliteFindingsRepository = class {
27069
27236
  accumulator.add(row);
27070
27237
  if (!matchesInstanceFilters(row, opts)) continue;
27071
27238
  total += 1;
27239
+ if (!isPastCursor(row)) continue;
27072
27240
  if (items.length < limit) {
27073
27241
  items.push(toInstanceDetail(row));
27074
27242
  last = row;
@@ -27077,15 +27245,6 @@ var SqliteFindingsRepository = class {
27077
27245
  }
27078
27246
  }
27079
27247
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27080
- if (cursor !== null) {
27081
- const resumed = this.pageAfter(cursor, opts, limit, query);
27082
- return Promise.resolve({
27083
- totals: { findings: total },
27084
- facets: accumulator.facets(),
27085
- items: resumed.items,
27086
- nextCursor: resumed.nextCursor
27087
- });
27088
- }
27089
27248
  return Promise.resolve({
27090
27249
  totals: { findings: total },
27091
27250
  facets: accumulator.facets(),
@@ -27093,35 +27252,6 @@ var SqliteFindingsRepository = class {
27093
27252
  nextCursor
27094
27253
  });
27095
27254
  }
27096
- /**
27097
- * The page of matching rows strictly after `cursor`. Separate from the
27098
- * counting pass because that one starts at the top of the scope by design;
27099
- * this one narrows the scan with the same keyset predicate the activity list
27100
- * uses, so a later page costs less than the first rather than more.
27101
- */
27102
- pageAfter(cursor, opts, limit, query) {
27103
- const items = [];
27104
- let last;
27105
- let hasMore = false;
27106
- for (const row of this.scanFindingRows({
27107
- sessionId: query.sessionId,
27108
- from: query.from,
27109
- after: cursor
27110
- })) {
27111
- if (!matchesInstanceFilters(row, opts)) continue;
27112
- if (items.length < limit) {
27113
- items.push(toInstanceDetail(row));
27114
- last = row;
27115
- } else {
27116
- hasMore = true;
27117
- break;
27118
- }
27119
- }
27120
- return {
27121
- items,
27122
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27123
- };
27124
- }
27125
27255
  /**
27126
27256
  * The same findings folded by location: repository, then file within it.
27127
27257
  *
@@ -27204,25 +27334,111 @@ var SqliteFindingsRepository = class {
27204
27334
  });
27205
27335
  }
27206
27336
  /**
27207
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27337
+ * Each group's newest instances, for the table's expanded rows.
27338
+ *
27339
+ * ONE index-ordered scan with early termination, and the shape is the point.
27340
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27341
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27342
+ * through a temp B-tree to keep a bounded preview of each group, and then
27343
+ * sorts the survivors again for the page order. Both sorts grow with the
27344
+ * store while the answer does not.
27345
+ *
27346
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27347
+ * (or the session or window index the scope names — see `findingScanSql`),
27348
+ * which is already the order the page wants, and keeps rows per rule until
27349
+ * each rule has as many as it can show. The aggregate the caller already holds
27350
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27351
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27352
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27353
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27354
+ * store with many firing rules widens it. The bound that DOES hold
27355
+ * unconditionally is the sorted form's floor: this scan visits at most as
27356
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27357
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27358
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27359
+ * wanted instances sitting at the tail of the scope — is one pass over
27360
+ * everything in scope with a block sort of the id tie-break only, never a
27361
+ * sort of the scope, which is still that floor.
27362
+ *
27363
+ * A row whose rule the aggregate did not see is skipped: the two statements
27364
+ * run without a shared snapshot, so a capture landing between them can add a
27365
+ * rule here that has no counts there, and the counts are what the group is
27366
+ * built from.
27367
+ */
27368
+ previewRows(aggregates, scope) {
27369
+ const wanted = /* @__PURE__ */ new Map();
27370
+ let remaining = 0;
27371
+ for (const [ruleId, agg] of aggregates) {
27372
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27373
+ wanted.set(ruleId, n);
27374
+ remaining += n;
27375
+ }
27376
+ const rows = [];
27377
+ if (remaining === 0) return rows;
27378
+ const { sql, params } = this.findingScanSql(scope);
27379
+ const taken = /* @__PURE__ */ new Map();
27380
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27381
+ const want = wanted.get(r.rule_id);
27382
+ if (want === void 0) continue;
27383
+ const have = taken.get(r.rule_id) ?? 0;
27384
+ if (have >= want) continue;
27385
+ taken.set(r.rule_id, have + 1);
27386
+ rows.push(r);
27387
+ remaining -= 1;
27388
+ if (remaining === 0) break;
27389
+ }
27390
+ return rows;
27391
+ }
27392
+ /**
27393
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27208
27394
  *
27209
27395
  * A generator so a caller streams the scope without it ever being an array:
27210
27396
  * the flat list counts and facets the whole filtered scope, which on a large
27211
- * store is far more rows than any page. Each batch advances the same keyset
27212
- * predicate the page read uses, so the scan is a sequence of bounded reads
27213
- * rather than one unbounded result set.
27214
- *
27215
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27216
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27217
- * makes it a point lookup per row, and the derived table would re-materialize
27218
- * a window over the whole resolution table once per batch.
27397
+ * store is far more rows than any page. The rows come off ONE statement,
27398
+ * iterated rather than materialized, in the index order `findingScanSql`
27399
+ * arranges so the scan is a single pass with a block sort of the id
27400
+ * tie-break only, never a sort of the scope, where a sequence of
27401
+ * keyset-bounded batches re-sorted everything below the cursor on every
27402
+ * batch and cost the square of the scope.
27219
27403
  *
27220
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27221
- * would be missing from its own facet, which is computed by excluding that
27222
- * dimension see listFindingInstances.
27404
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27405
+ * dimension narrowed here would be missing from its own facet, which is
27406
+ * computed by excluding that dimension (see listFindingInstances). There is
27407
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27408
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27409
+ * narrower statement, since the counting pass already visits every row a
27410
+ * page-2+ request would otherwise re-seek for.
27223
27411
  */
27224
27412
  *scanFindingRows(scope) {
27225
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27413
+ const { sql, params } = this.findingScanSql(scope);
27414
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27415
+ yield toFlatFindingRow(r);
27416
+ }
27417
+ }
27418
+ /**
27419
+ * The one statement both instance-level scans run: every finding in scope,
27420
+ * joined to its event and definition, newest first.
27421
+ *
27422
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27423
+ * the same two `recentFindings` documents at length, for the same reason:
27424
+ *
27425
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27426
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27427
+ * yields `started_at` order per event type, not across the four, so
27428
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27429
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27430
+ * `idx_audit_session` for a session scope, which is also `started_at`
27431
+ * ordered within the session — and the order falls out of the index.
27432
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27433
+ * JOINs the planner drives from the findings and sorts everything.
27434
+ *
27435
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27436
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27437
+ * index probe per keyed row, and a derived table over the whole resolution
27438
+ * table would be materialized before the first row streamed.
27439
+ */
27440
+ findingScanSql(scope) {
27441
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27226
27442
  const params = [];
27227
27443
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27228
27444
  conditions.push("e.root_session_id = ?");
@@ -27236,58 +27452,24 @@ var SqliteFindingsRepository = class {
27236
27452
  d.severity AS severity, f.masked_match AS masked_match,
27237
27453
  f.action_taken AS action_taken, f.confidence AS confidence,
27238
27454
  e.started_at AS occurred_at,
27239
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27240
- json_extract(e.attributes, '$.repo') AS repo,
27241
- json_extract(e.attributes, '$.file_path') AS file,
27242
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27455
+ e.source_tool AS source_tool,
27456
+ e.repo AS repo,
27457
+ e.file_path AS file,
27458
+ e.tool_name AS tool_name,
27243
27459
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27244
27460
  e.event_type AS kind, f.finding_key AS finding_key,
27245
27461
  ${latestResolutionStatusSql("f")} AS latest_status
27246
- FROM inspection_findings f
27247
- JOIN audit_events e ON e.id = f.audit_event_id
27248
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27462
+ FROM audit_events e
27463
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27464
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27249
27465
  WHERE ${conditions.join(" AND ")}
27250
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27251
- ORDER BY e.started_at DESC, f.id DESC
27252
- LIMIT ?`;
27253
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27254
- for (; ; ) {
27255
- const rows = allRows(this.db.prepare(sql), [
27256
- ...params,
27257
- after.startedAtMs,
27258
- after.startedAtMs,
27259
- after.id,
27260
- SCAN_BATCH_ROWS
27261
- ]);
27262
- for (const r of rows) {
27263
- yield {
27264
- id: r.id,
27265
- ruleId: r.rule_id,
27266
- category: r.category,
27267
- severity: r.severity,
27268
- maskedMatch: r.masked_match,
27269
- actionTaken: r.action_taken,
27270
- confidence: r.confidence,
27271
- occurredAt: epochMillisToIso(r.occurred_at),
27272
- sourceTool: r.source_tool,
27273
- repo: r.repo ?? "",
27274
- file: r.file ?? "",
27275
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27276
- eventId: r.event_id,
27277
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27278
- status: deriveInstanceStatus(r)
27279
- };
27280
- }
27281
- if (rows.length < SCAN_BATCH_ROWS) return;
27282
- const lastRow = rows[rows.length - 1];
27283
- if (lastRow === void 0) return;
27284
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27285
- }
27466
+ ORDER BY e.started_at DESC, f.id DESC`;
27467
+ return { sql, params };
27286
27468
  }
27287
27469
  groupAggregates(withSearchText, scope) {
27288
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27289
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27290
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27470
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27471
+ group_concat(DISTINCT e.file_path) AS files,
27472
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27291
27473
  const rows = this.db.prepare(
27292
27474
  `SELECT rule_id,
27293
27475
  sum(tuple_count) AS instance_count,
@@ -27305,7 +27487,7 @@ var SqliteFindingsRepository = class {
27305
27487
  coalesce(latest.status, '') AS status_tuple,
27306
27488
  count(*) AS tuple_count,
27307
27489
  max(e.started_at) AS latest_at,
27308
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27490
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27309
27491
  group_concat(DISTINCT f.action_taken) AS actions_taken
27310
27492
  ${innerSearchColumns}
27311
27493
  FROM inspection_findings f
@@ -27436,6 +27618,8 @@ function isoDay(ms) {
27436
27618
  // ../../packages/persistence/src/repositories/history-sync.ts
27437
27619
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27438
27620
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27621
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27622
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27439
27623
  var SKIPPED = -1;
27440
27624
  var ROW_COLUMNS = `id,
27441
27625
  parent_id AS parentId,
@@ -27475,6 +27659,20 @@ var SqliteHistorySyncRepository = class {
27475
27659
  ORDER BY (event_type = 'session') DESC, started_at
27476
27660
  LIMIT :limit`
27477
27661
  );
27662
+ this.captureRowsStmt = db.prepare(
27663
+ `SELECT ${ROW_COLUMNS}
27664
+ FROM audit_events
27665
+ WHERE synced_at IS NULL
27666
+ AND sync_claimed_at IS NULL
27667
+ AND outbox_owed = 1
27668
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27669
+ AND started_at < :before
27670
+ ORDER BY started_at
27671
+ LIMIT :limit`
27672
+ );
27673
+ this.markOwedStmt = db.prepare(
27674
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27675
+ );
27478
27676
  this.stampStmt = db.prepare(
27479
27677
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27480
27678
  );
@@ -27506,6 +27704,12 @@ var SqliteHistorySyncRepository = class {
27506
27704
  FROM audit_events
27507
27705
  WHERE event_type IN (${TYPE_LIST})`
27508
27706
  );
27707
+ this.captureSkipCountStmt = db.prepare(
27708
+ `SELECT COUNT(*) AS skipped
27709
+ FROM audit_events
27710
+ WHERE synced_at = ${String(SKIPPED)}
27711
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27712
+ );
27509
27713
  this.fingerprintStmt = db.prepare(
27510
27714
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27511
27715
  FROM history_sync WHERE id = 1`
@@ -27515,6 +27719,10 @@ var SqliteHistorySyncRepository = class {
27515
27719
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27516
27720
  WHERE id = 1`
27517
27721
  );
27722
+ this.disownCapturesStmt = db.prepare(
27723
+ `UPDATE audit_events SET outbox_owed = NULL
27724
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27725
+ );
27518
27726
  this.rearmStmt = db.prepare(
27519
27727
  `UPDATE audit_events SET synced_at = NULL
27520
27728
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27587,6 +27795,10 @@ var SqliteHistorySyncRepository = class {
27587
27795
  closeWindowStmt;
27588
27796
  releaseBoundaryStmt;
27589
27797
  freezeBoundaryStmt;
27798
+ captureRowsStmt;
27799
+ markOwedStmt;
27800
+ captureSkipCountStmt;
27801
+ disownCapturesStmt;
27590
27802
  partitionStmt;
27591
27803
  claimRowStmt;
27592
27804
  releaseRowStmt;
@@ -27620,6 +27832,34 @@ var SqliteHistorySyncRepository = class {
27620
27832
  pendingRows(sessionId, limit, before) {
27621
27833
  return allRows(this.rowsStmt, { sessionId, limit, before });
27622
27834
  }
27835
+ /**
27836
+ * Captures this machine still owes the deployment, oldest first.
27837
+ *
27838
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27839
+ * by a time window — see captureRowsStmt for why a window could not express
27840
+ * this. `before` is the grace window that leaves a just-recorded capture to
27841
+ * the live path.
27842
+ */
27843
+ pendingCaptureRows(limit, before) {
27844
+ return allRows(this.captureRowsStmt, { limit, before });
27845
+ }
27846
+ /**
27847
+ * Record that a capture is OWED to the deployment.
27848
+ *
27849
+ * Written by the attached forward path when a live send did not confirm
27850
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27851
+ * a fact rather than an inference: the machine was attached, the send did not
27852
+ * land, so the row is owed — which no time window can state, because the same
27853
+ * window that holds the rows a past attachment left owed also holds every
27854
+ * capture recorded while the machine was DETACHED, and those were never
27855
+ * offered to anyone.
27856
+ *
27857
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27858
+ * out of the drain's read.
27859
+ */
27860
+ markCaptureOwed(id) {
27861
+ this.markOwedStmt.run({ id });
27862
+ }
27623
27863
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27624
27864
  markSynced(ids, atMs) {
27625
27865
  this.stampAll(ids, atMs);
@@ -27703,10 +27943,12 @@ var SqliteHistorySyncRepository = class {
27703
27943
  this.countsStmt,
27704
27944
  { before }
27705
27945
  );
27946
+ const captures = getRow(this.captureSkipCountStmt);
27706
27947
  return {
27707
27948
  pending: row?.pending ?? 0,
27708
27949
  sent: row?.sent ?? 0,
27709
- skipped: row?.skipped ?? 0
27950
+ skipped: row?.skipped ?? 0,
27951
+ capturesSkipped: captures?.skipped ?? 0
27710
27952
  };
27711
27953
  }
27712
27954
  /**
@@ -27747,7 +27989,11 @@ var SqliteHistorySyncRepository = class {
27747
27989
  withTransaction(
27748
27990
  this.db,
27749
27991
  () => {
27992
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27750
27993
  this.rearmStmt.run();
27994
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27995
+ this.disownCapturesStmt.run();
27996
+ }
27751
27997
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27752
27998
  },
27753
27999
  "IMMEDIATE"
@@ -27944,7 +28190,259 @@ var SqliteInspectionFindingsRepository = class {
27944
28190
  };
27945
28191
 
27946
28192
  // ../../packages/persistence/src/repositories/installed-packs.ts
27947
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28193
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28194
+
28195
+ // ../../packages/persistence/src/policy-floor.ts
28196
+ import { readFileSync as readFileSync5 } from "fs";
28197
+ import { join as join6 } from "path";
28198
+
28199
+ // ../../packages/persistence/src/local-layout.ts
28200
+ import { renameSync as renameSync3 } from "fs";
28201
+ import { mkdir } from "fs/promises";
28202
+ import { homedir } from "os";
28203
+ import { join as join4 } from "path";
28204
+ function defaultDataDir() {
28205
+ return join4(homedir(), ".aka");
28206
+ }
28207
+ function settingsDir(base = defaultDataDir()) {
28208
+ return join4(base, "settings");
28209
+ }
28210
+ function dataDir(base = defaultDataDir()) {
28211
+ return join4(base, "data");
28212
+ }
28213
+ function dbPath(base = defaultDataDir()) {
28214
+ return join4(dataDir(base), "aka.db");
28215
+ }
28216
+ function keysDir(base = defaultDataDir()) {
28217
+ return join4(base, "keys");
28218
+ }
28219
+ async function ensureDataDir(dir = defaultDataDir()) {
28220
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
28221
+ tightenDir(dir);
28222
+ }
28223
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28224
+ ensureDataDirSync(dir);
28225
+ }
28226
+ function migrateLegacyLayout(base = defaultDataDir()) {
28227
+ const moves = [
28228
+ { name: "config.json", dest: settingsDir(base) },
28229
+ { name: "policy-cache.json", dest: dataDir(base) }
28230
+ ];
28231
+ for (const { name, dest } of moves) {
28232
+ try {
28233
+ ensureDataDirSync(dest);
28234
+ const moved = join4(dest, name);
28235
+ renameSync3(join4(base, name), moved);
28236
+ tightenFile(moved);
28237
+ } catch {
28238
+ }
28239
+ }
28240
+ }
28241
+
28242
+ // ../../packages/persistence/src/settings.ts
28243
+ import { readFileSync as readFileSync4 } from "fs";
28244
+ import { join as join5 } from "path";
28245
+
28246
+ // ../../packages/persistence/src/file-lock.ts
28247
+ import { randomUUID as randomUUID3 } from "crypto";
28248
+ import {
28249
+ closeSync,
28250
+ existsSync as existsSync2,
28251
+ openSync,
28252
+ readFileSync as readFileSync2,
28253
+ rmSync as rmSync5,
28254
+ statSync as statSync3,
28255
+ writeFileSync as writeFileSync2
28256
+ } from "fs";
28257
+ import { hostname as hostname3 } from "os";
28258
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28259
+
28260
+ // ../../packages/persistence/src/managed-settings.ts
28261
+ import { readFileSync as readFileSync3 } from "fs";
28262
+ import { posix, win32 } from "path";
28263
+ function managedSettingsPaths(platform2 = process.platform) {
28264
+ if (platform2 === "darwin") {
28265
+ return [
28266
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28267
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28268
+ ];
28269
+ }
28270
+ if (platform2 === "win32") {
28271
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28272
+ }
28273
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28274
+ }
28275
+ function readManagedSettings(paths = managedSettingsPaths()) {
28276
+ for (const path of paths) {
28277
+ let text;
28278
+ try {
28279
+ text = readFileSync3(path, "utf8");
28280
+ } catch {
28281
+ continue;
28282
+ }
28283
+ const record2 = parseJsonObject(text);
28284
+ if (!record2) continue;
28285
+ const parsed2 = ManagedSettings.safeParse(record2);
28286
+ if (parsed2.success) return parsed2.data;
28287
+ }
28288
+ return null;
28289
+ }
28290
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28291
+ if (!managed) return settings;
28292
+ const { values } = managed;
28293
+ const merged = { ...settings };
28294
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28295
+ if (values.controlPlane !== void 0) {
28296
+ merged.controlPlane = {
28297
+ ...values.controlPlane,
28298
+ // The administrator pinned WHICH deployment, not WHEN this machine
28299
+ // joined it. Keep the user's own attach time when the endpoint is
28300
+ // unchanged, so a managed machine does not appear to re-attach on every
28301
+ // read; stamp a fresh one when the administrator moved it.
28302
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28303
+ };
28304
+ }
28305
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28306
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28307
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28308
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28309
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28310
+ if (values.vaultConsent !== void 0) {
28311
+ merged.vaultConsent = values.vaultConsent ? (
28312
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28313
+ // at the current version otherwise.
28314
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28315
+ ) : void 0;
28316
+ }
28317
+ if (values.modelJudgeConsent !== void 0) {
28318
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28319
+ acknowledgedAt: now().toISOString(),
28320
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28321
+ } : void 0;
28322
+ }
28323
+ return merged;
28324
+ }
28325
+
28326
+ // ../../packages/persistence/src/settings.ts
28327
+ var SETTINGS_FILENAME = "settings.json";
28328
+ function readWorkspaceSettings(base = defaultDataDir()) {
28329
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28330
+ }
28331
+ function readUserSettings(base) {
28332
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28333
+ if (!record2) return defaultWorkspaceSettings();
28334
+ try {
28335
+ return WorkspaceSettings.parse(record2);
28336
+ } catch {
28337
+ return defaultWorkspaceSettings();
28338
+ }
28339
+ }
28340
+ function readJson(file2) {
28341
+ let text;
28342
+ try {
28343
+ text = readFileSync4(file2, "utf8");
28344
+ } catch {
28345
+ return null;
28346
+ }
28347
+ return parseJsonObject(text) ?? null;
28348
+ }
28349
+
28350
+ // ../../packages/persistence/src/policy-floor.ts
28351
+ function refusalMessage(pack, attempted, floor, refusal) {
28352
+ switch (refusal) {
28353
+ case "lock":
28354
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28355
+ case "disable":
28356
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28357
+ case "floor":
28358
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28359
+ }
28360
+ }
28361
+ var PolicyFloorError = class extends Error {
28362
+ /** `namespace/packId` of the detection whose write was refused. */
28363
+ pack;
28364
+ /**
28365
+ * The archetype the caller asked for, or null when the write named none —
28366
+ * clearing the assignment, or switching the detection off.
28367
+ */
28368
+ attempted;
28369
+ /** The weakest archetype the control plane permits for this pack. */
28370
+ floor;
28371
+ refusal;
28372
+ constructor(pack, attempted, floor, refusal) {
28373
+ super(refusalMessage(pack, attempted, floor, refusal));
28374
+ this.name = "PolicyFloorError";
28375
+ this.pack = pack;
28376
+ this.attempted = attempted;
28377
+ this.floor = floor;
28378
+ this.refusal = refusal;
28379
+ }
28380
+ };
28381
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28382
+ try {
28383
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28384
+ const parsed2 = JSON.parse(raw);
28385
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28386
+ return PolicyBundle.parse(parsed2.bundle);
28387
+ } catch {
28388
+ return null;
28389
+ }
28390
+ }
28391
+ function indexEnabled(policies) {
28392
+ const byRuleId = /* @__PURE__ */ new Map();
28393
+ const byCategory = /* @__PURE__ */ new Map();
28394
+ for (const policy of policies) {
28395
+ if (!policy.enabled) continue;
28396
+ if ("ruleId" in policy.target) {
28397
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28398
+ } else if (!byCategory.has(policy.target.category)) {
28399
+ byCategory.set(policy.target.category, policy.action);
28400
+ }
28401
+ }
28402
+ return { byRuleId, byCategory };
28403
+ }
28404
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28405
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28406
+ const categories = new Set(rules.map((rule) => rule.category));
28407
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28408
+ return policies.some((policy) => {
28409
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28410
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28411
+ });
28412
+ }
28413
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28414
+ const floors = openControlPlaneFloors(base);
28415
+ return floors === null ? null : floors.floorFor(rules);
28416
+ }
28417
+ function openControlPlaneFloors(base = defaultDataDir()) {
28418
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28419
+ const bundle = readCachedPolicyBundle(base);
28420
+ if (bundle === null) return null;
28421
+ const indexes = indexEnabled(bundle.policies);
28422
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28423
+ }
28424
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28425
+ let action = null;
28426
+ for (const rule of rules) {
28427
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28428
+ if (resolved === void 0) continue;
28429
+ action = action === null ? resolved : strongerAction(action, resolved);
28430
+ }
28431
+ if (action === null) return null;
28432
+ return {
28433
+ floor: weakestBuiltinAtLeast(action),
28434
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28435
+ };
28436
+ }
28437
+ function policyAssignmentRefusal(policyId, floor) {
28438
+ if (floor.locked) return "lock";
28439
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28440
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28441
+ }
28442
+ function packEnablementRefusal(enabled, floor) {
28443
+ if (floor === null || enabled) return null;
28444
+ return "disable";
28445
+ }
27948
28446
 
27949
28447
  // ../../packages/persistence/src/semver.ts
27950
28448
  function parse3(version2) {
@@ -28038,8 +28536,19 @@ function ruleIdsOf(rulesJson) {
28038
28536
  return ids;
28039
28537
  }
28040
28538
  var SqliteInstalledPacksRepository = class {
28041
- constructor(db) {
28539
+ /**
28540
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28541
+ * floor needs both halves of it (settings/ says whether this machine is
28542
+ * attached, data/ holds the cached bundle). It is optional because a caller
28543
+ * holding only a DatabaseSync — every test construction site, and any embedder
28544
+ * that opens the store itself — has no layout to point at, and such a caller
28545
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28546
+ * from `openLocalDatabase`, which is the single construction site that owns a
28547
+ * real `~/.aka`.
28548
+ */
28549
+ constructor(db, baseDir) {
28042
28550
  this.db = db;
28551
+ this.baseDir = baseDir;
28043
28552
  this.insertMissingStmt = db.prepare(
28044
28553
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
28045
28554
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28061,11 +28570,17 @@ var SqliteInstalledPacksRepository = class {
28061
28570
  this.signatureStmt = db.prepare(
28062
28571
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28063
28572
  );
28573
+ this.packRulesStmt = db.prepare(
28574
+ `SELECT rules_json AS rulesJson FROM installed_packs
28575
+ WHERE namespace = ? AND pack_id = ?`
28576
+ );
28064
28577
  }
28065
28578
  db;
28579
+ baseDir;
28066
28580
  insertMissingStmt;
28067
28581
  upsertAvailableStmt;
28068
28582
  signatureStmt;
28583
+ packRulesStmt;
28069
28584
  /**
28070
28585
  * Record the running binary's detection inventory. Refreshes the
28071
28586
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28107,7 +28622,7 @@ var SqliteInstalledPacksRepository = class {
28107
28622
  let behind = false;
28108
28623
  for (const row of rows) {
28109
28624
  const params = {
28110
- id: randomUUID3(),
28625
+ id: randomUUID4(),
28111
28626
  namespace: row.namespace,
28112
28627
  packId: row.packId,
28113
28628
  version: row.version,
@@ -28119,7 +28634,7 @@ var SqliteInstalledPacksRepository = class {
28119
28634
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28120
28635
  this.upsertAvailableStmt.run({
28121
28636
  ...params,
28122
- id: randomUUID3(),
28637
+ id: randomUUID4(),
28123
28638
  recordedBy: meta4?.recordedBy ?? null
28124
28639
  });
28125
28640
  } else {
@@ -28365,9 +28880,65 @@ var SqliteInstalledPacksRepository = class {
28365
28880
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28366
28881
  // caller rather than swallowing them. Each returns whether a row matched, so the
28367
28882
  // caller can tell an edit from a no-such-detection.
28883
+ /**
28884
+ * The rules one installed pack owns, reduced to what a floor computation
28885
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28886
+ * unreadable contributes no rules to a scan either, so it is not a detection
28887
+ * the control plane can be governing, and an empty list correctly imposes no
28888
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28889
+ * the user can re-enable, and its assignment stays governed meanwhile.
28890
+ */
28891
+ packFloorRules(namespace, packId) {
28892
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28893
+ if (!row) return [];
28894
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28895
+ }
28896
+ /**
28897
+ * What the connected control plane imposes on one installed pack, or null on a
28898
+ * machine that is its own authority (standalone, no cached bundle, or a
28899
+ * repository constructed without a layout base).
28900
+ *
28901
+ * Exposed as a READ so a surface can render the constraint — grey out the
28902
+ * choices below the floor, mark a locked detection as locked — rather than
28903
+ * offer the user a picker whose selections it will then be told it may not
28904
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28905
+ */
28906
+ policyFloor(namespace, packId) {
28907
+ if (this.baseDir === void 0) return null;
28908
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28909
+ }
28910
+ /**
28911
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28912
+ * entry only for a pack the control plane actually governs.
28913
+ *
28914
+ * A surface listing every detection asks per pack, and asking through
28915
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28916
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28917
+ * answer, repeated for each row, on every render. This reads all of that once.
28918
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28919
+ * exactly as the single-pack read returns null for them.
28920
+ */
28921
+ policyFloors(packs) {
28922
+ const floors = /* @__PURE__ */ new Map();
28923
+ if (this.baseDir === void 0) return floors;
28924
+ const source = openControlPlaneFloors(this.baseDir);
28925
+ if (source === null) return floors;
28926
+ for (const pack of packs) {
28927
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28928
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28929
+ }
28930
+ return floors;
28931
+ }
28368
28932
  /**
28369
28933
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28370
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28934
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28935
+ *
28936
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28937
+ * write below, and a detection the organization has authored a policy for is
28938
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28939
+ * a throw rather than a silently substituted value. This is the one device-local
28940
+ * write path for the assignment, so the check belongs here rather than on any
28941
+ * surface that offers the choice.
28371
28942
  */
28372
28943
  setPolicy(namespace, packId, policyId) {
28373
28944
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28375,14 +28946,38 @@ var SqliteInstalledPacksRepository = class {
28375
28946
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28376
28947
  );
28377
28948
  }
28949
+ const requested = policyId;
28950
+ const floor = this.policyFloor(namespace, packId);
28951
+ if (floor !== null) {
28952
+ const refusal = policyAssignmentRefusal(requested, floor);
28953
+ if (refusal !== null) {
28954
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28955
+ }
28956
+ }
28378
28957
  const res = this.db.prepare(
28379
28958
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28380
28959
  WHERE namespace = :namespace AND pack_id = :packId`
28381
28960
  ).run({ policyId, now: Date.now(), namespace, packId });
28382
28961
  return Number(res.changes) > 0;
28383
28962
  }
28384
- /** Enable or disable one installed pack. */
28963
+ /**
28964
+ * Enable or disable one installed pack.
28965
+ *
28966
+ * On an ATTACHED machine a detection the organization's bundle governs at all
28967
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
28968
+ * merely another point below the floor, and why re-enabling stays open. Like
28969
+ * the assignment above, the check belongs at this write path rather than on a
28970
+ * surface: this is the one device-local writer of the column, and a refusal
28971
+ * that lived in a page would leave the CLI free.
28972
+ */
28385
28973
  setEnabled(namespace, packId, enabled) {
28974
+ const floor = this.policyFloor(namespace, packId);
28975
+ if (floor !== null) {
28976
+ const refusal = packEnablementRefusal(enabled, floor);
28977
+ if (refusal !== null) {
28978
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
28979
+ }
28980
+ }
28386
28981
  const res = this.db.prepare(
28387
28982
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28388
28983
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28468,7 +29063,7 @@ var SqliteInventoryRepository = class {
28468
29063
  };
28469
29064
 
28470
29065
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28471
- import { randomUUID as randomUUID4 } from "crypto";
29066
+ import { randomUUID as randomUUID5 } from "crypto";
28472
29067
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28473
29068
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28474
29069
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28957,7 +29552,7 @@ var SqliteInventoryAssetsRepository = class {
28957
29552
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28958
29553
  VALUES (:id, :projectId, :path, :access, :now, :now)
28959
29554
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28960
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29555
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28961
29556
  }
28962
29557
  return true;
28963
29558
  }
@@ -28978,7 +29573,7 @@ var SqliteInventoryAssetsRepository = class {
28978
29573
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28979
29574
  VALUES (:id, :assetId, :trust, :now, :now)
28980
29575
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28981
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29576
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28982
29577
  }
28983
29578
  this.configRowsCache = void 0;
28984
29579
  return "ok";
@@ -29275,7 +29870,7 @@ var SqliteInventoryAssetsRepository = class {
29275
29870
  };
29276
29871
 
29277
29872
  // ../../packages/persistence/src/repositories/policies.ts
29278
- import { randomUUID as randomUUID5 } from "crypto";
29873
+ import { randomUUID as randomUUID6 } from "crypto";
29279
29874
  var SqlitePoliciesRepository = class {
29280
29875
  constructor(db) {
29281
29876
  this.db = db;
@@ -29310,7 +29905,7 @@ var SqlitePoliciesRepository = class {
29310
29905
  failOpenTransaction(this.db, () => {
29311
29906
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29312
29907
  stmt.run({
29313
- id: randomUUID5(),
29908
+ id: randomUUID6(),
29314
29909
  target: JSON.stringify({ category }),
29315
29910
  action,
29316
29911
  now: Date.now()
@@ -29330,7 +29925,7 @@ var SqlitePoliciesRepository = class {
29330
29925
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29331
29926
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29332
29927
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29333
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29928
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29334
29929
  }
29335
29930
  // Caps every global per-category policy currently set to block/redact down
29336
29931
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29398,7 +29993,7 @@ var SqlitePolicyCatalogRepository = class {
29398
29993
  };
29399
29994
 
29400
29995
  // ../../packages/persistence/src/repositories/project-files.ts
29401
- import { randomUUID as randomUUID6 } from "crypto";
29996
+ import { randomUUID as randomUUID7 } from "crypto";
29402
29997
  var SqliteProjectFilesRepository = class {
29403
29998
  constructor(db) {
29404
29999
  this.db = db;
@@ -29430,7 +30025,7 @@ var SqliteProjectFilesRepository = class {
29430
30025
  const stamp = Math.max(now, maxStamp + 1);
29431
30026
  for (const file2 of scan2.files) {
29432
30027
  this.upsertStmt.run({
29433
- id: randomUUID6(),
30028
+ id: randomUUID7(),
29434
30029
  projectId,
29435
30030
  path: file2.path,
29436
30031
  name: file2.name,
@@ -29444,9 +30039,9 @@ var SqliteProjectFilesRepository = class {
29444
30039
  };
29445
30040
 
29446
30041
  // ../../packages/persistence/src/repositories/resolutions.ts
29447
- import { randomUUID as randomUUID7 } from "crypto";
30042
+ import { randomUUID as randomUUID8 } from "crypto";
29448
30043
  var SqliteResolutionsRepository = class {
29449
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30044
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29450
30045
  this.db = db;
29451
30046
  this.now = now;
29452
30047
  this.newId = newId;
@@ -29659,7 +30254,7 @@ var SqliteScanLedgerRepository = class {
29659
30254
  };
29660
30255
 
29661
30256
  // ../../packages/persistence/src/repositories/secret-vault.ts
29662
- import { randomUUID as randomUUID8 } from "crypto";
30257
+ import { randomUUID as randomUUID9 } from "crypto";
29663
30258
  function pageLimit(requested, fallback) {
29664
30259
  if (requested === void 0) return fallback;
29665
30260
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29705,12 +30300,14 @@ var SELECT_COLUMNS = `
29705
30300
  ciphertext,
29706
30301
  nonce,
29707
30302
  auth_tag AS authTag,
30303
+ user_authorized AS userAuthorized,
29708
30304
  occurrence_count AS occurrenceCount,
29709
30305
  first_seen AS firstSeen,
29710
30306
  last_seen AS lastSeen`;
29711
30307
  function toRow(raw) {
29712
- const { provider, ...rest } = raw;
29713
- return provider === null ? rest : { ...rest, provider };
30308
+ const { provider, userAuthorized, ...rest } = raw;
30309
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30310
+ return provider === null ? row : { ...row, provider };
29714
30311
  }
29715
30312
  var SqliteSecretVaultRepository = class {
29716
30313
  constructor(db) {
@@ -29720,17 +30317,18 @@ var SqliteSecretVaultRepository = class {
29720
30317
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29721
30318
  format_version, category, rule_id, masked_match, provider,
29722
30319
  ciphertext, nonce, auth_tag,
29723
- occurrence_count, first_seen, last_seen
30320
+ user_authorized, occurrence_count, first_seen, last_seen
29724
30321
  ) VALUES (
29725
30322
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29726
30323
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29727
30324
  :ciphertext, :nonce, :authTag,
29728
- 1, :now, :now
30325
+ :userAuthorized, 1, :now, :now
29729
30326
  )`
29730
30327
  );
29731
30328
  this.bumpStmt = db.prepare(
29732
30329
  `UPDATE secret_vault
29733
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30330
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30331
+ user_authorized = max(user_authorized, :userAuthorized)
29734
30332
  WHERE value_fingerprint = :valueFingerprint`
29735
30333
  );
29736
30334
  this.byPointerStmt = db.prepare(
@@ -29750,6 +30348,7 @@ var SqliteSecretVaultRepository = class {
29750
30348
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29751
30349
  WHERE pointer_id = :pointerId`
29752
30350
  );
30351
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29753
30352
  this.derefStmt = db.prepare(
29754
30353
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29755
30354
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29763,6 +30362,7 @@ var SqliteSecretVaultRepository = class {
29763
30362
  listStmt;
29764
30363
  replaceCiphertextStmt;
29765
30364
  refreshFingerprintStmt;
30365
+ deleteByPointerStmt;
29766
30366
  derefStmt;
29767
30367
  /**
29768
30368
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29771,6 +30371,11 @@ var SqliteSecretVaultRepository = class {
29771
30371
  * pointer, category and ciphertext, so the same secret always resolves to one
29772
30372
  * wire token. `minted` is true only when this call created the row.
29773
30373
  *
30374
+ * `userAuthorized` is the one field a repeat call may still change, and only
30375
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30376
+ * the row is shared with every automatic path that vaults the same value. See
30377
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30378
+ *
29774
30379
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29775
30380
  * writers cannot both decide they are minting.
29776
30381
  */
@@ -29797,13 +30402,18 @@ var SqliteSecretVaultRepository = class {
29797
30402
  ciphertext: input2.ciphertext,
29798
30403
  nonce: input2.nonce,
29799
30404
  authTag: input2.authTag,
30405
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29800
30406
  now
29801
30407
  })
29802
30408
  );
29803
30409
  minted = true;
29804
30410
  return;
29805
30411
  }
29806
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30412
+ this.bumpStmt.run({
30413
+ valueFingerprint: input2.valueFingerprint,
30414
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30415
+ now
30416
+ });
29807
30417
  },
29808
30418
  "IMMEDIATE"
29809
30419
  );
@@ -29863,6 +30473,42 @@ var SqliteSecretVaultRepository = class {
29863
30473
  );
29864
30474
  return destroyed;
29865
30475
  }
30476
+ /**
30477
+ * Destroy the named entries and report WHICH ones went — the scoped
30478
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30479
+ * values back where they came from. Ids the store does not hold are absent
30480
+ * from the answer rather than an error, so a set assembled from a stale read
30481
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30482
+ * it.
30483
+ *
30484
+ * The ids come back rather than a count because the caller's next act is to
30485
+ * write a purge row per destroyed entry, and a record of destruction has to
30486
+ * be a record of what was really destroyed: a selection is a claim about a
30487
+ * read that has since gone stale, and auditing from it invents a purge for an
30488
+ * entry still sitting in the vault.
30489
+ *
30490
+ * One transaction over the whole set rather than a statement per id: the
30491
+ * caller hands this the result of a restore pass it has completed, and a
30492
+ * fault partway through must leave the vault as it was found rather than
30493
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30494
+ * stands for, so half a delete is not a state anything can recover from.
30495
+ */
30496
+ deleteByPointerIds(pointerIds) {
30497
+ if (pointerIds.length === 0) return [];
30498
+ const deleted = [];
30499
+ withTransaction(
30500
+ this.db,
30501
+ () => {
30502
+ for (const pointerId of pointerIds) {
30503
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30504
+ deleted.push(pointerId);
30505
+ }
30506
+ }
30507
+ },
30508
+ "IMMEDIATE"
30509
+ );
30510
+ return deleted;
30511
+ }
29866
30512
  /**
29867
30513
  * Record (or re-stamp) one place a pointer has been written. One row per
29868
30514
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29875,7 +30521,7 @@ var SqliteSecretVaultRepository = class {
29875
30521
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29876
30522
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29877
30523
  ).run({
29878
- id: randomUUID8(),
30524
+ id: randomUUID9(),
29879
30525
  pointerId: entry.pointerId,
29880
30526
  location: entry.location,
29881
30527
  kind: entry.kind,
@@ -30388,15 +31034,15 @@ var SqliteSecurityRepository = class {
30388
31034
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30389
31035
  const rows = allRows(
30390
31036
  this.db.prepare(
30391
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31037
+ `SELECT e.repo AS repo, count(*) AS c
30392
31038
  FROM inspection_findings f
30393
31039
  JOIN audit_events e ON e.id = f.audit_event_id
30394
31040
  WHERE e.started_at >= :from AND e.started_at < :to
30395
31041
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30396
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30397
- AND json_extract(e.attributes, '$.repo') != ''
30398
- GROUP BY repo
30399
- ORDER BY c DESC, repo
31042
+ AND e.repo IS NOT NULL
31043
+ AND e.repo != ''
31044
+ GROUP BY e.repo
31045
+ ORDER BY c DESC, e.repo
30400
31046
  LIMIT :limit`
30401
31047
  ),
30402
31048
  { from, to: now, limit }
@@ -30458,7 +31104,7 @@ var SqliteSecurityRepository = class {
30458
31104
  `SELECT f.finding_key AS finding_key,
30459
31105
  d.rule_id AS rule_id,
30460
31106
  d.severity AS severity,
30461
- json_extract(e.attributes, '$.file_path') AS path,
31107
+ e.file_path AS path,
30462
31108
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30463
31109
  latest.resolved_at AS latest_resolved_at
30464
31110
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30511,7 +31157,7 @@ var SqliteSecurityRepository = class {
30511
31157
  };
30512
31158
 
30513
31159
  // ../../packages/persistence/src/repositories/shares.ts
30514
- import { randomUUID as randomUUID9 } from "crypto";
31160
+ import { randomUUID as randomUUID10 } from "crypto";
30515
31161
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30516
31162
  var IN_CHUNK = 500;
30517
31163
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30599,7 +31245,7 @@ function buildSummary(dest, endpoints) {
30599
31245
  callSiteCount,
30600
31246
  transports: distinctTransports(transports),
30601
31247
  dataClasses: distinctDataClasses(dataClasses),
30602
- review: buildReviewInfo(dest.trust, transports),
31248
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30603
31249
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30604
31250
  endpoints: endpoints.map(toEndpointSummary)
30605
31251
  };
@@ -30626,7 +31272,7 @@ function buildDetail(dest, endpoints, callSites) {
30626
31272
  lastSeen: new Date(lastSeenMs).toISOString(),
30627
31273
  transports: distinctTransports(transports),
30628
31274
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30629
- review: buildReviewInfo(dest.trust, transports),
31275
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30630
31276
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30631
31277
  note: dest.note,
30632
31278
  endpoints: endpoints.map((ep) => ({
@@ -30655,7 +31301,11 @@ var SqliteSharesRepository = class {
30655
31301
  FROM share_destination d
30656
31302
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30657
31303
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30658
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31304
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31305
+ AND NOT EXISTS (
31306
+ SELECT 1 FROM egress_decision_override o
31307
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31308
+ )`
30659
31309
  );
30660
31310
  const kindCounts = countBy(
30661
31311
  this.db,
@@ -30767,7 +31417,7 @@ var SqliteSharesRepository = class {
30767
31417
  (id, destination_id, host, decision, created_at, updated_at)
30768
31418
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30769
31419
  ).run({
30770
- id: randomUUID9(),
31420
+ id: randomUUID10(),
30771
31421
  destinationId,
30772
31422
  host: dest.host,
30773
31423
  decision,
@@ -30916,7 +31566,7 @@ var SqliteSharesRepository = class {
30916
31566
  let destinationId = destIds.get(hit.host);
30917
31567
  if (destinationId === void 0) {
30918
31568
  destStmt.run({
30919
- id: randomUUID9(),
31569
+ id: randomUUID10(),
30920
31570
  kind: hit.kind,
30921
31571
  name: hit.name,
30922
31572
  host: hit.host,
@@ -30932,7 +31582,7 @@ var SqliteSharesRepository = class {
30932
31582
  let endpointId = endpointIds.get(endpointKey);
30933
31583
  if (endpointId === void 0) {
30934
31584
  endpointStmt.run({
30935
- id: randomUUID9(),
31585
+ id: randomUUID10(),
30936
31586
  destinationId,
30937
31587
  method: hit.method,
30938
31588
  transport: hit.transport,
@@ -30945,7 +31595,7 @@ var SqliteSharesRepository = class {
30945
31595
  endpointIds.set(endpointKey, endpointId);
30946
31596
  }
30947
31597
  siteStmt.run({
30948
- id: randomUUID9(),
31598
+ id: randomUUID10(),
30949
31599
  endpointId,
30950
31600
  project: input2.project,
30951
31601
  projectKey: input2.projectKey,
@@ -31310,6 +31960,7 @@ function purgeSampleData(db) {
31310
31960
  }
31311
31961
 
31312
31962
  // ../../packages/persistence/src/database.ts
31963
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31313
31964
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31314
31965
  "aka.persistence.unsafeTestOnlyRawHandle"
31315
31966
  );
@@ -31357,7 +32008,7 @@ function backupLegacyStore(db, file2) {
31357
32008
  discardStore(file2, backup);
31358
32009
  return backup;
31359
32010
  }
31360
- function openAndInitialize(file2) {
32011
+ function openAndInitialize(file2, base) {
31361
32012
  let db = openWithPragmas(file2);
31362
32013
  try {
31363
32014
  if (isForeignSqliteLineage(db)) {
@@ -31370,7 +32021,7 @@ function openAndInitialize(file2) {
31370
32021
  applyMigrations(db, file2);
31371
32022
  tightenPerms(file2);
31372
32023
  const policies = new SqlitePoliciesRepository(db);
31373
- const installedPacks = new SqliteInstalledPacksRepository(db);
32024
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31374
32025
  const repositories = {
31375
32026
  events: new SqliteEventsRepository(db),
31376
32027
  findings: new SqliteFindingsRepository(db),
@@ -31406,7 +32057,7 @@ function openAndInitialize(file2) {
31406
32057
  }
31407
32058
  function openLocalDatabase(dir) {
31408
32059
  ensureDataDirSync(dir);
31409
- const file2 = join4(dir, DB_FILENAME);
32060
+ const file2 = join7(dir, DB_FILENAME);
31410
32061
  reapStalePartials(file2);
31411
32062
  const {
31412
32063
  db,
@@ -31434,7 +32085,13 @@ function openLocalDatabase(dir) {
31434
32085
  inspectionDefinitions,
31435
32086
  inspectionFindings,
31436
32087
  configInventory
31437
- } = openAndInitialize(file2);
32088
+ } = openAndInitialize(
32089
+ file2,
32090
+ // `dir` is always `<base>/data` — every caller resolves it through
32091
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32092
+ // settings/ and data/, and the pack-policy floor needs both halves.
32093
+ dirname2(dir)
32094
+ );
31438
32095
  function captureRowId(event) {
31439
32096
  return captureId(
31440
32097
  event.metadata?.sessionId ?? null,
@@ -31447,6 +32104,21 @@ function openLocalDatabase(dir) {
31447
32104
  historySync.markSynced([captureRowId(event)], atMs);
31448
32105
  });
31449
32106
  }
32107
+ function markCaptureOwed(event) {
32108
+ failOpenTransaction(db, () => {
32109
+ historySync.markCaptureOwed(captureRowId(event));
32110
+ });
32111
+ }
32112
+ function markAuditEventsDelivered(events2, atMs) {
32113
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32114
+ if (stampable.length === 0) return;
32115
+ failOpenTransaction(db, () => {
32116
+ historySync.markSynced(
32117
+ stampable.map((event) => event.id),
32118
+ atMs
32119
+ );
32120
+ });
32121
+ }
31450
32122
  function recordCapture(event, detected) {
31451
32123
  failOpenTransaction(db, () => {
31452
32124
  const sessionId = event.metadata?.sessionId;
@@ -31533,7 +32205,7 @@ function openLocalDatabase(dir) {
31533
32205
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31534
32206
  if (!definitionId) continue;
31535
32207
  inspectionFindings.insertFinding({
31536
- id: randomUUID10(),
32208
+ id: randomUUID11(),
31537
32209
  auditEventId: record2.scanEvent.id,
31538
32210
  inspectionDefinitionId: definitionId,
31539
32211
  span: finding.span,
@@ -31629,6 +32301,8 @@ function openLocalDatabase(dir) {
31629
32301
  inspectionFindings,
31630
32302
  recordCapture,
31631
32303
  markCaptureDelivered,
32304
+ markCaptureOwed,
32305
+ markAuditEventsDelivered,
31632
32306
  ensureInventory,
31633
32307
  recordConfigScan,
31634
32308
  recordProjectFiles,
@@ -31647,32 +32321,18 @@ function openLocalDatabase(dir) {
31647
32321
  };
31648
32322
  }
31649
32323
 
31650
- // ../../packages/persistence/src/file-lock.ts
31651
- import { randomUUID as randomUUID11 } from "crypto";
31652
- import {
31653
- closeSync,
31654
- existsSync as existsSync2,
31655
- openSync,
31656
- readFileSync as readFileSync2,
31657
- rmSync as rmSync5,
31658
- statSync as statSync3,
31659
- writeFileSync as writeFileSync2
31660
- } from "fs";
31661
- import { hostname as hostname3 } from "os";
31662
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31663
-
31664
32324
  // ../../packages/persistence/src/finding-key.ts
31665
32325
  import { createHash as createHash3 } from "crypto";
31666
32326
 
31667
32327
  // ../../packages/persistence/src/fingerprint.ts
31668
32328
  import { createHmac, randomBytes } from "crypto";
31669
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31670
- import { join as join5 } from "path";
32329
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32330
+ import { join as join8 } from "path";
31671
32331
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31672
32332
  var EXCEPTION_KEY_FILENAME = "exception.key";
31673
32333
  var KEY_MATERIAL_BYTES = 32;
31674
32334
  function keyFilePath(dataDir2) {
31675
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32335
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31676
32336
  }
31677
32337
  function parseKeyFile(raw) {
31678
32338
  const parsed2 = JSON.parse(raw);
@@ -31695,7 +32355,7 @@ function parseKeyFile(raw) {
31695
32355
  function readFingerprintKey(dataDir2) {
31696
32356
  let raw;
31697
32357
  try {
31698
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32358
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31699
32359
  } catch (err) {
31700
32360
  if (err.code === "ENOENT") return null;
31701
32361
  throw err instanceof Error ? err : new Error(String(err));
@@ -31705,146 +32365,12 @@ function readFingerprintKey(dataDir2) {
31705
32365
 
31706
32366
  // ../../packages/persistence/src/history-preview.ts
31707
32367
  import { existsSync as existsSync4 } from "fs";
31708
- import { join as join6 } from "path";
32368
+ import { join as join9 } from "path";
31709
32369
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31710
32370
 
31711
- // ../../packages/persistence/src/local-layout.ts
31712
- import { renameSync as renameSync3 } from "fs";
31713
- import { mkdir } from "fs/promises";
31714
- import { homedir } from "os";
31715
- import { join as join7 } from "path";
31716
- function defaultDataDir() {
31717
- return join7(homedir(), ".aka");
31718
- }
31719
- function settingsDir(base = defaultDataDir()) {
31720
- return join7(base, "settings");
31721
- }
31722
- function dataDir(base = defaultDataDir()) {
31723
- return join7(base, "data");
31724
- }
31725
- function dbPath(base = defaultDataDir()) {
31726
- return join7(dataDir(base), "aka.db");
31727
- }
31728
- function keysDir(base = defaultDataDir()) {
31729
- return join7(base, "keys");
31730
- }
31731
- async function ensureDataDir(dir = defaultDataDir()) {
31732
- await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
31733
- tightenDir(dir);
31734
- }
31735
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31736
- ensureDataDirSync(dir);
31737
- }
31738
- function migrateLegacyLayout(base = defaultDataDir()) {
31739
- const moves = [
31740
- { name: "config.json", dest: settingsDir(base) },
31741
- { name: "policy-cache.json", dest: dataDir(base) }
31742
- ];
31743
- for (const { name, dest } of moves) {
31744
- try {
31745
- ensureDataDirSync(dest);
31746
- const moved = join7(dest, name);
31747
- renameSync3(join7(base, name), moved);
31748
- tightenFile(moved);
31749
- } catch {
31750
- }
31751
- }
31752
- }
31753
-
31754
- // ../../packages/persistence/src/managed-settings.ts
31755
- import { readFileSync as readFileSync4 } from "fs";
31756
- import { posix, win32 } from "path";
31757
- function managedSettingsPaths(platform2 = process.platform) {
31758
- if (platform2 === "darwin") {
31759
- return [
31760
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31761
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31762
- ];
31763
- }
31764
- if (platform2 === "win32") {
31765
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31766
- }
31767
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31768
- }
31769
- function readManagedSettings(paths = managedSettingsPaths()) {
31770
- for (const path of paths) {
31771
- let text;
31772
- try {
31773
- text = readFileSync4(path, "utf8");
31774
- } catch {
31775
- continue;
31776
- }
31777
- const record2 = parseJsonObject(text);
31778
- if (!record2) continue;
31779
- const parsed2 = ManagedSettings.safeParse(record2);
31780
- if (parsed2.success) return parsed2.data;
31781
- }
31782
- return null;
31783
- }
31784
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31785
- if (!managed) return settings;
31786
- const { values } = managed;
31787
- const merged = { ...settings };
31788
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31789
- if (values.controlPlane !== void 0) {
31790
- merged.controlPlane = {
31791
- ...values.controlPlane,
31792
- // The administrator pinned WHICH deployment, not WHEN this machine
31793
- // joined it. Keep the user's own attach time when the endpoint is
31794
- // unchanged, so a managed machine does not appear to re-attach on every
31795
- // read; stamp a fresh one when the administrator moved it.
31796
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31797
- };
31798
- }
31799
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31800
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31801
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31802
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31803
- if (values.vaultConsent !== void 0) {
31804
- merged.vaultConsent = values.vaultConsent ? (
31805
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31806
- // at the current version otherwise.
31807
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31808
- ) : void 0;
31809
- }
31810
- if (values.modelJudgeConsent !== void 0) {
31811
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31812
- acknowledgedAt: now().toISOString(),
31813
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31814
- } : void 0;
31815
- }
31816
- return merged;
31817
- }
31818
-
31819
- // ../../packages/persistence/src/settings.ts
31820
- import { readFileSync as readFileSync5 } from "fs";
31821
- import { join as join8 } from "path";
31822
- var SETTINGS_FILENAME = "settings.json";
31823
- function readWorkspaceSettings(base = defaultDataDir()) {
31824
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31825
- }
31826
- function readUserSettings(base) {
31827
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31828
- if (!record2) return defaultWorkspaceSettings();
31829
- try {
31830
- return WorkspaceSettings.parse(record2);
31831
- } catch {
31832
- return defaultWorkspaceSettings();
31833
- }
31834
- }
31835
- function readJson(file2) {
31836
- let text;
31837
- try {
31838
- text = readFileSync5(file2, "utf8");
31839
- } catch {
31840
- return null;
31841
- }
31842
- return parseJsonObject(text) ?? null;
31843
- }
31844
-
31845
32371
  // ../../packages/persistence/src/store-symlinks.ts
31846
32372
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31847
- import { dirname as dirname2, join as join9, resolve } from "path";
32373
+ import { dirname as dirname3, join as join10, resolve } from "path";
31848
32374
  var STORE_DB = "the store database (including the prompt corpus)";
31849
32375
  var STORE_SETTINGS = "your settings file";
31850
32376
  function storeContents(home) {
@@ -31853,7 +32379,7 @@ function storeContents(home) {
31853
32379
  [settingsDir(home), STORE_SETTINGS],
31854
32380
  [dataDir(home), STORE_DB],
31855
32381
  [keysDir(home), "the vault key"],
31856
- [join9(settingsDir(home), "settings.json"), STORE_SETTINGS],
32382
+ [join10(settingsDir(home), "settings.json"), STORE_SETTINGS],
31857
32383
  [dbPath(home), STORE_DB]
31858
32384
  ]);
31859
32385
  }
@@ -31881,7 +32407,7 @@ function linkTarget(path) {
31881
32407
  try {
31882
32408
  return realpathSync(path);
31883
32409
  } catch {
31884
- return resolve(dirname2(path), readlinkSync(path));
32410
+ return resolve(dirname3(path), readlinkSync(path));
31885
32411
  }
31886
32412
  }
31887
32413
  function targetMode(path, platform2) {
@@ -31905,19 +32431,19 @@ import {
31905
32431
  // ../../packages/persistence/src/vault/key-provider.ts
31906
32432
  import { execFileSync } from "child_process";
31907
32433
  import { randomBytes as randomBytes2 } from "crypto";
31908
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
31909
- import { join as join10 } from "path";
32434
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32435
+ import { join as join11 } from "path";
31910
32436
 
31911
32437
  // ../../packages/persistence/src/vault/vault.ts
31912
32438
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
31913
32439
 
31914
32440
  // ../../packages/persistence/src/warn-era-cap.ts
31915
32441
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
31916
- import { join as join11 } from "path";
32442
+ import { join as join12 } from "path";
31917
32443
  var MARKER = "warn-era-capped";
31918
32444
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
31919
32445
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
31920
- const marker = join11(dataDir2, MARKER);
32446
+ const marker = join12(dataDir2, MARKER);
31921
32447
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
31922
32448
  const capped = db.policies.capCategoryActions();
31923
32449
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -31977,7 +32503,7 @@ function resolveProvider() {
31977
32503
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
31978
32504
  try {
31979
32505
  ensureLayoutDirSync(base);
31980
- const settingsFile = join12(settingsDir(base), "settings.json");
32506
+ const settingsFile = join13(settingsDir(base), "settings.json");
31981
32507
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
31982
32508
  } catch {
31983
32509
  }
@@ -32001,9 +32527,9 @@ function resolveProviderSafe(resolveProviderFn) {
32001
32527
  }
32002
32528
 
32003
32529
  // ../../packages/plugin-sdk/src/config-inventory.ts
32004
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32530
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32005
32531
  import { homedir as homedir2 } from "os";
32006
- import { basename as basename3, join as join14 } from "path";
32532
+ import { basename as basename3, join as join15 } from "path";
32007
32533
 
32008
32534
  // ../../packages/detections/src/egress/registry.ts
32009
32535
  var EXTRACTOR_VERSION = "1";
@@ -34786,8 +35312,8 @@ function bundledDetections() {
34786
35312
  }
34787
35313
 
34788
35314
  // ../../packages/plugin-sdk/src/repo.ts
34789
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
34790
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
35315
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
35316
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
34791
35317
 
34792
35318
  // ../../packages/plugin-sdk/src/events.ts
34793
35319
  import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
@@ -34799,8 +35325,8 @@ import { Worker } from "worker_threads";
34799
35325
 
34800
35326
  // ../../packages/plugin-sdk/src/ignore-layers.ts
34801
35327
  var import_ignore = __toESM(require_ignore(), 1);
34802
- import { readFileSync as readFileSync9 } from "fs";
34803
- import { join as join15 } from "path";
35328
+ import { readFileSync as readFileSync10 } from "fs";
35329
+ import { join as join16 } from "path";
34804
35330
 
34805
35331
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
34806
35332
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -34811,11 +35337,11 @@ import {
34811
35337
  fstatSync,
34812
35338
  mkdirSync as mkdirSync2,
34813
35339
  openSync as openSync2,
34814
- readFileSync as readFileSync10,
35340
+ readFileSync as readFileSync11,
34815
35341
  readSync,
34816
35342
  writeFileSync as writeFileSync5
34817
35343
  } from "fs";
34818
- import { join as join16 } from "path";
35344
+ import { join as join17 } from "path";
34819
35345
  var SESSION_MODEL_MARKER = "session-model";
34820
35346
  var DATE_SUFFIX = /-\d{8}$/;
34821
35347
  function normalizeModelId(model) {
@@ -34833,7 +35359,7 @@ function recordSessionModel(dataDir2, sessionId, model) {
34833
35359
  if (model === void 0 || model === "") return;
34834
35360
  try {
34835
35361
  mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
34836
- writeFileSync5(join16(dataDir2, SESSION_MODEL_MARKER), JSON.stringify({ sessionId, model }), {
35362
+ writeFileSync5(join17(dataDir2, SESSION_MODEL_MARKER), JSON.stringify({ sessionId, model }), {
34837
35363
  encoding: "utf8",
34838
35364
  mode: DATA_FILE_MODE
34839
35365
  });
@@ -34842,8 +35368,9 @@ function recordSessionModel(dataDir2, sessionId, model) {
34842
35368
  }
34843
35369
  var TAIL_BYTES = 256 * 1024;
34844
35370
  function prohibitedModelMessage(model, action) {
34845
- const subject = action === "switch" ? `Cannot switch to ${model}` : `This session is running on ${model}, which cannot be used`;
34846
- return `${subject} \u2014 your organization has prohibited this model. Switch to an approved model with /model, or ask an administrator to change its status in AKA under Govern \u2192 LLM Providers.`;
35371
+ const subject = action === "switch" ? `Cannot switch to ${model}` : action === "spawn" ? `Cannot start a subagent on ${model}` : `This session is running on ${model}, which cannot be used`;
35372
+ const remedy = action === "spawn" ? "Name an approved model on the subagent" : "Switch to an approved model with /model";
35373
+ return `${subject} \u2014 your organization has prohibited this model. ${remedy}, or ask an administrator to change its status in AKA under Govern \u2192 LLM Providers.`;
34847
35374
  }
34848
35375
  function buildModelRefusalEvent(input2) {
34849
35376
  return {
@@ -34859,22 +35386,24 @@ function buildModelRefusalEvent(input2) {
34859
35386
  // plane group refusals by the same id the prohibition was keyed on.
34860
35387
  model: input2.model,
34861
35388
  refusal_seam: input2.seam,
34862
- source_tool: input2.sourceTool
35389
+ source_tool: input2.sourceTool,
35390
+ // Omitted rather than duplicated when the caller named the id itself.
35391
+ ...input2.requestedModel === void 0 || input2.requestedModel === input2.model ? {} : { requested_model: input2.requestedModel }
34863
35392
  }
34864
35393
  };
34865
35394
  }
34866
35395
 
34867
35396
  // ../../packages/plugin-sdk/src/nudge.ts
34868
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
34869
- import { join as join17 } from "path";
35397
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35398
+ import { join as join18 } from "path";
34870
35399
 
34871
35400
  // ../../packages/plugin-sdk/src/paths.ts
34872
35401
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
34873
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
35402
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
34874
35403
 
34875
35404
  // ../../packages/plugin-sdk/src/project-files.ts
34876
35405
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
34877
- import { basename as basename5, join as join18 } from "path";
35406
+ import { basename as basename5, join as join19 } from "path";
34878
35407
 
34879
35408
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
34880
35409
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -34910,13 +35439,16 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
34910
35439
 
34911
35440
  // ../../packages/plugin-sdk/src/throttle.ts
34912
35441
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
34913
- import { join as join19 } from "path";
35442
+ import { join as join20 } from "path";
34914
35443
 
34915
35444
  // src/hooks/model-switch-run.ts
34916
35445
  import { randomUUID as randomUUID16 } from "crypto";
34917
35446
 
34918
35447
  // src/hooks/model-guard.ts
34919
35448
  import { randomUUID as randomUUID15 } from "crypto";
35449
+ import { readFileSync as readFileSync13, statSync as statSync9 } from "fs";
35450
+ import { homedir as homedir3 } from "os";
35451
+ import { dirname as dirname6, join as join21 } from "path";
34920
35452
  function decidePreModelSwitch(toModel, prohibitedModels) {
34921
35453
  if (toModel === void 0 || toModel === "") return null;
34922
35454
  if (!isModelProhibited(toModel, prohibitedModels)) return null;
@@ -35016,8 +35548,8 @@ function emit(output2) {
35016
35548
  }
35017
35549
 
35018
35550
  // src/hooks/store-health.ts
35019
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "fs";
35020
- import { dirname as dirname5, join as join26 } from "path";
35551
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "fs";
35552
+ import { dirname as dirname7, join as join28 } from "path";
35021
35553
 
35022
35554
  // ../../packages/plugin-runtime/src/attached/egress-wire.ts
35023
35555
  import { createHash as createHash5 } from "crypto";
@@ -35056,6 +35588,307 @@ function toEgressIngestRequest(input2) {
35056
35588
  };
35057
35589
  }
35058
35590
 
35591
+ // ../../packages/remote/src/http.ts
35592
+ import { request as httpRequest } from "http";
35593
+ import { request as httpsRequest } from "https";
35594
+ var DEFAULT_TIMEOUT_MS = 1e4;
35595
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
35596
+ var RemoteRequestError = class extends Error {
35597
+ constructor(status) {
35598
+ super(`control-plane request failed with status ${String(status)}`);
35599
+ this.status = status;
35600
+ this.name = "RemoteRequestError";
35601
+ }
35602
+ status;
35603
+ };
35604
+ var RemoteRouteAbsent = class extends Error {
35605
+ constructor(route) {
35606
+ super(`control plane does not serve ${route}`);
35607
+ this.route = route;
35608
+ this.name = "RemoteRouteAbsent";
35609
+ }
35610
+ route;
35611
+ };
35612
+ var RemoteRequestInvalid = class extends Error {
35613
+ constructor(route, cause) {
35614
+ super(`refusing to send a malformed body to ${route}`);
35615
+ this.cause = cause;
35616
+ this.name = "RemoteRequestInvalid";
35617
+ }
35618
+ cause;
35619
+ };
35620
+ var RemoteResponseInvalid = class extends Error {
35621
+ constructor(route, detail) {
35622
+ super(`control plane answered ${route} with ${detail}`);
35623
+ this.name = "RemoteResponseInvalid";
35624
+ }
35625
+ };
35626
+ var RemoteTransportError = class extends Error {
35627
+ /**
35628
+ * The status the peer sent, when headers arrived and only the BODY was
35629
+ * refused.
35630
+ *
35631
+ * Undefined for the ordinary case this class was written for — no answer at
35632
+ * all. It exists because two paths reject after a status has already been
35633
+ * delivered: an oversized body and an aborted response. Discarding it there
35634
+ * reported a deployment answering 401 with a verbose body as a network
35635
+ * outage, which sends the reader to look at their network instead of their
35636
+ * credential.
35637
+ */
35638
+ constructor(reason, status) {
35639
+ super(`control-plane request did not complete: ${reason}`);
35640
+ this.status = status;
35641
+ this.name = "RemoteTransportError";
35642
+ }
35643
+ status;
35644
+ };
35645
+ async function send(options) {
35646
+ const url2 = new URL(options.url);
35647
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35648
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
35649
+ const requestOptions = {
35650
+ method: options.method,
35651
+ headers: {
35652
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
35653
+ // last they win, and two of the values below are ones no caller may
35654
+ // replace: `x-api-key` is the credential, and `content-length` is the
35655
+ // byte count that stops a multi-byte body being truncated by the
35656
+ // receiver. `SendOptions.headers` is a free-form record on an exported
35657
+ // function, so "no caller does that today" is not the guarantee to rely
35658
+ // on. The one header any caller actually passes — `if-none-match` on the
35659
+ // conditional GET — is untouched by this order.
35660
+ ...options.headers,
35661
+ // The credential. One header, matching what the deployment authenticates
35662
+ // on; a second copy in an `Authorization` header would be one more place
35663
+ // it can be logged by an intermediary for no gain.
35664
+ //
35665
+ // Spread conditionally rather than assigned as `undefined`: Node's header
35666
+ // handling and `content-length` bookkeeping treat a present-but-undefined
35667
+ // key differently from an absent one, and "the header is not there" is
35668
+ // the property the attach flow needs.
35669
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
35670
+ accept: "application/json",
35671
+ ...options.body === void 0 ? {} : {
35672
+ "content-type": "application/json",
35673
+ // Byte length, not string length: a multi-byte body sent with a
35674
+ // character count is truncated by the receiver.
35675
+ "content-length": String(Buffer.byteLength(options.body))
35676
+ }
35677
+ }
35678
+ };
35679
+ return new Promise((resolve2, reject) => {
35680
+ let settled = false;
35681
+ const fail = (reason, status) => {
35682
+ if (settled) return;
35683
+ settled = true;
35684
+ reject(new RemoteTransportError(reason, status));
35685
+ };
35686
+ const req = send_(url2, requestOptions, (res) => {
35687
+ const chunks = [];
35688
+ let size = 0;
35689
+ res.on("data", (chunk) => {
35690
+ size += chunk.length;
35691
+ if (size > MAX_RESPONSE_BYTES) {
35692
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
35693
+ res.destroy();
35694
+ req.destroy();
35695
+ return;
35696
+ }
35697
+ chunks.push(chunk);
35698
+ });
35699
+ res.on("aborted", () => {
35700
+ fail("the response was aborted", res.statusCode);
35701
+ });
35702
+ res.on("end", () => {
35703
+ if (settled) return;
35704
+ settled = true;
35705
+ resolve2({
35706
+ status: res.statusCode ?? 0,
35707
+ headers: res.headers,
35708
+ body: Buffer.concat(chunks).toString("utf8")
35709
+ });
35710
+ });
35711
+ });
35712
+ const deadline = setTimeout(() => {
35713
+ fail(`no response within ${String(timeoutMs)}ms`);
35714
+ req.destroy();
35715
+ }, timeoutMs);
35716
+ deadline.unref();
35717
+ req.on("upgrade", (_res, socket) => {
35718
+ fail("the deployment answered with a protocol upgrade");
35719
+ socket.destroy();
35720
+ });
35721
+ req.on("close", () => {
35722
+ fail("the connection closed before a response was read");
35723
+ clearTimeout(deadline);
35724
+ });
35725
+ req.on("error", (err) => {
35726
+ fail(err.message);
35727
+ });
35728
+ if (options.body !== void 0) req.write(options.body);
35729
+ req.end();
35730
+ });
35731
+ }
35732
+
35733
+ // ../../packages/remote/src/client.ts
35734
+ var ROUTES = {
35735
+ events: "/v1/events",
35736
+ auditEvents: "/v1/audit-events",
35737
+ auditEventsBatch: "/v1/audit-events/batch",
35738
+ inventory: "/v1/inventory",
35739
+ storePosture: "/v1/store-posture",
35740
+ policyBundle: "/v1/policy-bundle",
35741
+ whoami: "/v1/plugin/whoami",
35742
+ shares: "/v1/shares",
35743
+ commands: "/v1/plugin/commands"
35744
+ };
35745
+ function ackRoute(id) {
35746
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
35747
+ }
35748
+ function headerValue(response, name) {
35749
+ const raw = response.headers[name];
35750
+ if (raw === void 0) return void 0;
35751
+ return Array.isArray(raw) ? raw[0] : raw;
35752
+ }
35753
+ function okBody(response) {
35754
+ if (response.status < 200 || response.status >= 300) {
35755
+ throw new RemoteRequestError(response.status);
35756
+ }
35757
+ return response.body;
35758
+ }
35759
+ function parsed(schema, body, route) {
35760
+ let json2;
35761
+ try {
35762
+ json2 = JSON.parse(body);
35763
+ } catch {
35764
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
35765
+ }
35766
+ const result = schema.safeParse(json2);
35767
+ if (!result.success) {
35768
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
35769
+ }
35770
+ return result.data;
35771
+ }
35772
+ function withoutTrailingSlashes(endpoint) {
35773
+ let end = endpoint.length;
35774
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
35775
+ return endpoint.slice(0, end);
35776
+ }
35777
+ var SLASH = "/".charCodeAt(0);
35778
+ function createRemoteClient(options) {
35779
+ const base = withoutTrailingSlashes(options.endpoint);
35780
+ const url2 = (route) => `${base}${route}`;
35781
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
35782
+ const sendOne = async (event) => {
35783
+ const validated = RecordAuditEventRequest.safeParse(event);
35784
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
35785
+ const response = await send({
35786
+ ...common,
35787
+ method: "POST",
35788
+ url: url2(ROUTES.auditEvents),
35789
+ body: JSON.stringify(validated.data)
35790
+ });
35791
+ okBody(response);
35792
+ };
35793
+ return {
35794
+ async ingestEvents(batch) {
35795
+ const response = await send({
35796
+ ...common,
35797
+ method: "POST",
35798
+ url: url2(ROUTES.events),
35799
+ body: JSON.stringify(batch)
35800
+ });
35801
+ return parsed(IngestAck, okBody(response), ROUTES.events);
35802
+ },
35803
+ async ingestInventory(context) {
35804
+ const response = await send({
35805
+ ...common,
35806
+ method: "POST",
35807
+ url: url2(ROUTES.inventory),
35808
+ body: JSON.stringify(context)
35809
+ });
35810
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
35811
+ },
35812
+ async recordAuditEvent(event) {
35813
+ await sendOne(event);
35814
+ },
35815
+ async recordAuditEvents(events, opts) {
35816
+ const validated = RecordAuditEventBatch.safeParse({ events });
35817
+ if (!validated.success) {
35818
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
35819
+ }
35820
+ const response = await send({
35821
+ ...common,
35822
+ method: "POST",
35823
+ url: url2(ROUTES.auditEventsBatch),
35824
+ body: JSON.stringify(validated.data)
35825
+ });
35826
+ if (response.status === 404) {
35827
+ if (opts?.fallbackToSingleEvents !== true) {
35828
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
35829
+ }
35830
+ for (const event of validated.data.events) await sendOne(event);
35831
+ return { accepted: validated.data.events.length };
35832
+ }
35833
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
35834
+ },
35835
+ async reportStorePosture(snapshot) {
35836
+ const response = await send({
35837
+ ...common,
35838
+ method: "POST",
35839
+ url: url2(ROUTES.storePosture),
35840
+ body: JSON.stringify(snapshot)
35841
+ });
35842
+ okBody(response);
35843
+ },
35844
+ async getPolicyBundle(etag) {
35845
+ const response = await send({
35846
+ ...common,
35847
+ method: "GET",
35848
+ url: url2(ROUTES.policyBundle),
35849
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
35850
+ });
35851
+ if (response.status === 304) {
35852
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
35853
+ }
35854
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
35855
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
35856
+ },
35857
+ async whoami() {
35858
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
35859
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
35860
+ },
35861
+ async recordProjectEgress(request) {
35862
+ const validated = EgressIngestRequest.safeParse(request);
35863
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
35864
+ const response = await send({
35865
+ ...common,
35866
+ method: "POST",
35867
+ url: url2(ROUTES.shares),
35868
+ body: JSON.stringify(validated.data)
35869
+ });
35870
+ okBody(response);
35871
+ },
35872
+ async pollCommand() {
35873
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
35874
+ if (response.status === 404) return null;
35875
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
35876
+ },
35877
+ async ackCommand(id, body) {
35878
+ const validated = DeviceCommandAckBody.safeParse(body);
35879
+ const route = ackRoute(id);
35880
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
35881
+ const response = await send({
35882
+ ...common,
35883
+ method: "POST",
35884
+ url: url2(route),
35885
+ body: JSON.stringify(validated.data)
35886
+ });
35887
+ okBody(response);
35888
+ }
35889
+ };
35890
+ }
35891
+
35059
35892
  // ../../packages/plugin-runtime/src/attached/failure.ts
35060
35893
  function statusOf(err) {
35061
35894
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -35074,12 +35907,27 @@ function classifyFailure(err) {
35074
35907
  }
35075
35908
  }
35076
35909
 
35910
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
35911
+ var REQUEST_TIMEOUT_MS = 2e3;
35912
+ function withTimeout(promise2, ms) {
35913
+ let timer;
35914
+ const timeout = new Promise((_, reject) => {
35915
+ timer = setTimeout(() => {
35916
+ reject(new Error("attached gateway request timed out"));
35917
+ }, ms);
35918
+ });
35919
+ promise2.catch(() => void 0);
35920
+ return Promise.race([promise2, timeout]).finally(() => {
35921
+ clearTimeout(timer);
35922
+ });
35923
+ }
35924
+
35077
35925
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
35078
- import { readFileSync as readFileSync12 } from "fs";
35079
- import { join as join20 } from "path";
35926
+ import { readFileSync as readFileSync14 } from "fs";
35927
+ import { join as join22 } from "path";
35080
35928
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
35081
35929
  function forwardDropsPath(dataDir2) {
35082
- return join20(dataDir2, FORWARD_DROPS_FILENAME);
35930
+ return join22(dataDir2, FORWARD_DROPS_FILENAME);
35083
35931
  }
35084
35932
  function recordForwardDrops(dataDir2, count, nowMs) {
35085
35933
  if (count <= 0) return;
@@ -35097,7 +35945,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
35097
35945
  }
35098
35946
  function readForwardDrops(dataDir2) {
35099
35947
  try {
35100
- const parsed2 = JSON.parse(readFileSync12(forwardDropsPath(dataDir2), "utf8"));
35948
+ const parsed2 = JSON.parse(readFileSync14(forwardDropsPath(dataDir2), "utf8"));
35101
35949
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
35102
35950
  const record2 = parsed2;
35103
35951
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -35115,29 +35963,19 @@ function readForwardDrops(dataDir2) {
35115
35963
 
35116
35964
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
35117
35965
  import { randomUUID as randomUUID17 } from "crypto";
35118
- import { readFileSync as readFileSync13 } from "fs";
35966
+ import { readFileSync as readFileSync15 } from "fs";
35119
35967
  import { readFile, rename, writeFile } from "fs/promises";
35120
- import { join as join21 } from "path";
35121
-
35122
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
35123
- var REQUEST_TIMEOUT_MS = 2e3;
35124
- function withTimeout(promise2, ms) {
35125
- let timer;
35126
- const timeout = new Promise((_, reject) => {
35127
- timer = setTimeout(() => {
35128
- reject(new Error("attached gateway request timed out"));
35129
- }, ms);
35130
- });
35131
- promise2.catch(() => void 0);
35132
- return Promise.race([promise2, timeout]).finally(() => {
35133
- clearTimeout(timer);
35134
- });
35135
- }
35136
-
35137
- // ../../packages/plugin-runtime/src/attached/forward-policy.ts
35968
+ import { join as join23 } from "path";
35138
35969
  function isInvalidRequest(err) {
35139
35970
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
35140
35971
  }
35972
+ function isRouteAbsent(err) {
35973
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
35974
+ }
35975
+ function isServerRejection(err) {
35976
+ const status = statusOf(err);
35977
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
35978
+ }
35141
35979
  var FORWARD_BUDGET_MS = 1500;
35142
35980
  var DECISION_PATH_BUDGET_MS = 800;
35143
35981
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -35165,7 +36003,7 @@ function parseBreakerState(raw, nowMs) {
35165
36003
  }
35166
36004
  function createForwardPolicy(deps) {
35167
36005
  const now = deps.now ?? (() => Date.now());
35168
- const file2 = join21(deps.dir, STATE_FILENAME);
36006
+ const file2 = join23(deps.dir, STATE_FILENAME);
35169
36007
  let state = null;
35170
36008
  let loading = null;
35171
36009
  async function readState() {
@@ -35205,6 +36043,20 @@ function createForwardPolicy(deps) {
35205
36043
  } catch {
35206
36044
  current = { ...CLOSED };
35207
36045
  }
36046
+ const restoreOpenedAtMs = (openedAtMs) => persist({
36047
+ consecutiveFailures: current.consecutiveFailures,
36048
+ openedAtMs,
36049
+ lastFailure: current.lastFailure
36050
+ });
36051
+ const recordFailure = (cause) => {
36052
+ const failures = current.consecutiveFailures + 1;
36053
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
36054
+ return persist({
36055
+ consecutiveFailures: failures,
36056
+ openedAtMs: shouldOpen ? now() : null,
36057
+ lastFailure: cause
36058
+ });
36059
+ };
35208
36060
  const at = now();
35209
36061
  if (current.openedAtMs !== null) {
35210
36062
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -35223,15 +36075,20 @@ function createForwardPolicy(deps) {
35223
36075
  }
35224
36076
  return { ok: true, value };
35225
36077
  } catch (err) {
35226
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
36078
+ if (isInvalidRequest(err)) {
36079
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
36080
+ return { ok: false, reason: "invalid-request" };
36081
+ }
36082
+ if (isRouteAbsent(err)) {
36083
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
36084
+ return { ok: false, reason: "route-absent" };
36085
+ }
36086
+ if (isServerRejection(err)) {
36087
+ await recordFailure("unreachable");
36088
+ return { ok: false, reason: "rejected" };
36089
+ }
35227
36090
  const reason = classifyFailure(err);
35228
- const failures = current.consecutiveFailures + 1;
35229
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
35230
- await persist({
35231
- consecutiveFailures: failures,
35232
- openedAtMs: shouldOpen ? now() : null,
35233
- lastFailure: reason
35234
- });
36091
+ await recordFailure(reason);
35235
36092
  return { ok: false, reason };
35236
36093
  }
35237
36094
  }
@@ -35239,13 +36096,11 @@ function createForwardPolicy(deps) {
35239
36096
  }
35240
36097
 
35241
36098
  // ../../packages/plugin-runtime/src/attached/gateway.ts
35242
- var ACTION_STRENGTH = {
35243
- allow: 0,
35244
- log: 1,
35245
- warn: 2,
35246
- redact: 3,
35247
- block: 4
35248
- };
36099
+ function strongerOf(a, b) {
36100
+ if (a === null) return b;
36101
+ if (b === null) return a;
36102
+ return strongerAction(a, b);
36103
+ }
35249
36104
  function ruleCategoryMap(wireRules, localRules) {
35250
36105
  const map2 = /* @__PURE__ */ new Map();
35251
36106
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -35255,11 +36110,6 @@ function ruleCategoryMap(wireRules, localRules) {
35255
36110
  }
35256
36111
  return map2;
35257
36112
  }
35258
- function strongerOf(a, b) {
35259
- if (a === null) return b;
35260
- if (b === null) return a;
35261
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
35262
- }
35263
36113
  function policyKey(policy) {
35264
36114
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
35265
36115
  }
@@ -35278,7 +36128,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35278
36128
  const floor = floorFor(policy, categoryByRuleId);
35279
36129
  remoteCategoryAction.set(
35280
36130
  policy.target.category,
35281
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
36131
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
35282
36132
  );
35283
36133
  }
35284
36134
  for (const policy of localPolicies) {
@@ -35295,7 +36145,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35295
36145
  }
35296
36146
  merged.set(
35297
36147
  key,
35298
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
36148
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
35299
36149
  );
35300
36150
  }
35301
36151
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -35315,13 +36165,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
35315
36165
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
35316
36166
  }
35317
36167
  const effectiveFloor = strongerOf(floor, localFloor);
35318
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
36168
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
35319
36169
  const existing = merged.get(key);
35320
36170
  if (existing === void 0) {
35321
36171
  merged.set(key, clamped);
35322
36172
  continue;
35323
36173
  }
35324
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
36174
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
35325
36175
  merged.set(key, clamped);
35326
36176
  }
35327
36177
  }
@@ -35354,6 +36204,8 @@ var AttachedDataGateway = class {
35354
36204
  );
35355
36205
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
35356
36206
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
36207
+ } else {
36208
+ this.deps.local.markCaptureOwed(record2.event);
35357
36209
  }
35358
36210
  }
35359
36211
  async ensureInventory(ctx) {
@@ -35390,9 +36242,10 @@ var AttachedDataGateway = class {
35390
36242
  // a retried tool_call, exactly this path — can never stomp a populated row.
35391
36243
  async recordAuditEvent(event) {
35392
36244
  await this.deps.local.recordAuditEvent(event);
35393
- await this.deps.forward.run(
36245
+ const forwarded = await this.deps.forward.run(
35394
36246
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
35395
36247
  );
36248
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
35396
36249
  }
35397
36250
  // Attached `llm_call` is written locally by the inner gateway, then routed to
35398
36251
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -35401,44 +36254,170 @@ var AttachedDataGateway = class {
35401
36254
  // which would write the event to the local store a second time.
35402
36255
  async recordLlmCall(input2) {
35403
36256
  await this.deps.local.recordLlmCall(input2);
35404
- await this.deps.forward.run(
35405
- () => this.deps.client.recordAuditEvent(
35406
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
35407
- )
36257
+ const event = llmAuditEvent(input2);
36258
+ const forwarded = await this.deps.forward.run(
36259
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
35408
36260
  );
36261
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
35409
36262
  }
35410
36263
  /**
35411
- * Forward one batch, item by item, under ONE aggregate deadline.
36264
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
36265
+ *
36266
+ * This used to send one HTTP request per event, which is what made the batch
36267
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
36268
+ * away everything after them. The same rows now cross 50 at a time over
36269
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
36270
+ * used — so the same budget admits ~750. The wire cap is the server's own
36271
+ * constant, sized against server cost, and the client REFUSES a longer array
36272
+ * client-side, so the chunking here is not a convention.
35412
36273
  *
35413
- * Per-item budgets bound each request and nothing bounded their sum see
35414
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
35415
- * rather than sent: the local write has already succeeded, so every caller
35416
- * has a correct result to return, and a drop is the outcome this path is
35417
- * built to accept (G8) where a blown hook timeout is not.
36274
+ * Still serial, and still for the original reason: firing N requests at once
36275
+ * would trade a latency problem for a burst the plane's per-key rate limiting
36276
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
36277
+ * the fix; more concurrent ones is not.
35418
36278
  *
35419
- * Serial rather than concurrent on purpose. Firing N requests at once would
35420
- * trade a latency problem for a burst the plane's own per-key rate limiting
35421
- * would answer with the refusals the breaker then counts.
36279
+ * When the deadline passes the remainder is dropped rather than sent: the
36280
+ * local write has already succeeded, so every caller has a correct result to
36281
+ * return. What is dropped is COUNTED, everywhere it can happen — this path
36282
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
36283
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
36284
+ * the breaker closed, renders a healthy block, and discards the tail of every
36285
+ * batch indefinitely. The SAME tally also covers a single that fails inside
36286
+ * the per-item retry below — the breaker opening mid-retry is a failure the
36287
+ * breaker's own state DOES capture, but the events still in this chunk once
36288
+ * that happens are neither delivered nor otherwise counted anywhere, which is
36289
+ * the same invisibility with a different cause.
35422
36290
  *
35423
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
35424
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
35425
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
35426
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
35427
- * plane produces no failures, keeps the breaker closed, renders a healthy
35428
- * block, and discards the tail of every batch indefinitely.
36291
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
36292
+ * single-event ack and at fifty times the blast radius here:
36293
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
36294
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
36295
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
36296
+ * delivered and never re-offer the twenty the plane did not take. So success
36297
+ * is checked against `chunk.length`; anything short of it falls into the same
36298
+ * per-item pass as a refused chunk, which is the only way to recover the
36299
+ * rows that did not land, since the ack carries no per-row verdict to
36300
+ * resend by.
36301
+ *
36302
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
36303
+ * no-op rather than a second cost — an assumption this file cannot verify.
36304
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
36305
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
36306
+ * the batch size as the invariant `recordCapture` reads), so whether a
36307
+ * duplicate counts toward THIS route's `accepted` is not expressed
36308
+ * anywhere in this repo. If it follows its sibling's convention and does
36309
+ * NOT, a chunk containing even one already-delivered row — the ordinary
36310
+ * consequence of a lost stamp, which this file already treats as cheap —
36311
+ * answers short forever and enters the per-item pass on every pass it is
36312
+ * offered again. The cost of that is bounded rather than silent: the
36313
+ * pass converges (every row lands and stamps), so it is one wasted round
36314
+ * of singles rather than a stall, and it errs toward an extra resend
36315
+ * rather than toward the lost row the alternative risks.
36316
+ *
36317
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
36318
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
36319
+ * settles none — which is why the whole chunk is stamped together on a FULL
36320
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
36321
+ * treatment, alongside a short accept, and all are re-sent one event at a
36322
+ * time:
36323
+ *
36324
+ * `invalid-request` a chunk the client refused to send at all. One malformed
36325
+ * event would otherwise cost the 49 good ones beside it —
36326
+ * a new way to lose data introduced by the very change
36327
+ * meant to stop losing it.
36328
+ * `route-absent` a deployment that predates the batch route. The
36329
+ * single-event route is the one it serves, and re-sending
36330
+ * here rather than inside the client is what gives each
36331
+ * request its own budget instead of 50 inside one.
36332
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
36333
+ * a 4xx body refusal from schema drift on the other side
36334
+ * of the wire. Settlement is batch-atomic on this reason
36335
+ * exactly as on the others, so leaving it out would cost
36336
+ * the whole chunk for one event the DEPLOYMENT considers
36337
+ * malformed, where the per-item form cost only that one.
36338
+ *
36339
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
36340
+ * chunk, and re-sending it item by item would just spend the budget failing 50
36341
+ * more times — for those, the blast radius stays exactly what it was before
36342
+ * batching.
35429
36343
  */
35430
36344
  async forwardBatch(inputs, toEvent) {
35431
36345
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
35432
- for (let i = 0; i < inputs.length; i += 1) {
35433
- const now = Date.now();
35434
- if (now >= deadline) {
35435
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
35436
- return;
36346
+ const delivered = [];
36347
+ try {
36348
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
36349
+ const now = Date.now();
36350
+ if (now >= deadline) {
36351
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
36352
+ return;
36353
+ }
36354
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
36355
+ const forwarded = await this.deps.forward.run(
36356
+ () => this.deps.client.recordAuditEvents(
36357
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
36358
+ )
36359
+ );
36360
+ if (forwarded.ok) {
36361
+ if (forwarded.value.accepted === chunk.length) {
36362
+ delivered.push(...chunk);
36363
+ continue;
36364
+ }
36365
+ } else if (
36366
+ // THREE reasons are worth a second pass, one at a time, and they are
36367
+ // the three settled BEFORE the control plane refused anything, or
36368
+ // (for `rejected`) refused the BODY rather than the connection.
36369
+ //
36370
+ // `invalid-request` — the CLIENT refused the body before any request
36371
+ // went out: a defect in one event, not an outage. Re-sending singly
36372
+ // isolates the bad one instead of charging its 49 neighbours for it.
36373
+ //
36374
+ // `route-absent` — the deployment predates the batch route and serves
36375
+ // only the single-event one. The retry IS the compatibility path, and
36376
+ // it has to live HERE rather than inside the client: each single gets
36377
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
36378
+ // fallback would spend 50 sequential round trips inside the ONE
36379
+ // budget wrapping this call — turning a working older deployment into
36380
+ // a timeout, three of those into an open breaker, and every row into
36381
+ // a silent drop while the status surface called an answering
36382
+ // deployment down.
36383
+ //
36384
+ // `rejected` — the deployment's own 4xx refusal of the body, the
36385
+ // server-side twin of `invalid-request`: isolating it the same way
36386
+ // costs one event instead of the whole chunk for a defect the
36387
+ // deployment considers local to one row.
36388
+ //
36389
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
36390
+ // the whole chunk; re-sending it item by item would just spend the
36391
+ // budget failing 50 more times.
36392
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
36393
+ ) {
36394
+ continue;
36395
+ }
36396
+ for (const [j, event] of chunk.entries()) {
36397
+ const at = Date.now();
36398
+ if (at >= deadline) {
36399
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
36400
+ return;
36401
+ }
36402
+ const single = await this.deps.forward.run(
36403
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
36404
+ );
36405
+ if (single.ok) {
36406
+ delivered.push(event);
36407
+ continue;
36408
+ }
36409
+ if (single.reason === "breaker-open") {
36410
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
36411
+ return;
36412
+ }
36413
+ recordForwardDrops(this.deps.dataDir, 1, at);
36414
+ }
36415
+ }
36416
+ } finally {
36417
+ try {
36418
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
36419
+ } catch {
35437
36420
  }
35438
- const input2 = inputs[i];
35439
- await this.deps.forward.run(
35440
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
35441
- );
35442
36421
  }
35443
36422
  }
35444
36423
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -35481,9 +36460,10 @@ var AttachedDataGateway = class {
35481
36460
  // local store.
35482
36461
  async recordConfigScan(record2) {
35483
36462
  await this.deps.local.recordConfigScan(record2);
35484
- await this.deps.forward.run(
36463
+ const forwarded = await this.deps.forward.run(
35485
36464
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
35486
36465
  );
36466
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
35487
36467
  }
35488
36468
  async recordBlockedDetection(entry) {
35489
36469
  return this.deps.local.recordBlockedDetection(entry);
@@ -35617,6 +36597,18 @@ var AttachedDataGateway = class {
35617
36597
  // exactly what it did, leaving the whole control inert on every device
35618
36598
  // while every test around it stayed green.
35619
36599
  prohibitedModels: cached2.prohibitedModels
36600
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
36601
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
36602
+ // it emits, so an 'authored' policy arriving from the control plane
36603
+ // keeps that marker even where the clamp rebuilds it with a stronger
36604
+ // action. The device reads it in exactly one direction — the rules such a
36605
+ // policy targets are not locally re-assignable — so it sits on the
36606
+ // `prohibitedModels` side of the line for the same reason that field
36607
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
36608
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
36609
+ // would be the silent failure rather than the safe one — the action would
36610
+ // still be enforced while the local override the organization authored
36611
+ // away quietly came back.
35620
36612
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
35621
36613
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
35622
36614
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -35656,10 +36648,10 @@ var AttachedDataGateway = class {
35656
36648
  //
35657
36649
  // Implementing these is what actually closes the skipped-local-maintenance
35658
36650
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
35659
- // any object carrying all five, so the composite qualifies and SessionStart
36651
+ // any object carrying them all, so the composite qualifies and SessionStart
35660
36652
  // runs maintenance on the device's real store.
35661
36653
  //
35662
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
36654
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
35663
36655
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
35664
36656
  // return value directly; declaring them `async` here would hand those call
35665
36657
  // sites a Promise and silently break both.
@@ -35682,9 +36674,15 @@ var AttachedDataGateway = class {
35682
36674
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
35683
36675
  // gives: `recordCapture` calls it after the forward has already settled, on a
35684
36676
  // path that has nothing left to await.
36677
+ markCaptureOwed(event) {
36678
+ this.deps.local.markCaptureOwed(event);
36679
+ }
35685
36680
  markCaptureDelivered(event, atMs) {
35686
36681
  this.deps.local.markCaptureDelivered(event, atMs);
35687
36682
  }
36683
+ markAuditEventsDelivered(events, atMs) {
36684
+ this.deps.local.markAuditEventsDelivered(events, atMs);
36685
+ }
35688
36686
  };
35689
36687
  function reKeyForForward(event, remote) {
35690
36688
  if (remote === null) {
@@ -35727,281 +36725,17 @@ function toolAuditEvent(input2) {
35727
36725
  }
35728
36726
 
35729
36727
  // ../../packages/plugin-runtime/src/attached/history-state.ts
35730
- import { readFileSync as readFileSync14 } from "fs";
35731
- import { join as join22 } from "path";
36728
+ import { readFileSync as readFileSync16 } from "fs";
36729
+ import { join as join24 } from "path";
35732
36730
 
35733
36731
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
35734
36732
  import { createHash as createHash6 } from "crypto";
35735
36733
  import { hostname as hostname5 } from "os";
35736
36734
 
35737
- // ../../packages/remote/src/http.ts
35738
- import { request as httpRequest } from "http";
35739
- import { request as httpsRequest } from "https";
35740
- var DEFAULT_TIMEOUT_MS = 1e4;
35741
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
35742
- var RemoteRequestError = class extends Error {
35743
- constructor(status) {
35744
- super(`control-plane request failed with status ${String(status)}`);
35745
- this.status = status;
35746
- this.name = "RemoteRequestError";
35747
- }
35748
- status;
35749
- };
35750
- var RemoteRequestInvalid = class extends Error {
35751
- constructor(route, cause) {
35752
- super(`refusing to send a malformed body to ${route}`);
35753
- this.cause = cause;
35754
- this.name = "RemoteRequestInvalid";
35755
- }
35756
- cause;
35757
- };
35758
- var RemoteResponseInvalid = class extends Error {
35759
- constructor(route, detail) {
35760
- super(`control plane answered ${route} with ${detail}`);
35761
- this.name = "RemoteResponseInvalid";
35762
- }
35763
- };
35764
- var RemoteTransportError = class extends Error {
35765
- /**
35766
- * The status the peer sent, when headers arrived and only the BODY was
35767
- * refused.
35768
- *
35769
- * Undefined for the ordinary case this class was written for — no answer at
35770
- * all. It exists because two paths reject after a status has already been
35771
- * delivered: an oversized body and an aborted response. Discarding it there
35772
- * reported a deployment answering 401 with a verbose body as a network
35773
- * outage, which sends the reader to look at their network instead of their
35774
- * credential.
35775
- */
35776
- constructor(reason, status) {
35777
- super(`control-plane request did not complete: ${reason}`);
35778
- this.status = status;
35779
- this.name = "RemoteTransportError";
35780
- }
35781
- status;
35782
- };
35783
- async function send(options) {
35784
- const url2 = new URL(options.url);
35785
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35786
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
35787
- const requestOptions = {
35788
- method: options.method,
35789
- headers: {
35790
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
35791
- // last they win, and two of the values below are ones no caller may
35792
- // replace: `x-api-key` is the credential, and `content-length` is the
35793
- // byte count that stops a multi-byte body being truncated by the
35794
- // receiver. `SendOptions.headers` is a free-form record on an exported
35795
- // function, so "no caller does that today" is not the guarantee to rely
35796
- // on. The one header any caller actually passes — `if-none-match` on the
35797
- // conditional GET — is untouched by this order.
35798
- ...options.headers,
35799
- // The credential. One header, matching what the deployment authenticates
35800
- // on; a second copy in an `Authorization` header would be one more place
35801
- // it can be logged by an intermediary for no gain.
35802
- //
35803
- // Spread conditionally rather than assigned as `undefined`: Node's header
35804
- // handling and `content-length` bookkeeping treat a present-but-undefined
35805
- // key differently from an absent one, and "the header is not there" is
35806
- // the property the attach flow needs.
35807
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
35808
- accept: "application/json",
35809
- ...options.body === void 0 ? {} : {
35810
- "content-type": "application/json",
35811
- // Byte length, not string length: a multi-byte body sent with a
35812
- // character count is truncated by the receiver.
35813
- "content-length": String(Buffer.byteLength(options.body))
35814
- }
35815
- }
35816
- };
35817
- return new Promise((resolve2, reject) => {
35818
- let settled = false;
35819
- const fail = (reason, status) => {
35820
- if (settled) return;
35821
- settled = true;
35822
- reject(new RemoteTransportError(reason, status));
35823
- };
35824
- const req = send_(url2, requestOptions, (res) => {
35825
- const chunks = [];
35826
- let size = 0;
35827
- res.on("data", (chunk) => {
35828
- size += chunk.length;
35829
- if (size > MAX_RESPONSE_BYTES) {
35830
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
35831
- res.destroy();
35832
- req.destroy();
35833
- return;
35834
- }
35835
- chunks.push(chunk);
35836
- });
35837
- res.on("aborted", () => {
35838
- fail("the response was aborted", res.statusCode);
35839
- });
35840
- res.on("end", () => {
35841
- if (settled) return;
35842
- settled = true;
35843
- resolve2({
35844
- status: res.statusCode ?? 0,
35845
- headers: res.headers,
35846
- body: Buffer.concat(chunks).toString("utf8")
35847
- });
35848
- });
35849
- });
35850
- const deadline = setTimeout(() => {
35851
- fail(`no response within ${String(timeoutMs)}ms`);
35852
- req.destroy();
35853
- }, timeoutMs);
35854
- deadline.unref();
35855
- req.on("upgrade", (_res, socket) => {
35856
- fail("the deployment answered with a protocol upgrade");
35857
- socket.destroy();
35858
- });
35859
- req.on("close", () => {
35860
- fail("the connection closed before a response was read");
35861
- clearTimeout(deadline);
35862
- });
35863
- req.on("error", (err) => {
35864
- fail(err.message);
35865
- });
35866
- if (options.body !== void 0) req.write(options.body);
35867
- req.end();
35868
- });
35869
- }
35870
-
35871
- // ../../packages/remote/src/client.ts
35872
- var ROUTES = {
35873
- events: "/v1/events",
35874
- auditEvents: "/v1/audit-events",
35875
- auditEventsBatch: "/v1/audit-events/batch",
35876
- inventory: "/v1/inventory",
35877
- storePosture: "/v1/store-posture",
35878
- policyBundle: "/v1/policy-bundle",
35879
- whoami: "/v1/plugin/whoami",
35880
- shares: "/v1/shares"
35881
- };
35882
- function headerValue(response, name) {
35883
- const raw = response.headers[name];
35884
- if (raw === void 0) return void 0;
35885
- return Array.isArray(raw) ? raw[0] : raw;
35886
- }
35887
- function okBody(response) {
35888
- if (response.status < 200 || response.status >= 300) {
35889
- throw new RemoteRequestError(response.status);
35890
- }
35891
- return response.body;
35892
- }
35893
- function parsed(schema, body, route) {
35894
- let json2;
35895
- try {
35896
- json2 = JSON.parse(body);
35897
- } catch {
35898
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
35899
- }
35900
- const result = schema.safeParse(json2);
35901
- if (!result.success) {
35902
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
35903
- }
35904
- return result.data;
35905
- }
35906
- function withoutTrailingSlashes(endpoint) {
35907
- let end = endpoint.length;
35908
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
35909
- return endpoint.slice(0, end);
35910
- }
35911
- var SLASH = "/".charCodeAt(0);
35912
- function createRemoteClient(options) {
35913
- const base = withoutTrailingSlashes(options.endpoint);
35914
- const url2 = (route) => `${base}${route}`;
35915
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
35916
- const sendOne = async (event) => {
35917
- const validated = RecordAuditEventRequest.safeParse(event);
35918
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
35919
- const response = await send({
35920
- ...common,
35921
- method: "POST",
35922
- url: url2(ROUTES.auditEvents),
35923
- body: JSON.stringify(validated.data)
35924
- });
35925
- okBody(response);
35926
- };
35927
- return {
35928
- async ingestEvents(batch) {
35929
- const response = await send({
35930
- ...common,
35931
- method: "POST",
35932
- url: url2(ROUTES.events),
35933
- body: JSON.stringify(batch)
35934
- });
35935
- return parsed(IngestAck, okBody(response), ROUTES.events);
35936
- },
35937
- async ingestInventory(context) {
35938
- const response = await send({
35939
- ...common,
35940
- method: "POST",
35941
- url: url2(ROUTES.inventory),
35942
- body: JSON.stringify(context)
35943
- });
35944
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
35945
- },
35946
- async recordAuditEvent(event) {
35947
- await sendOne(event);
35948
- },
35949
- async recordAuditEvents(events) {
35950
- const validated = RecordAuditEventBatch.safeParse({ events });
35951
- if (!validated.success) {
35952
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
35953
- }
35954
- const response = await send({
35955
- ...common,
35956
- method: "POST",
35957
- url: url2(ROUTES.auditEventsBatch),
35958
- body: JSON.stringify(validated.data)
35959
- });
35960
- if (response.status === 404) {
35961
- for (const event of validated.data.events) await sendOne(event);
35962
- return { accepted: validated.data.events.length };
35963
- }
35964
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
35965
- },
35966
- async reportStorePosture(snapshot) {
35967
- const response = await send({
35968
- ...common,
35969
- method: "POST",
35970
- url: url2(ROUTES.storePosture),
35971
- body: JSON.stringify(snapshot)
35972
- });
35973
- okBody(response);
35974
- },
35975
- async getPolicyBundle(etag) {
35976
- const response = await send({
35977
- ...common,
35978
- method: "GET",
35979
- url: url2(ROUTES.policyBundle),
35980
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
35981
- });
35982
- if (response.status === 304) {
35983
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
35984
- }
35985
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
35986
- return { changed: true, bundle, etag: headerValue(response, "etag") };
35987
- },
35988
- async whoami() {
35989
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
35990
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
35991
- },
35992
- async recordProjectEgress(request) {
35993
- const validated = EgressIngestRequest.safeParse(request);
35994
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
35995
- const response = await send({
35996
- ...common,
35997
- method: "POST",
35998
- url: url2(ROUTES.shares),
35999
- body: JSON.stringify(validated.data)
36000
- });
36001
- okBody(response);
36002
- }
36003
- };
36004
- }
36735
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
36736
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
36737
+ var TRACE_ID = EventMetadata.shape.traceId;
36738
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
36005
36739
 
36006
36740
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
36007
36741
  import { spawn } from "child_process";
@@ -36009,7 +36743,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
36009
36743
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
36010
36744
 
36011
36745
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
36012
- import { readFileSync as readFileSync15 } from "fs";
36746
+ import { readFileSync as readFileSync17 } from "fs";
36013
36747
  function createPluginBlock(build, policyStore) {
36014
36748
  return async () => {
36015
36749
  const cached2 = await policyStore.read();
@@ -36028,7 +36762,7 @@ function createPluginBlock(build, policyStore) {
36028
36762
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36029
36763
  import { randomUUID as randomUUID18 } from "crypto";
36030
36764
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
36031
- import { join as join23 } from "path";
36765
+ import { join as join25 } from "path";
36032
36766
 
36033
36767
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
36034
36768
  import { rename as rename2 } from "fs/promises";
@@ -36052,7 +36786,7 @@ async function publishByRename(tmp, file2, move = rename2) {
36052
36786
 
36053
36787
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36054
36788
  function createPolicyStore(dir = dataDir()) {
36055
- const file2 = join23(dir, "policy-cache.json");
36789
+ const file2 = join25(dir, "policy-cache.json");
36056
36790
  async function read() {
36057
36791
  try {
36058
36792
  const raw = await readFile2(file2, "utf8");
@@ -36061,22 +36795,32 @@ function createPolicyStore(dir = dataDir()) {
36061
36795
  const record2 = parsed2;
36062
36796
  const bundle = PolicyBundle.parse(record2.bundle);
36063
36797
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
36064
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
36798
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
36799
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
36800
+ const etag = replayable ? stored : void 0;
36065
36801
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
36066
36802
  } catch {
36067
36803
  return null;
36068
36804
  }
36069
36805
  }
36070
- async function write(bundle, etag) {
36071
- await ensureDataDir(dir);
36072
- const stored = {
36073
- bundle,
36074
- fetchedAtMs: Date.now(),
36075
- ...etag === void 0 ? {} : { etag }
36076
- };
36806
+ function knowsMoreThanThisBuild(shapeId) {
36807
+ if (typeof shapeId !== "string" || shapeId === "") return false;
36808
+ const theirs = new Set(shapeId.split(","));
36809
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
36810
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
36811
+ }
36812
+ async function priorRecord() {
36813
+ try {
36814
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
36815
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
36816
+ } catch {
36817
+ return null;
36818
+ }
36819
+ }
36820
+ async function publishRecord(record2) {
36077
36821
  const tmp = `${file2}.${randomUUID18()}.tmp`;
36078
36822
  try {
36079
- await writeFile2(tmp, JSON.stringify(stored), {
36823
+ await writeFile2(tmp, JSON.stringify(record2), {
36080
36824
  encoding: "utf8",
36081
36825
  mode: DATA_FILE_MODE,
36082
36826
  flag: "wx"
@@ -36087,6 +36831,27 @@ function createPolicyStore(dir = dataDir()) {
36087
36831
  throw err;
36088
36832
  }
36089
36833
  }
36834
+ async function write(bundle, etag) {
36835
+ await ensureDataDir(dir);
36836
+ const prior = await priorRecord();
36837
+ const priorVersion = prior?.bundle?.version;
36838
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
36839
+ await publishRecord({
36840
+ ...prior,
36841
+ fetchedAtMs: Date.now()
36842
+ });
36843
+ return;
36844
+ }
36845
+ await publishRecord({
36846
+ bundle,
36847
+ fetchedAtMs: Date.now(),
36848
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
36849
+ // bundle it already holds, and the point of the stamp is to describe the
36850
+ // build that last narrowed those bytes, which is this one.
36851
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
36852
+ ...etag === void 0 ? {} : { etag }
36853
+ });
36854
+ }
36090
36855
  return { read, write, file: file2 };
36091
36856
  }
36092
36857
 
@@ -36139,7 +36904,7 @@ function createPostureReporter(deps) {
36139
36904
  }
36140
36905
 
36141
36906
  // ../../packages/plugin-runtime/src/attached/posture-snapshot.ts
36142
- import { statSync as statSync9 } from "fs";
36907
+ import { statSync as statSync10 } from "fs";
36143
36908
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
36144
36909
 
36145
36910
  // ../../packages/plugin-runtime/src/attached/action-counts.ts
@@ -36170,7 +36935,7 @@ function emptyReadout(readError = false) {
36170
36935
  }
36171
36936
  function readStorePosture(dbPath2) {
36172
36937
  try {
36173
- statSync9(dbPath2);
36938
+ statSync10(dbPath2);
36174
36939
  } catch (err) {
36175
36940
  const code = err.code;
36176
36941
  if (code === "ENOENT" || code === "ENOTDIR") return emptyReadout();
@@ -36252,11 +37017,11 @@ function readStorePosture(dbPath2) {
36252
37017
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
36253
37018
  import { randomUUID as randomUUID19 } from "crypto";
36254
37019
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
36255
- import { join as join24 } from "path";
37020
+ import { join as join26 } from "path";
36256
37021
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
36257
37022
  function createPostureStore(dir = settingsDir(), legacyDir) {
36258
- const file2 = join24(dir, "posture-state.json");
36259
- const legacyFile = legacyDir === void 0 ? null : join24(legacyDir, "posture-state.json");
37023
+ const file2 = join26(dir, "posture-state.json");
37024
+ const legacyFile = legacyDir === void 0 ? null : join26(legacyDir, "posture-state.json");
36260
37025
  async function persist(state) {
36261
37026
  await ensureDataDir(dir);
36262
37027
  const tmp = `${file2}.${randomUUID19()}.tmp`;
@@ -36324,8 +37089,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
36324
37089
  }
36325
37090
 
36326
37091
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
36327
- import { readFileSync as readFileSync16 } from "fs";
36328
- import { join as join25 } from "path";
37092
+ import { readFileSync as readFileSync18 } from "fs";
37093
+ import { join as join27 } from "path";
36329
37094
 
36330
37095
  // ../../packages/plugin-runtime/src/attached/status.ts
36331
37096
  var REFUSAL_LINES = {
@@ -36643,9 +37408,21 @@ var StandaloneDataGateway = class {
36643
37408
  // for the whole of it, so a member that threw would make that answer a lie
36644
37409
  // the moment a composite delegated to it. A store-level no-op is the honest
36645
37410
  // shape — a standalone machine has nothing delivered to record.
37411
+ markCaptureOwed(event) {
37412
+ this.db.markCaptureOwed(event);
37413
+ }
36646
37414
  markCaptureDelivered(event, atMs) {
36647
37415
  this.db.markCaptureDelivered(event, atMs);
36648
37416
  }
37417
+ // Implemented, not stubbed, for the same reason its sibling above is: the
37418
+ // attached gateway is a DECORATOR over an instance of this class
37419
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
37420
+ // stamp the live forward makes lands here with a non-empty array. This is the
37421
+ // production write path for that feature, not a shape-satisfying no-op — a
37422
+ // machine that is merely standalone simply never calls it.
37423
+ markAuditEventsDelivered(events, atMs) {
37424
+ this.db.markAuditEventsDelivered(events, atMs);
37425
+ }
36649
37426
  staleBinaryNotice(currentVersion) {
36650
37427
  try {
36651
37428
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -36793,12 +37570,12 @@ function openGatewayOrNull(config2) {
36793
37570
  }
36794
37571
  }
36795
37572
  function markerDirs(dataDir2) {
36796
- return [dataDir2, dirname5(dataDir2)];
37573
+ return [dataDir2, dirname7(dataDir2)];
36797
37574
  }
36798
37575
  function alreadyClaimed(dirs, marker, sessionId) {
36799
37576
  return dirs.some((dir) => {
36800
37577
  try {
36801
- return readFileSync17(join26(dir, marker), "utf8") === sessionId;
37578
+ return readFileSync19(join28(dir, marker), "utf8") === sessionId;
36802
37579
  } catch {
36803
37580
  return false;
36804
37581
  }
@@ -36808,7 +37585,7 @@ function recordClaim(dirs, marker, sessionId) {
36808
37585
  for (const dir of dirs) {
36809
37586
  try {
36810
37587
  mkdirSync5(dir, { recursive: true, mode: DATA_DIR_MODE });
36811
- writeFileSync8(join26(dir, marker), sessionId, { mode: DATA_FILE_MODE });
37588
+ writeFileSync8(join28(dir, marker), sessionId, { mode: DATA_FILE_MODE });
36812
37589
  return;
36813
37590
  } catch {
36814
37591
  }
@@ -36833,7 +37610,7 @@ function formatMode(mode) {
36833
37610
  }
36834
37611
  function warnIfStoreRedirected(config2, sessionId, write = (message) => void process.stderr.write(message)) {
36835
37612
  try {
36836
- const paths = symlinkedStorePaths(dirname5(config2.dataDir));
37613
+ const paths = symlinkedStorePaths(dirname7(config2.dataDir));
36837
37614
  if (paths.length === 0) return;
36838
37615
  if (!sessionId) {
36839
37616
  write(storeRedirectedMessage(paths));