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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -497,6 +497,7 @@ import { createHash as createHash4 } from "crypto";
497
497
  // ../../packages/persistence/src/attached-derived.ts
498
498
  import { rmSync } from "fs";
499
499
  import { join } from "path";
500
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
500
501
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
501
502
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
502
503
 
@@ -597,6 +598,30 @@ var SQLITE_MIGRATIONS = [
597
598
  {
598
599
  tag: "0022_audit_inspection_ms",
599
600
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
601
+ },
602
+ {
603
+ tag: "0023_secret_vault_user_authorized",
604
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
605
+ },
606
+ {
607
+ tag: "0024_finding_resolution_key_created_index",
608
+ sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
609
+ },
610
+ {
611
+ tag: "0025_audit_capture_attribute_columns",
612
+ sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
613
+ },
614
+ {
615
+ tag: "0026_audit_llm_call_usage_columns",
616
+ sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
617
+ },
618
+ {
619
+ tag: "0027_audit_llm_usage_index",
620
+ sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
621
+ },
622
+ {
623
+ tag: "0028_activity_session_probe_indexes",
624
+ sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
600
625
  }
601
626
  ];
602
627
 
@@ -22136,6 +22161,26 @@ var AttachTokenResponse = external_exports.union([
22136
22161
  AttachTokenExpired,
22137
22162
  external_exports.object({ status: printable(64) })
22138
22163
  ]);
22164
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22165
+ var DeviceCommand = external_exports.object({
22166
+ id: printable(128).min(1),
22167
+ kind: DeviceCommandKind,
22168
+ issuedAt: printable(64).min(1),
22169
+ expiresAt: printable(64).min(1)
22170
+ }).strict();
22171
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22172
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22173
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22174
+ external_exports.object({
22175
+ outcome: external_exports.literal("reported"),
22176
+ projectsScanned: external_exports.number().int().nonnegative()
22177
+ }).strict(),
22178
+ external_exports.object({
22179
+ outcome: external_exports.literal("failed"),
22180
+ reason: DeviceCommandFailureReason,
22181
+ projectsScanned: external_exports.number().int().nonnegative()
22182
+ }).strict()
22183
+ ]);
22139
22184
 
22140
22185
  // ../../packages/schema/src/zod/registry.ts
22141
22186
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22302,7 +22347,7 @@ var PackManifest = external_exports.object({
22302
22347
  }).meta({ id: "PackManifest" });
22303
22348
 
22304
22349
  // ../../packages/schema/src/zod/detection.ts
22305
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22350
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22306
22351
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22307
22352
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22308
22353
  var DetectionCounts = external_exports.object({
@@ -22439,14 +22484,17 @@ function optional2(key, parsed2, raw) {
22439
22484
  function isStringArray(value) {
22440
22485
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22441
22486
  }
22487
+ var ORIGIN_VALUES = { library: true, custom: true };
22488
+ function resolveOrigin(origin) {
22489
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22490
+ }
22442
22491
  function summaryToDetectionListItem(s) {
22443
22492
  return {
22444
22493
  id: `${s.namespace}/${s.packId}`,
22445
22494
  name: s.name,
22446
22495
  version: s.version,
22447
22496
  enabled: s.enabled,
22448
- origin: "library",
22449
- // v1: every installed pack is library origin
22497
+ origin: resolveOrigin(s.origin),
22450
22498
  namespace: s.namespace,
22451
22499
  packId: s.packId,
22452
22500
  ruleCount: s.ruleCount,
@@ -22498,7 +22546,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22498
22546
  name: row.name,
22499
22547
  version: row.version,
22500
22548
  enabled: row.enabled,
22501
- origin: "library",
22549
+ origin: resolveOrigin(row.origin),
22502
22550
  namespace: row.namespace,
22503
22551
  packId: row.packId,
22504
22552
  ruleCount: row.rules.length,
@@ -22518,16 +22566,20 @@ function splitDetectionId(id) {
22518
22566
  }
22519
22567
  function buildDetectionsList(summaries, query) {
22520
22568
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22569
+ const originOf = (s) => resolveOrigin(s.origin);
22521
22570
  const counts = {
22522
22571
  all: summaries.length,
22523
- library: summaries.length,
22524
- // all origin=library in v1
22525
- custom: 0,
22572
+ library: summaries.filter((s) => originOf(s) === "library").length,
22573
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22574
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22575
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22576
+ // place, and that state does not exist — editing a library pack forks it. See
22577
+ // OriginEnum.
22526
22578
  customized: 0,
22527
22579
  updates: withUpdate.length
22528
22580
  };
22529
22581
  const filter = query.filter;
22530
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22582
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22531
22583
  if (query.q) {
22532
22584
  const q = query.q.toLowerCase();
22533
22585
  filtered = filtered.filter(
@@ -22607,8 +22659,9 @@ var Event = external_exports.object({
22607
22659
  metadata: EventMetadata.optional()
22608
22660
  }).meta({ id: "Event" });
22609
22661
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22662
+ var INGEST_BATCH_MAX = 100;
22610
22663
  var IngestBatch = external_exports.object({
22611
- events: external_exports.array(IngestEvent).min(1).max(100),
22664
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22612
22665
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22613
22666
  // additionally rejects any event whose contentHash the store has already
22614
22667
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23164,383 +23217,11 @@ var PatchInstalledPackRequest = external_exports.object({
23164
23217
  message: "At least one field must be provided"
23165
23218
  }).meta({ id: "PatchInstalledPackRequest" });
23166
23219
 
23167
- // ../../packages/schema/src/zod/vault.ts
23168
- var POINTER_FORMAT_VERSION = 2;
23169
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23170
- var POINTER_TOKEN_PATTERN = new RegExp(
23171
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23172
- );
23173
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23174
- function pointerTokenScanner() {
23175
- return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23176
- }
23177
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23178
- var ParsedPointer = external_exports.object({
23179
- category: DetectionCategory,
23180
- keyVersion: external_exports.number().int().positive(),
23181
- pointerId: external_exports.string(),
23182
- tag: external_exports.string()
23183
- });
23184
- var VaultEntry = external_exports.object({
23185
- pointerId: external_exports.string(),
23186
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23187
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23188
- // independently of the vault encryption key below.
23189
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23190
- fingerprintKeyVersion: external_exports.number().int().positive(),
23191
- // The vault-key epoch this row's ciphertext was sealed under.
23192
- keyVersion: external_exports.number().int().positive(),
23193
- // Fixed at first mint and never updated: the same value detected later under a
23194
- // different rule's category keeps the category it was minted with, so one
23195
- // value always produces exactly one wire token.
23196
- category: DetectionCategory,
23197
- ruleId: external_exports.string(),
23198
- // Partial-reveal preview for badges and listings. Never the raw value.
23199
- maskedMatch: external_exports.string(),
23200
- provider: external_exports.string().optional(),
23201
- ciphertext: external_exports.string(),
23202
- nonce: external_exports.string(),
23203
- authTag: external_exports.string(),
23204
- // How many times this value has been detected on this machine — the reuse
23205
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23206
- occurrenceCount: external_exports.number().int().nonnegative(),
23207
- firstSeen: external_exports.string(),
23208
- lastSeen: external_exports.string()
23209
- });
23210
- var PointerDescriptor = external_exports.object({
23211
- category: DetectionCategory,
23212
- provider: external_exports.string().optional(),
23213
- maskedMatch: external_exports.string(),
23214
- occurrences: external_exports.number().int().nonnegative(),
23215
- firstSeen: external_exports.string(),
23216
- lastSeen: external_exports.string()
23217
- });
23218
- var PointerIdentity = external_exports.object({
23219
- ruleId: external_exports.string(),
23220
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23221
- fingerprintKeyVersion: external_exports.number().int().positive()
23222
- });
23223
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23224
- var VaultDerefReason = external_exports.enum([
23225
- "display",
23226
- "explicit-reveal",
23227
- "view-render",
23228
- "model-input",
23229
- "remediation",
23230
- "purge"
23231
- ]);
23232
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23233
- var BATCHED_DEREF_REASONS = ["display", "view-render"];
23234
- function isBatchedDerefReason(reason) {
23235
- return BATCHED_DEREF_REASONS.includes(reason);
23236
- }
23237
- var VaultDeref = external_exports.object({
23238
- id: external_exports.guid(),
23239
- pointerId: external_exports.string(),
23240
- at: external_exports.string(),
23241
- target: DetokenizeTarget,
23242
- reason: VaultDerefReason,
23243
- outcome: VaultDerefOutcome,
23244
- // Present only on a model-target crossing that a reveal grant authorized.
23245
- grantId: external_exports.string().optional(),
23246
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23247
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23248
- pointerCount: external_exports.number().int().positive().default(1)
23249
- });
23250
- var VaultSightingKind = external_exports.enum([
23251
- "prompt",
23252
- "tool-input",
23253
- "tool-output",
23254
- "file",
23255
- "transcript"
23256
- ]);
23257
- var VaultSighting = external_exports.object({
23258
- location: external_exports.string(),
23259
- kind: VaultSightingKind,
23260
- firstSeen: external_exports.string(),
23261
- lastSeen: external_exports.string()
23262
- });
23263
- var VaultInventoryEntry = external_exports.object({
23264
- pointerId: external_exports.string(),
23265
- category: DetectionCategory,
23266
- provider: external_exports.string().optional(),
23267
- maskedMatch: external_exports.string(),
23268
- occurrences: external_exports.number().int().nonnegative(),
23269
- firstSeen: external_exports.string(),
23270
- lastSeen: external_exports.string(),
23271
- // The active reveal-to-model grant covering this value, when one exists —
23272
- // the inventory badges it, the row links to revocation.
23273
- revealGrantId: external_exports.string().nullable(),
23274
- sightings: external_exports.array(VaultSighting)
23275
- });
23276
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23277
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23278
- var MAX_VAULT_PAGE_LIMIT = 200;
23279
- var ListVaultInventoryQuery = external_exports.object({
23280
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23281
- // Opaque; names the last row of the page just served.
23282
- cursor: external_exports.string().optional()
23283
- });
23284
- var ListVaultInventoryResponse = external_exports.object({
23285
- // Vaulted values across the whole store, not just this page — cursor-
23286
- // independent, so paging never changes what the count claims.
23287
- totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23288
- items: external_exports.array(VaultInventoryEntry),
23289
- // `null` once the last page is reached.
23290
- nextCursor: external_exports.string().nullable()
23291
- });
23292
- var ListVaultReuseQuery = external_exports.object({
23293
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23294
- cursor: external_exports.string().optional()
23295
- });
23296
- var ListVaultReuseResponse = external_exports.object({
23297
- // Reused values across the whole store — the number the section's claim
23298
- // ("values detected in more than one place") is about.
23299
- totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23300
- items: external_exports.array(VaultInventoryEntry),
23301
- nextCursor: external_exports.string().nullable()
23302
- });
23303
- var ListVaultDerefsQuery = external_exports.object({
23304
- // Include the batched, high-volume reasons (display, view-render). Omitted
23305
- // hides them and counts them into `hiddenBatched` instead, so the model
23306
- // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23307
- // over a Server Action, which preserves the type, never as a URL param.
23308
- includeBatched: external_exports.boolean().optional(),
23309
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23310
- cursor: external_exports.string().optional()
23311
- });
23312
- var ListVaultDerefsResponse = external_exports.object({
23313
- items: external_exports.array(VaultDeref),
23314
- nextCursor: external_exports.string().nullable(),
23315
- // Display/view-render rows the query hid, over the WHOLE trail rather than
23316
- // this page — it is the count the "N hidden" line and its toggle speak for.
23317
- // Always 0 when `includeBatched` was set, since nothing was hidden.
23318
- hiddenBatched: external_exports.number().int().nonnegative()
23319
- });
23320
- var VaultKeyCustody = external_exports.string();
23321
- var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23322
- var VAULT_EVENT_NOTE_MAX_POINTERS = 8;
23323
- var VAULT_CONSENT_VERSION = 1;
23324
- var VaultConsent = external_exports.object({
23325
- acknowledgedAt: external_exports.iso.datetime(),
23326
- version: external_exports.number().int().positive()
23327
- });
23328
- function isVaultConsentValid(consent) {
23329
- return consent?.version === VAULT_CONSENT_VERSION;
23330
- }
23331
-
23332
- // ../../packages/schema/src/zod/local.ts
23333
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23334
- var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23335
- var RunMode = external_exports.enum(["standalone", "attached"]);
23336
- var ControlPlaneConnection = external_exports.object({
23337
- endpoint: external_exports.string().min(1),
23338
- // Display name for the deployment, shown instead of the raw endpoint.
23339
- label: external_exports.string().min(1).optional(),
23340
- attachedAt: external_exports.iso.datetime()
23341
- }).meta({ id: "ControlPlaneConnection" });
23342
- var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23343
- var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23344
- var ModelJudgeConsent = external_exports.object({
23345
- acknowledgedAt: external_exports.iso.datetime(),
23346
- payloadVersion: external_exports.number().int().positive()
23347
- });
23348
- var HistorySyncConsent = external_exports.object({
23349
- acknowledgedAt: external_exports.iso.datetime(),
23350
- payloadVersion: external_exports.number().int().positive(),
23351
- endpoint: external_exports.string()
23352
- });
23353
- var WorkspaceSettings = external_exports.object({
23354
- specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23355
- runMode: RunMode.default("standalone"),
23356
- // Present only while attached; a detach clears it. Its presence is what makes
23357
- // `runMode: 'attached'` mean anything — see isAttached.
23358
- controlPlane: ControlPlaneConnection.optional(),
23359
- policy: SimpleDetectionPolicy.default("redact"),
23360
- // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23361
- historicalAccess: HistoricalAccess.default("session-only"),
23362
- // In-place egress extraction on the scan paths; disable to stop all Data
23363
- // Shares writes.
23364
- dataSharesInPlace: external_exports.boolean().default(true),
23365
- // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23366
- // vault, instead of destroying them. Absent by default: this is a custody
23367
- // change from one-way redaction, so it is never an assumed grant on upgrade.
23368
- // Revoking stops future vaulting; it does not erase what is already stored —
23369
- // purging the vault is the eraser.
23370
- vaultConsent: VaultConsent.optional(),
23371
- // Where the vault master key lives.
23372
- vaultKeyCustody: VaultKeyCustody.default("file"),
23373
- // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23374
- vaultInlineReveal: VaultInlineReveal.default("masked"),
23375
- // Absent until /aka:setup completes; its presence is what "onboarded" means.
23376
- onboardedAt: external_exports.iso.datetime().optional(),
23377
- // Records that the user consented to sending findings to the model API for
23378
- // the /aka:setup judge, along with the payload-shape version they agreed to.
23379
- // Absent until granted; a stale payloadVersion means the consent no longer
23380
- // covers the current payload and must be re-granted.
23381
- modelJudgeConsent: ModelJudgeConsent.optional(),
23382
- // Records that the user consented to sending the activity already recorded on
23383
- // this machine to the deployment it is attached to, along with the payload
23384
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23385
- // a different endpoint or an older payload no longer counts.
23386
- historySyncConsent: HistorySyncConsent.optional()
23387
- });
23388
- function defaultWorkspaceSettings() {
23389
- return WorkspaceSettings.parse({});
23390
- }
23391
- function isAttached(settings) {
23392
- return settings.runMode === "attached" && settings.controlPlane !== void 0;
23393
- }
23394
- function toInventoryRow(input2, id, now) {
23395
- return {
23396
- id,
23397
- objectType: input2.objectType,
23398
- location: input2.location ?? null,
23399
- title: input2.title ?? null,
23400
- hostId: input2.hostId ?? null,
23401
- attributes: JSON.stringify(input2.attributes),
23402
- firstSeen: now,
23403
- lastSeen: now
23404
- };
23405
- }
23406
- function toSourceProjectRow(input2, id, now) {
23407
- return {
23408
- id,
23409
- url: input2.url,
23410
- name: input2.name ?? null,
23411
- attributes: JSON.stringify(input2.attributes),
23412
- firstSeen: now,
23413
- lastSeen: now
23414
- };
23415
- }
23416
- function toAuditEventRow(input2) {
23417
- return {
23418
- id: input2.id,
23419
- parentId: input2.parentId ?? null,
23420
- rootSessionId: input2.rootSessionId ?? null,
23421
- eventType: input2.eventType,
23422
- hostId: input2.hostId ?? null,
23423
- harnessId: input2.harnessId ?? null,
23424
- sourceProjectId: input2.sourceProjectId ?? null,
23425
- startedAt: isoToEpochMillis(input2.startedAt),
23426
- endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23427
- severity: input2.severity ?? null,
23428
- priority: input2.priority ?? null,
23429
- content: input2.content ?? null,
23430
- contentHash: input2.contentHash ?? null,
23431
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23432
- };
23433
- }
23434
- function toClassifiedDataRow(input2, id) {
23435
- return {
23436
- id,
23437
- class: input2.class,
23438
- label: input2.label ?? null,
23439
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23440
- };
23441
- }
23442
- function toInspectionDefinitionRow(input2, id) {
23443
- return {
23444
- id,
23445
- ruleId: input2.ruleId,
23446
- name: input2.name,
23447
- category: input2.category,
23448
- severity: input2.severity,
23449
- definition: input2.definition,
23450
- version: input2.version
23451
- };
23452
- }
23453
- function toInspectionFindingRow(input2) {
23454
- return {
23455
- id: input2.id,
23456
- auditEventId: input2.auditEventId,
23457
- inspectionDefinitionId: input2.inspectionDefinitionId,
23458
- classifiedDataId: input2.classifiedDataId ?? null,
23459
- spanStart: input2.span.start,
23460
- spanEnd: input2.span.end,
23461
- maskedMatch: input2.maskedMatch,
23462
- actionTaken: input2.actionTaken,
23463
- confidence: input2.confidence,
23464
- findingKey: input2.findingKey ?? null,
23465
- firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23466
- };
23467
- }
23468
- function toCaptureAttributes(event) {
23469
- const metadata = event.metadata;
23470
- return {
23471
- source_tool: event.sourceTool,
23472
- ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23473
- ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23474
- ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23475
- ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23476
- ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23477
- ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23478
- ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23479
- ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23480
- ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23481
- // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23482
- // has ever populated either), but every legacy metadata key still rides
23483
- // the bag rather than being silently dropped — CaptureAttributes'
23484
- // `.catchall(z.unknown())` carries the long tail.
23485
- ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23486
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23487
- };
23488
- }
23489
- function captureDefinitionVersion(finding) {
23490
- return `capture/${finding.category}/${finding.severity}`;
23491
- }
23492
- function toCaptureDefinitionInput(finding) {
23493
- return {
23494
- ruleId: finding.ruleId,
23495
- version: captureDefinitionVersion(finding),
23496
- name: finding.ruleId,
23497
- category: finding.category,
23498
- severity: finding.severity,
23499
- definition: JSON.stringify({ ruleId: finding.ruleId })
23500
- };
23501
- }
23502
-
23503
- // ../../packages/schema/src/zod/managed.ts
23504
- var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23505
- var MANAGED_SETTINGS_SPEC_VERSION = 1;
23506
- var ManagedSettingKey = external_exports.enum([
23507
- "runMode",
23508
- "historicalAccess",
23509
- "vaultConsent",
23510
- "vaultKeyCustody",
23511
- "vaultInlineReveal",
23512
- "modelJudgeConsent",
23513
- "dataSharesInPlace"
23514
- ]).meta({ id: "ManagedSettingKey" });
23515
- var ManagedSettingsValues = external_exports.object({
23516
- runMode: external_exports.enum(["standalone", "attached"]).optional(),
23517
- controlPlane: external_exports.object({
23518
- endpoint: external_exports.string().min(1),
23519
- label: external_exports.string().min(1).optional()
23520
- }).optional(),
23521
- historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23522
- vaultConsent: external_exports.boolean().optional(),
23523
- vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23524
- vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23525
- modelJudgeConsent: external_exports.boolean().optional(),
23526
- dataSharesInPlace: external_exports.boolean().optional()
23527
- }).meta({ id: "ManagedSettingsValues" });
23528
- var ManagedSettings = external_exports.object({
23529
- specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23530
- // Shown on every locked control, so the user can tell an administrative
23531
- // decision from a bug. Absent renders as a generic "your organization".
23532
- organization: external_exports.string().min(1).optional(),
23533
- // What the administrator pinned.
23534
- values: ManagedSettingsValues.default({}),
23535
- // Which of those the user may not change. A key here with no matching value
23536
- // freezes whatever the user last chose; a value with no lock is a DEFAULT
23537
- // the user may still override. The two are separable on purpose.
23538
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23539
- }).meta({ id: "ManagedSettings" });
23540
-
23541
23220
  // ../../packages/schema/src/zod/policy.ts
23542
23221
  var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23543
23222
  var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23223
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23224
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23544
23225
  var Policy = external_exports.object({
23545
23226
  id: external_exports.guid(),
23546
23227
  scope: PolicyScope,
@@ -23550,7 +23231,27 @@ var Policy = external_exports.object({
23550
23231
  customKeywords: external_exports.array(external_exports.string()).optional(),
23551
23232
  // Display name — optional so older policy rows without name still parse.
23552
23233
  // Added for the findings API (policy.name column migration).
23553
- name: external_exports.string().optional()
23234
+ name: external_exports.string().optional(),
23235
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23236
+ // which row this is. A producer that collapses several rows onto one target
23237
+ // must carry the marker onto whichever row survives, or the collapse decides
23238
+ // the answer; a survivor may therefore be a built-in expansion still marked
23239
+ // 'authored' because an authored sibling targeted the same thing.
23240
+ // Optional so an older producer — and an older on-disk cache — still parses;
23241
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23242
+ //
23243
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23244
+ // built-in archetype catalog entry a policy is, which every catalog surface
23245
+ // reads and which a caller may state. This one is a statement the PRODUCER
23246
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23247
+ // — the CRUD routes neither accept nor set it.
23248
+ //
23249
+ // A device consumes this in exactly one direction: an 'authored' policy
23250
+ // arriving from a control plane marks the rules it targets as not
23251
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23252
+ // which is what makes it safe to honour from an unsigned cache — the same
23253
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23254
+ provenance: PolicyProvenance.optional()
23554
23255
  }).meta({ id: "Policy" });
23555
23256
  var PolicyBundle = external_exports.object({
23556
23257
  version: external_exports.string(),
@@ -23602,6 +23303,12 @@ var PolicyBundle = external_exports.object({
23602
23303
  customKeywords: external_exports.array(external_exports.string()),
23603
23304
  fetchedAt: external_exports.iso.datetime()
23604
23305
  }).meta({ id: "PolicyBundle" });
23306
+ var POLICY_BUNDLE_SHAPE_ID = [
23307
+ ...Object.keys(PolicyBundle.shape),
23308
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23309
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23310
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23311
+ ].sort().join(",");
23605
23312
  var OBSERVE_ONLY_CATEGORIES = ["config"];
23606
23313
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23607
23314
  var CATEGORY_PEAK_SEVERITY = {
@@ -23622,9 +23329,11 @@ function severityFloorPolicy(category) {
23622
23329
  const peak = CATEGORY_PEAK_SEVERITY[category];
23623
23330
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23624
23331
  }
23625
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23626
23332
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23627
23333
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23334
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23335
+ id: "RedactFallback"
23336
+ });
23628
23337
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23629
23338
  var BUILTIN_POLICY_SPECS = {
23630
23339
  monitor: {
@@ -23661,6 +23370,42 @@ var BUILTIN_POLICY_SPECS = {
23661
23370
  function builtinPolicyToAction(id) {
23662
23371
  return BUILTIN_POLICY_SPECS[id].action;
23663
23372
  }
23373
+ var PALETTE_WEAKEST_FIRST = [
23374
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23375
+ ];
23376
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23377
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23378
+ );
23379
+ var ACTION_STRENGTH_ORDER = [
23380
+ ...BELOW_PALETTE,
23381
+ ...PALETTE_WEAKEST_FIRST
23382
+ ];
23383
+ function actionRank(action) {
23384
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23385
+ }
23386
+ function isActionAtLeast(action, floor) {
23387
+ return actionRank(action) >= actionRank(floor);
23388
+ }
23389
+ function strongerAction(a, b) {
23390
+ return actionRank(a) >= actionRank(b) ? a : b;
23391
+ }
23392
+ function weakestBuiltinAtLeast(floor) {
23393
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23394
+ }
23395
+ var PackPolicyFloor = external_exports.object({
23396
+ /**
23397
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23398
+ * rather than a raw ActionTaken because that is the vocabulary the user
23399
+ * picks from — a floor a UI cannot name is one it cannot explain.
23400
+ */
23401
+ floor: BuiltinPolicyId,
23402
+ /**
23403
+ * True when the organization AUTHORED a policy governing this pack rather
23404
+ * than stating a minimum: it gave the answer, so the pack is not
23405
+ * re-assignable locally in either direction.
23406
+ */
23407
+ locked: external_exports.boolean()
23408
+ }).describe("PackPolicyFloor");
23664
23409
  var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23665
23410
  (id) => !BUILTIN_POLICY_SPECS[id].reversible
23666
23411
  );
@@ -23718,6 +23463,405 @@ var PolicyStatsResponse = external_exports.object({
23718
23463
  detectionsGoverned: external_exports.number().int().nonnegative()
23719
23464
  }).meta({ id: "PolicyStatsResponse" });
23720
23465
 
23466
+ // ../../packages/schema/src/zod/vault.ts
23467
+ var POINTER_FORMAT_VERSION = 2;
23468
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23469
+ var POINTER_TOKEN_PATTERN = new RegExp(
23470
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23471
+ );
23472
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23473
+ function pointerTokenScanner() {
23474
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23475
+ }
23476
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23477
+ var ParsedPointer = external_exports.object({
23478
+ category: DetectionCategory,
23479
+ keyVersion: external_exports.number().int().positive(),
23480
+ pointerId: external_exports.string(),
23481
+ tag: external_exports.string()
23482
+ });
23483
+ var VaultEntry = external_exports.object({
23484
+ pointerId: external_exports.string(),
23485
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23486
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23487
+ // independently of the vault encryption key below.
23488
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23489
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23490
+ // The vault-key epoch this row's ciphertext was sealed under.
23491
+ keyVersion: external_exports.number().int().positive(),
23492
+ // Fixed at first mint and never updated: the same value detected later under a
23493
+ // different rule's category keeps the category it was minted with, so one
23494
+ // value always produces exactly one wire token.
23495
+ category: DetectionCategory,
23496
+ ruleId: external_exports.string(),
23497
+ // Partial-reveal preview for badges and listings. Never the raw value.
23498
+ maskedMatch: external_exports.string(),
23499
+ provider: external_exports.string().optional(),
23500
+ ciphertext: external_exports.string(),
23501
+ nonce: external_exports.string(),
23502
+ authTag: external_exports.string(),
23503
+ // How many times this value has been detected on this machine — the reuse
23504
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23505
+ occurrenceCount: external_exports.number().int().nonnegative(),
23506
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23507
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23508
+ // one row however many paths vault it, so this is what tells a policy sweep
23509
+ // that the row carries somebody's own instruction and not just an assignment
23510
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23511
+ // vaulting of the same value must never clear it — what the user said about
23512
+ // the value does not expire.
23513
+ userAuthorized: external_exports.boolean(),
23514
+ firstSeen: external_exports.string(),
23515
+ lastSeen: external_exports.string()
23516
+ });
23517
+ var PointerDescriptor = external_exports.object({
23518
+ category: DetectionCategory,
23519
+ provider: external_exports.string().optional(),
23520
+ maskedMatch: external_exports.string(),
23521
+ occurrences: external_exports.number().int().nonnegative(),
23522
+ firstSeen: external_exports.string(),
23523
+ lastSeen: external_exports.string()
23524
+ });
23525
+ var PointerIdentity = external_exports.object({
23526
+ ruleId: external_exports.string(),
23527
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23528
+ fingerprintKeyVersion: external_exports.number().int().positive()
23529
+ });
23530
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23531
+ var VaultDerefReason = external_exports.enum([
23532
+ "display",
23533
+ "explicit-reveal",
23534
+ "view-render",
23535
+ "model-input",
23536
+ "remediation",
23537
+ "purge"
23538
+ ]);
23539
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23540
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
23541
+ function isBatchedDerefReason(reason) {
23542
+ return BATCHED_DEREF_REASONS.includes(reason);
23543
+ }
23544
+ var VaultDeref = external_exports.object({
23545
+ id: external_exports.guid(),
23546
+ pointerId: external_exports.string(),
23547
+ at: external_exports.string(),
23548
+ target: DetokenizeTarget,
23549
+ reason: VaultDerefReason,
23550
+ outcome: VaultDerefOutcome,
23551
+ // Present only on a model-target crossing that a reveal grant authorized.
23552
+ grantId: external_exports.string().optional(),
23553
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23554
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23555
+ pointerCount: external_exports.number().int().positive().default(1)
23556
+ });
23557
+ var VaultSightingKind = external_exports.enum([
23558
+ "prompt",
23559
+ "tool-input",
23560
+ "tool-output",
23561
+ "file",
23562
+ "transcript"
23563
+ ]);
23564
+ var VaultSighting = external_exports.object({
23565
+ location: external_exports.string(),
23566
+ kind: VaultSightingKind,
23567
+ firstSeen: external_exports.string(),
23568
+ lastSeen: external_exports.string()
23569
+ });
23570
+ var VaultInventoryEntry = external_exports.object({
23571
+ pointerId: external_exports.string(),
23572
+ category: DetectionCategory,
23573
+ provider: external_exports.string().optional(),
23574
+ maskedMatch: external_exports.string(),
23575
+ occurrences: external_exports.number().int().nonnegative(),
23576
+ firstSeen: external_exports.string(),
23577
+ lastSeen: external_exports.string(),
23578
+ // The active reveal-to-model grant covering this value, when one exists —
23579
+ // the inventory badges it, the row links to revocation.
23580
+ revealGrantId: external_exports.string().nullable(),
23581
+ sightings: external_exports.array(VaultSighting)
23582
+ });
23583
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23584
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23585
+ var MAX_VAULT_PAGE_LIMIT = 200;
23586
+ var ListVaultInventoryQuery = external_exports.object({
23587
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23588
+ // Opaque; names the last row of the page just served.
23589
+ cursor: external_exports.string().optional()
23590
+ });
23591
+ var ListVaultInventoryResponse = external_exports.object({
23592
+ // Vaulted values across the whole store, not just this page — cursor-
23593
+ // independent, so paging never changes what the count claims.
23594
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23595
+ items: external_exports.array(VaultInventoryEntry),
23596
+ // `null` once the last page is reached.
23597
+ nextCursor: external_exports.string().nullable()
23598
+ });
23599
+ var ListVaultReuseQuery = external_exports.object({
23600
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23601
+ cursor: external_exports.string().optional()
23602
+ });
23603
+ var ListVaultReuseResponse = external_exports.object({
23604
+ // Reused values across the whole store — the number the section's claim
23605
+ // ("values detected in more than one place") is about.
23606
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23607
+ items: external_exports.array(VaultInventoryEntry),
23608
+ nextCursor: external_exports.string().nullable()
23609
+ });
23610
+ var ListVaultDerefsQuery = external_exports.object({
23611
+ // Include the batched, high-volume reasons (display, view-render). Omitted
23612
+ // hides them and counts them into `hiddenBatched` instead, so the model
23613
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23614
+ // over a Server Action, which preserves the type, never as a URL param.
23615
+ includeBatched: external_exports.boolean().optional(),
23616
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23617
+ cursor: external_exports.string().optional()
23618
+ });
23619
+ var ListVaultDerefsResponse = external_exports.object({
23620
+ items: external_exports.array(VaultDeref),
23621
+ nextCursor: external_exports.string().nullable(),
23622
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
23623
+ // this page — it is the count the "N hidden" line and its toggle speak for.
23624
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
23625
+ hiddenBatched: external_exports.number().int().nonnegative()
23626
+ });
23627
+ var VaultKeyCustody = external_exports.string();
23628
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23629
+ var VAULT_EVENT_NOTE_MAX_POINTERS = 8;
23630
+ var VAULT_CONSENT_VERSION = 1;
23631
+ var VaultConsent = external_exports.object({
23632
+ acknowledgedAt: external_exports.iso.datetime(),
23633
+ version: external_exports.number().int().positive()
23634
+ });
23635
+ function isVaultConsentValid(consent) {
23636
+ return consent?.version === VAULT_CONSENT_VERSION;
23637
+ }
23638
+
23639
+ // ../../packages/schema/src/zod/local.ts
23640
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23641
+ var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23642
+ var RunMode = external_exports.enum(["standalone", "attached"]);
23643
+ var ControlPlaneConnection = external_exports.object({
23644
+ endpoint: external_exports.string().min(1),
23645
+ // Display name for the deployment, shown instead of the raw endpoint.
23646
+ label: external_exports.string().min(1).optional(),
23647
+ attachedAt: external_exports.iso.datetime()
23648
+ }).meta({ id: "ControlPlaneConnection" });
23649
+ var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23650
+ var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23651
+ var ModelJudgeConsent = external_exports.object({
23652
+ acknowledgedAt: external_exports.iso.datetime(),
23653
+ payloadVersion: external_exports.number().int().positive()
23654
+ });
23655
+ var HistorySyncConsent = external_exports.object({
23656
+ acknowledgedAt: external_exports.iso.datetime(),
23657
+ payloadVersion: external_exports.number().int().positive(),
23658
+ endpoint: external_exports.string()
23659
+ });
23660
+ var WorkspaceSettings = external_exports.object({
23661
+ specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23662
+ runMode: RunMode.default("standalone"),
23663
+ // Present only while attached; a detach clears it. Its presence is what makes
23664
+ // `runMode: 'attached'` mean anything — see isAttached.
23665
+ controlPlane: ControlPlaneConnection.optional(),
23666
+ policy: SimpleDetectionPolicy.default("redact"),
23667
+ // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23668
+ historicalAccess: HistoricalAccess.default("session-only"),
23669
+ // In-place egress extraction on the scan paths; disable to stop all Data
23670
+ // Shares writes.
23671
+ dataSharesInPlace: external_exports.boolean().default(true),
23672
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23673
+ // vault, instead of destroying them. Absent by default: this is a custody
23674
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
23675
+ // Revoking stops future vaulting; it does not erase what is already stored —
23676
+ // purging the vault is the eraser.
23677
+ vaultConsent: VaultConsent.optional(),
23678
+ // Where the vault master key lives.
23679
+ vaultKeyCustody: VaultKeyCustody.default("file"),
23680
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23681
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
23682
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23683
+ // place. Not a handling policy: the policy has already resolved to redact,
23684
+ // and this only says what happens when the host offers no channel to carry it
23685
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23686
+ // Claude Code decline to mask a field that EXECUTES because masking would
23687
+ // change what runs. Per FIELD rather than per host, so a host that can
23688
+ // rewrite some inputs keeps true redaction on those.
23689
+ //
23690
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23691
+ // an attached machine's merge is `strongerAction` over the one action ladder
23692
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23693
+ // word and stays out of the stored value.
23694
+ redactFallback: RedactFallback.default("warn"),
23695
+ // Absent until /aka:setup completes; its presence is what "onboarded" means.
23696
+ onboardedAt: external_exports.iso.datetime().optional(),
23697
+ // Records that the user consented to sending findings to the model API for
23698
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
23699
+ // Absent until granted; a stale payloadVersion means the consent no longer
23700
+ // covers the current payload and must be re-granted.
23701
+ modelJudgeConsent: ModelJudgeConsent.optional(),
23702
+ // Records that the user consented to the DEFERRED send — the outbox — along
23703
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23704
+ // that covers both the pre-attach backlog and undelivered captures (which
23705
+ // carry prompt/reply text in `content`); the key name predates the widening.
23706
+ // Absent until granted, and a grant for a different endpoint or an older
23707
+ // payload no longer counts.
23708
+ historySyncConsent: HistorySyncConsent.optional()
23709
+ });
23710
+ function defaultWorkspaceSettings() {
23711
+ return WorkspaceSettings.parse({});
23712
+ }
23713
+ function isAttached(settings) {
23714
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23715
+ }
23716
+ function toInventoryRow(input2, id, now) {
23717
+ return {
23718
+ id,
23719
+ objectType: input2.objectType,
23720
+ location: input2.location ?? null,
23721
+ title: input2.title ?? null,
23722
+ hostId: input2.hostId ?? null,
23723
+ attributes: JSON.stringify(input2.attributes),
23724
+ firstSeen: now,
23725
+ lastSeen: now
23726
+ };
23727
+ }
23728
+ function toSourceProjectRow(input2, id, now) {
23729
+ return {
23730
+ id,
23731
+ url: input2.url,
23732
+ name: input2.name ?? null,
23733
+ attributes: JSON.stringify(input2.attributes),
23734
+ firstSeen: now,
23735
+ lastSeen: now
23736
+ };
23737
+ }
23738
+ function toAuditEventRow(input2) {
23739
+ return {
23740
+ id: input2.id,
23741
+ parentId: input2.parentId ?? null,
23742
+ rootSessionId: input2.rootSessionId ?? null,
23743
+ eventType: input2.eventType,
23744
+ hostId: input2.hostId ?? null,
23745
+ harnessId: input2.harnessId ?? null,
23746
+ sourceProjectId: input2.sourceProjectId ?? null,
23747
+ startedAt: isoToEpochMillis(input2.startedAt),
23748
+ endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23749
+ severity: input2.severity ?? null,
23750
+ priority: input2.priority ?? null,
23751
+ content: input2.content ?? null,
23752
+ contentHash: input2.contentHash ?? null,
23753
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23754
+ };
23755
+ }
23756
+ function toClassifiedDataRow(input2, id) {
23757
+ return {
23758
+ id,
23759
+ class: input2.class,
23760
+ label: input2.label ?? null,
23761
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23762
+ };
23763
+ }
23764
+ function toInspectionDefinitionRow(input2, id) {
23765
+ return {
23766
+ id,
23767
+ ruleId: input2.ruleId,
23768
+ name: input2.name,
23769
+ category: input2.category,
23770
+ severity: input2.severity,
23771
+ definition: input2.definition,
23772
+ version: input2.version
23773
+ };
23774
+ }
23775
+ function toInspectionFindingRow(input2) {
23776
+ return {
23777
+ id: input2.id,
23778
+ auditEventId: input2.auditEventId,
23779
+ inspectionDefinitionId: input2.inspectionDefinitionId,
23780
+ classifiedDataId: input2.classifiedDataId ?? null,
23781
+ spanStart: input2.span.start,
23782
+ spanEnd: input2.span.end,
23783
+ maskedMatch: input2.maskedMatch,
23784
+ actionTaken: input2.actionTaken,
23785
+ confidence: input2.confidence,
23786
+ findingKey: input2.findingKey ?? null,
23787
+ firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23788
+ };
23789
+ }
23790
+ function toCaptureAttributes(event) {
23791
+ const metadata = event.metadata;
23792
+ return {
23793
+ source_tool: event.sourceTool,
23794
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23795
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23796
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23797
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23798
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23799
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23800
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23801
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23802
+ ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23803
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23804
+ // has ever populated either), but every legacy metadata key still rides
23805
+ // the bag rather than being silently dropped — CaptureAttributes'
23806
+ // `.catchall(z.unknown())` carries the long tail.
23807
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23808
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23809
+ };
23810
+ }
23811
+ function captureDefinitionVersion(finding) {
23812
+ return `capture/${finding.category}/${finding.severity}`;
23813
+ }
23814
+ function toCaptureDefinitionInput(finding) {
23815
+ return {
23816
+ ruleId: finding.ruleId,
23817
+ version: captureDefinitionVersion(finding),
23818
+ name: finding.ruleId,
23819
+ category: finding.category,
23820
+ severity: finding.severity,
23821
+ definition: JSON.stringify({ ruleId: finding.ruleId })
23822
+ };
23823
+ }
23824
+
23825
+ // ../../packages/schema/src/zod/managed.ts
23826
+ var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23827
+ var MANAGED_SETTINGS_SPEC_VERSION = 1;
23828
+ var ManagedSettingKey = external_exports.enum([
23829
+ "runMode",
23830
+ "historicalAccess",
23831
+ "vaultConsent",
23832
+ "vaultKeyCustody",
23833
+ "vaultInlineReveal",
23834
+ "modelJudgeConsent",
23835
+ "dataSharesInPlace",
23836
+ "redactFallback"
23837
+ ]).meta({ id: "ManagedSettingKey" });
23838
+ var ManagedSettingsValues = external_exports.object({
23839
+ runMode: external_exports.enum(["standalone", "attached"]).optional(),
23840
+ controlPlane: external_exports.object({
23841
+ endpoint: external_exports.string().min(1),
23842
+ label: external_exports.string().min(1).optional()
23843
+ }).optional(),
23844
+ historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23845
+ vaultConsent: external_exports.boolean().optional(),
23846
+ vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23847
+ vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23848
+ modelJudgeConsent: external_exports.boolean().optional(),
23849
+ dataSharesInPlace: external_exports.boolean().optional(),
23850
+ redactFallback: RedactFallback.optional()
23851
+ }).meta({ id: "ManagedSettingsValues" });
23852
+ var ManagedSettings = external_exports.object({
23853
+ specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23854
+ // Shown on every locked control, so the user can tell an administrative
23855
+ // decision from a bug. Absent renders as a generic "your organization".
23856
+ organization: external_exports.string().min(1).optional(),
23857
+ // What the administrator pinned.
23858
+ values: ManagedSettingsValues.default({}),
23859
+ // Which of those the user may not change. A key here with no matching value
23860
+ // freezes whatever the user last chose; a value with no lock is a DEFAULT
23861
+ // the user may still override. The two are separable on purpose.
23862
+ lockedFields: external_exports.array(ManagedSettingKey).default([])
23863
+ }).meta({ id: "ManagedSettings" });
23864
+
23721
23865
  // ../../packages/schema/src/zod/project-files.ts
23722
23866
  var ProjectFileInput = external_exports.object({
23723
23867
  path: external_exports.string().min(1),
@@ -23963,10 +24107,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23963
24107
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23964
24108
 
23965
24109
  // ../../packages/schema/src/zod/settings-action.ts
24110
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24111
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23966
24112
  var SaveSettingsInput = external_exports.object({
23967
24113
  historicalAccess: external_exports.string(),
23968
- modelJudgeConsent: external_exports.boolean(),
23969
- historySyncConsent: external_exports.boolean(),
24114
+ modelJudgeConsent: ModelJudgeConsentChoice,
24115
+ historySyncConsent: HistorySyncConsentChoice,
23970
24116
  vaultConsent: external_exports.string(),
23971
24117
  vaultInlineReveal: external_exports.string()
23972
24118
  });
@@ -24116,9 +24262,9 @@ function deriveReviewReasons(trust, transports) {
24116
24262
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24117
24263
  return reasons;
24118
24264
  }
24119
- function buildReviewInfo(trust, transports) {
24265
+ function buildReviewInfo(trust, transports, decided) {
24120
24266
  const reasons = deriveReviewReasons(trust, transports);
24121
- return { needsReview: reasons.length > 0, reasons };
24267
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24122
24268
  }
24123
24269
  function distinctTransports(transports) {
24124
24270
  return Array.from(new Set(transports));
@@ -24336,8 +24482,8 @@ function readControlPlaneCredential(settingsDir2, connection) {
24336
24482
  }
24337
24483
 
24338
24484
  // ../../packages/persistence/src/database.ts
24339
- import { randomUUID as randomUUID10 } from "crypto";
24340
- import { join as join4, sep } from "path";
24485
+ import { randomUUID as randomUUID11 } from "crypto";
24486
+ import { dirname as dirname2, join as join7, sep } from "path";
24341
24487
  import { DatabaseSync } from "node:sqlite";
24342
24488
 
24343
24489
  // ../../packages/persistence/src/ids.ts
@@ -24602,6 +24748,10 @@ function allRows(stmt, params) {
24602
24748
  if (Array.isArray(params)) return stmt.all(...params);
24603
24749
  return stmt.all(params);
24604
24750
  }
24751
+ function* iterateRows(stmt, params) {
24752
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24753
+ for (const row of rows) yield row;
24754
+ }
24605
24755
  function getRow(stmt, params) {
24606
24756
  if (params === void 0) return stmt.get();
24607
24757
  if (Array.isArray(params)) return stmt.get(...params);
@@ -25070,10 +25220,17 @@ function ensureSyncedAtColumn(db, table) {
25070
25220
  if (!columns.includes("sync_claimed_at")) {
25071
25221
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
25072
25222
  }
25223
+ if (!columns.includes("outbox_owed")) {
25224
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25225
+ }
25073
25226
  db.exec(
25074
25227
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25075
25228
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
25076
25229
  );
25230
+ db.exec(
25231
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25232
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25233
+ );
25077
25234
  db.exec(
25078
25235
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
25079
25236
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25178,7 +25335,6 @@ function decodeKeysetCursor(cursor) {
25178
25335
  // ../../packages/persistence/src/repositories/activity.ts
25179
25336
  var DAY_MS = 864e5;
25180
25337
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25181
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25182
25338
  function defaultTimeZone() {
25183
25339
  try {
25184
25340
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25233,6 +25389,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25233
25389
  error: "error",
25234
25390
  active: "active"
25235
25391
  };
25392
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25236
25393
  function safeParseStringArray(raw) {
25237
25394
  if (!raw) return [];
25238
25395
  const parsed2 = safeJson(raw, null);
@@ -25306,6 +25463,37 @@ var TIMELINE_COLUMNS = `
25306
25463
  json_extract(attributes, '$.targetId') AS target_id,
25307
25464
  json_extract(attributes, '$.internal') AS internal,
25308
25465
  json_extract(attributes, '$.flagged') AS flagged`;
25466
+ var LLM_USAGE_SELECT = `
25467
+ SELECT root_session_id AS sessionId,
25468
+ provider,
25469
+ model,
25470
+ service_tier AS serviceTier,
25471
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25472
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25473
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25474
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25475
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25476
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25477
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25478
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25479
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25480
+ function usageLeaves(rows) {
25481
+ return rows.map((row) => {
25482
+ const attributes = {
25483
+ input_tokens: row.inputTokens,
25484
+ output_tokens: row.outputTokens,
25485
+ cache_creation_input_tokens: row.cacheCreationTokens,
25486
+ cache_read_input_tokens: row.cacheReadTokens,
25487
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25488
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25489
+ web_search_requests: row.webSearchRequests
25490
+ };
25491
+ if (row.provider !== null) attributes.provider = row.provider;
25492
+ if (row.model !== null) attributes.model = row.model;
25493
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25494
+ return { sessionId: row.sessionId, attributes };
25495
+ });
25496
+ }
25309
25497
  var SESSION_ROOT = `event_type = 'session'`;
25310
25498
  var HAS_ACTIVITY = `EXISTS (
25311
25499
  SELECT 1 FROM audit_events c
@@ -25331,16 +25519,17 @@ var SqliteActivityRepository = class {
25331
25519
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25332
25520
  const liveNow = countScalar(
25333
25521
  this.db,
25334
- `SELECT count(*) AS n FROM audit_events s
25522
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25335
25523
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25336
- AND max(
25337
- s.started_at,
25338
- coalesce(
25339
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25340
- s.started_at
25341
- )
25342
- ) >= ?`,
25343
- [liveThreshold]
25524
+ AND s.id IN (
25525
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25526
+ UNION
25527
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25528
+ WHERE started_at >= ?
25529
+ UNION
25530
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25531
+ WHERE ended_at >= ?)`,
25532
+ [liveThreshold, liveThreshold, liveThreshold]
25344
25533
  );
25345
25534
  const toolCallsToday = countScalar(
25346
25535
  this.db,
@@ -25470,7 +25659,7 @@ var SqliteActivityRepository = class {
25470
25659
  this.db.prepare(
25471
25660
  `SELECT ${TIMELINE_COLUMNS}
25472
25661
  FROM audit_events
25473
- WHERE id = ? OR root_session_id = ?
25662
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25474
25663
  ORDER BY started_at ASC, id ASC`
25475
25664
  ),
25476
25665
  [sessionId, sessionId]
@@ -25483,14 +25672,14 @@ var SqliteActivityRepository = class {
25483
25672
  coalesce(sum(output_tokens), 0) AS output,
25484
25673
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25485
25674
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25486
- FROM audit_events
25675
+ FROM audit_events INDEXED BY idx_audit_session_type
25487
25676
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25488
25677
  ),
25489
25678
  [sessionId]
25490
25679
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25491
25680
  const primaryModel = getRow(
25492
25681
  this.db.prepare(
25493
- `SELECT model, provider FROM audit_events
25682
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25494
25683
  WHERE root_session_id = ? AND event_type = 'llm_call'
25495
25684
  ORDER BY started_at ASC, id ASC
25496
25685
  LIMIT 1`
@@ -25501,7 +25690,7 @@ var SqliteActivityRepository = class {
25501
25690
  this.db.prepare(
25502
25691
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25503
25692
  count(*) AS n
25504
- FROM audit_events
25693
+ FROM audit_events INDEXED BY idx_audit_session
25505
25694
  WHERE root_session_id = ? AND event_type = 'tool_call'
25506
25695
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25507
25696
  ),
@@ -25509,7 +25698,7 @@ var SqliteActivityRepository = class {
25509
25698
  );
25510
25699
  const modelRows = allRows(
25511
25700
  this.db.prepare(
25512
- `SELECT DISTINCT model FROM audit_events
25701
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25513
25702
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25514
25703
  ORDER BY model`
25515
25704
  ),
@@ -25518,7 +25707,7 @@ var SqliteActivityRepository = class {
25518
25707
  const derivedModels = modelRows.map((r) => r.model);
25519
25708
  const commits = countScalar(
25520
25709
  this.db,
25521
- `SELECT count(*) AS n FROM audit_events
25710
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25522
25711
  WHERE root_session_id = ? AND event_type = 'commit'`,
25523
25712
  [sessionId]
25524
25713
  );
@@ -25554,25 +25743,57 @@ var SqliteActivityRepository = class {
25554
25743
  return Promise.resolve(session);
25555
25744
  }
25556
25745
  /**
25557
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25558
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25559
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25560
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25561
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25562
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25746
+ * Cross-session token report — every `llm_call` in the store (or in a
25747
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25748
+ * session, with USD cost DERIVED at read time via the shared
25749
+ * `defaultCostModel` (never stored). The caller collapses these onto
25750
+ * per-model rows with `aggregateTokenUsage`.
25751
+ *
25752
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25753
+ * the members the rollup sums — and priced once per group, which is exact
25754
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25755
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25756
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25757
+ * the index stores the values once, at write, and answers the same window in
25758
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25759
+ * planner prefers the general event-type index and fetches every row to
25760
+ * recompute the columns it could have read. The index is one every open
25761
+ * store carries, since opening runs the migrations, so the hard requirement
25762
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25763
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25764
+ * per call, no bag parsed.
25563
25765
  */
25564
25766
  tokenReports(fromMs) {
25565
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25566
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25767
+ const rows = allRows(
25768
+ this.db.prepare(
25769
+ `${LLM_USAGE_SELECT}
25770
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25771
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25772
+ ${LLM_USAGE_GROUP}`
25773
+ ),
25774
+ fromMs === void 0 ? void 0 : [fromMs]
25775
+ );
25776
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25567
25777
  }
25568
25778
  /**
25569
- * One session's token report — its `llm_call` leaves grouped per (provider,
25570
- * model) with derived cost, or `null` when the session made no `llm_call`s
25571
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25572
- * breakdown + estimated cost.
25779
+ * One session's token report — its `llm_call`s grouped per (provider,
25780
+ * model, tier) with derived cost, or `null` when the session made no
25781
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25782
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25783
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25784
+ * it replaces walked every `llm_call` in the store to find one session's.
25573
25785
  */
25574
25786
  tokenReportForSession(sessionId) {
25575
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25787
+ const rows = allRows(
25788
+ this.db.prepare(
25789
+ `${LLM_USAGE_SELECT}
25790
+ FROM audit_events
25791
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25792
+ ${LLM_USAGE_GROUP}`
25793
+ ),
25794
+ [sessionId]
25795
+ );
25796
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25576
25797
  return Promise.resolve(reports[0] ?? null);
25577
25798
  }
25578
25799
  /**
@@ -25596,42 +25817,6 @@ var SqliteActivityRepository = class {
25596
25817
  for (const row of rows) seen.add(toHarness(row.harness));
25597
25818
  return Promise.resolve([...seen]);
25598
25819
  }
25599
- /**
25600
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25601
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25602
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25603
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25604
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25605
- */
25606
- readLlmCallLeaves(opts = {}) {
25607
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25608
- const params = [];
25609
- if (opts.sessionId !== void 0) {
25610
- conditions.push("root_session_id = ?");
25611
- params.push(opts.sessionId);
25612
- }
25613
- if (opts.fromMs !== void 0) {
25614
- conditions.push("started_at >= ?");
25615
- params.push(opts.fromMs);
25616
- }
25617
- const rows = allRows(
25618
- this.db.prepare(
25619
- `SELECT root_session_id AS sessionId, attributes
25620
- FROM audit_events
25621
- WHERE ${conditions.join(" AND ")}`
25622
- ),
25623
- params
25624
- );
25625
- return mapRowsTolerant(
25626
- rows.filter(
25627
- (row) => row.sessionId !== null
25628
- ),
25629
- (row) => ({
25630
- sessionId: row.sessionId,
25631
- attributes: JSON.parse(row.attributes)
25632
- })
25633
- );
25634
- }
25635
25820
  /**
25636
25821
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25637
25822
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25646,20 +25831,23 @@ var SqliteActivityRepository = class {
25646
25831
  const inClause = placeholders(sessionIds.length);
25647
25832
  const lastActivityRows = allRows(
25648
25833
  this.db.prepare(
25649
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25650
- WHERE root_session_id IN (${inClause})
25651
- GROUP BY root_session_id`
25834
+ `SELECT ids.value AS id,
25835
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25836
+ (SELECT max(ended_at) FROM audit_events e
25837
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25838
+ FROM json_each(?) AS ids`
25652
25839
  ),
25653
- sessionIds
25840
+ [JSON.stringify(sessionIds)]
25654
25841
  );
25655
25842
  for (const row of lastActivityRows) {
25656
- if (row.id === null) continue;
25657
25843
  const entry = result.get(row.id);
25658
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25844
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25845
+ if (entry && last > 0) entry.lastActivityMs = last;
25659
25846
  }
25660
25847
  const turnsRows = allRows(
25661
25848
  this.db.prepare(
25662
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25849
+ `SELECT root_session_id AS id, count(*) AS n
25850
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25663
25851
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25664
25852
  GROUP BY root_session_id`
25665
25853
  ),
@@ -25674,7 +25862,7 @@ var SqliteActivityRepository = class {
25674
25862
  this.db.prepare(
25675
25863
  `SELECT root_session_id AS id,
25676
25864
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25677
- FROM audit_events
25865
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25678
25866
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25679
25867
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25680
25868
  GROUP BY root_session_id`
@@ -25704,7 +25892,7 @@ var SqliteActivityRepository = class {
25704
25892
  this.db.prepare(
25705
25893
  `SELECT root_session_id AS id,
25706
25894
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25707
- FROM audit_events
25895
+ FROM audit_events INDEXED BY idx_audit_session_share
25708
25896
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25709
25897
  GROUP BY root_session_id`
25710
25898
  ),
@@ -26733,7 +26921,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26733
26921
 
26734
26922
  // ../../packages/persistence/src/repositories/findings.ts
26735
26923
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26736
- var SCAN_BATCH_ROWS = 1e3;
26737
26924
  var DEFAULT_LOCATIONS_LIMIT = 100;
26738
26925
  var LOCATION_RULE_IDS_CAP = 20;
26739
26926
  function compareLocationOrder(a, b) {
@@ -26762,6 +26949,25 @@ function deriveInstanceStatus(row) {
26762
26949
  latestResolutionStatus: row.latest_status
26763
26950
  });
26764
26951
  }
26952
+ function toFlatFindingRow(r) {
26953
+ return {
26954
+ id: r.id,
26955
+ ruleId: r.rule_id,
26956
+ category: r.category,
26957
+ severity: r.severity,
26958
+ maskedMatch: r.masked_match,
26959
+ actionTaken: r.action_taken,
26960
+ confidence: r.confidence,
26961
+ occurredAt: epochMillisToIso(r.occurred_at),
26962
+ sourceTool: r.source_tool,
26963
+ repo: r.repo ?? "",
26964
+ file: r.file ?? "",
26965
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26966
+ eventId: r.event_id,
26967
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26968
+ status: deriveInstanceStatus(r)
26969
+ };
26970
+ }
26765
26971
  function encodeGroupCursor(group) {
26766
26972
  const payload = {
26767
26973
  sev: group.severity,
@@ -26837,7 +27043,7 @@ var SqliteFindingsRepository = class {
26837
27043
  this.db.prepare(
26838
27044
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26839
27045
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26840
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27046
+ e.source_tool AS source_tool,
26841
27047
  e.event_type AS kind
26842
27048
  FROM audit_events e
26843
27049
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26945,56 +27151,11 @@ var SqliteFindingsRepository = class {
26945
27151
  predicate,
26946
27152
  params: sessionParams
26947
27153
  });
26948
- const rows = allRows(
26949
- this.db.prepare(
26950
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26951
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26952
- kind, finding_key, latest_status
26953
- FROM (
26954
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26955
- d.severity AS severity, f.masked_match AS masked_match,
26956
- f.action_taken AS action_taken, f.confidence AS confidence,
26957
- e.started_at AS occurred_at,
26958
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26959
- json_extract(e.attributes, '$.repo') AS repo,
26960
- json_extract(e.attributes, '$.file_path') AS file,
26961
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26962
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26963
- e.event_type AS kind, f.finding_key AS finding_key,
26964
- latest.status AS latest_status,
26965
- ROW_NUMBER() OVER (
26966
- PARTITION BY d.rule_id
26967
- ORDER BY e.started_at DESC, f.id DESC
26968
- ) AS rn
26969
- FROM inspection_findings f
26970
- JOIN audit_events e ON e.id = f.audit_event_id
26971
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26972
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26973
- ON latest.finding_key = f.finding_key
26974
- ${predicate}
26975
- )
26976
- WHERE rn <= :cap
26977
- ORDER BY occurred_at DESC, id DESC`
26978
- ),
26979
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26980
- );
26981
- const groupable = rows.map((r) => ({
26982
- id: r.id,
26983
- ruleId: r.rule_id,
26984
- category: r.category,
26985
- severity: r.severity,
26986
- maskedMatch: r.masked_match,
26987
- actionTaken: r.action_taken,
26988
- confidence: r.confidence,
26989
- occurredAt: epochMillisToIso(r.occurred_at),
26990
- sourceTool: r.source_tool,
26991
- repo: r.repo ?? "",
26992
- file: r.file ?? "",
26993
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26994
- eventId: r.event_id,
26995
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26996
- status: deriveInstanceStatus(r)
26997
- }));
27154
+ const rows = this.previewRows(aggregates, {
27155
+ sessionId: query.sessionId,
27156
+ from: query.from
27157
+ });
27158
+ const groupable = rows.map(toFlatFindingRow);
26998
27159
  const allGroups = buildFindingGroups(groupable, { aggregates });
26999
27160
  const filterOpts = {
27000
27161
  severity: query.severity,
@@ -27080,8 +27241,10 @@ var SqliteFindingsRepository = class {
27080
27241
  *
27081
27242
  * The scan runs from the top of the scope on every request, not from the
27082
27243
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
27083
- * move as the caller pages. Rows are pulled in batches so memory stays flat
27084
- * while the counting runs, and only the page itself is retained.
27244
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27245
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27246
+ * counting runs — a generator streaming the index order, not a sequence of
27247
+ * fetched batches; only the page itself is retained.
27085
27248
  */
27086
27249
  listFindingInstances(query) {
27087
27250
  const opts = {
@@ -27097,6 +27260,10 @@ var SqliteFindingsRepository = class {
27097
27260
  };
27098
27261
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27099
27262
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27263
+ const isPastCursor = cursor === null ? () => true : (row) => {
27264
+ const rowMs = isoToEpochMillis(row.occurredAt);
27265
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27266
+ };
27100
27267
  const accumulator = createInstanceFacetAccumulator(opts);
27101
27268
  const items = [];
27102
27269
  let total = 0;
@@ -27109,6 +27276,7 @@ var SqliteFindingsRepository = class {
27109
27276
  accumulator.add(row);
27110
27277
  if (!matchesInstanceFilters(row, opts)) continue;
27111
27278
  total += 1;
27279
+ if (!isPastCursor(row)) continue;
27112
27280
  if (items.length < limit) {
27113
27281
  items.push(toInstanceDetail(row));
27114
27282
  last = row;
@@ -27117,15 +27285,6 @@ var SqliteFindingsRepository = class {
27117
27285
  }
27118
27286
  }
27119
27287
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27120
- if (cursor !== null) {
27121
- const resumed = this.pageAfter(cursor, opts, limit, query);
27122
- return Promise.resolve({
27123
- totals: { findings: total },
27124
- facets: accumulator.facets(),
27125
- items: resumed.items,
27126
- nextCursor: resumed.nextCursor
27127
- });
27128
- }
27129
27288
  return Promise.resolve({
27130
27289
  totals: { findings: total },
27131
27290
  facets: accumulator.facets(),
@@ -27133,35 +27292,6 @@ var SqliteFindingsRepository = class {
27133
27292
  nextCursor
27134
27293
  });
27135
27294
  }
27136
- /**
27137
- * The page of matching rows strictly after `cursor`. Separate from the
27138
- * counting pass because that one starts at the top of the scope by design;
27139
- * this one narrows the scan with the same keyset predicate the activity list
27140
- * uses, so a later page costs less than the first rather than more.
27141
- */
27142
- pageAfter(cursor, opts, limit, query) {
27143
- const items = [];
27144
- let last;
27145
- let hasMore = false;
27146
- for (const row of this.scanFindingRows({
27147
- sessionId: query.sessionId,
27148
- from: query.from,
27149
- after: cursor
27150
- })) {
27151
- if (!matchesInstanceFilters(row, opts)) continue;
27152
- if (items.length < limit) {
27153
- items.push(toInstanceDetail(row));
27154
- last = row;
27155
- } else {
27156
- hasMore = true;
27157
- break;
27158
- }
27159
- }
27160
- return {
27161
- items,
27162
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27163
- };
27164
- }
27165
27295
  /**
27166
27296
  * The same findings folded by location: repository, then file within it.
27167
27297
  *
@@ -27244,25 +27374,111 @@ var SqliteFindingsRepository = class {
27244
27374
  });
27245
27375
  }
27246
27376
  /**
27247
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27377
+ * Each group's newest instances, for the table's expanded rows.
27378
+ *
27379
+ * ONE index-ordered scan with early termination, and the shape is the point.
27380
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27381
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27382
+ * through a temp B-tree to keep a bounded preview of each group, and then
27383
+ * sorts the survivors again for the page order. Both sorts grow with the
27384
+ * store while the answer does not.
27385
+ *
27386
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27387
+ * (or the session or window index the scope names — see `findingScanSql`),
27388
+ * which is already the order the page wants, and keeps rows per rule until
27389
+ * each rule has as many as it can show. The aggregate the caller already holds
27390
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27391
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27392
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27393
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27394
+ * store with many firing rules widens it. The bound that DOES hold
27395
+ * unconditionally is the sorted form's floor: this scan visits at most as
27396
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27397
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27398
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27399
+ * wanted instances sitting at the tail of the scope — is one pass over
27400
+ * everything in scope with a block sort of the id tie-break only, never a
27401
+ * sort of the scope, which is still that floor.
27402
+ *
27403
+ * A row whose rule the aggregate did not see is skipped: the two statements
27404
+ * run without a shared snapshot, so a capture landing between them can add a
27405
+ * rule here that has no counts there, and the counts are what the group is
27406
+ * built from.
27407
+ */
27408
+ previewRows(aggregates, scope) {
27409
+ const wanted = /* @__PURE__ */ new Map();
27410
+ let remaining = 0;
27411
+ for (const [ruleId, agg] of aggregates) {
27412
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27413
+ wanted.set(ruleId, n);
27414
+ remaining += n;
27415
+ }
27416
+ const rows = [];
27417
+ if (remaining === 0) return rows;
27418
+ const { sql, params } = this.findingScanSql(scope);
27419
+ const taken = /* @__PURE__ */ new Map();
27420
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27421
+ const want = wanted.get(r.rule_id);
27422
+ if (want === void 0) continue;
27423
+ const have = taken.get(r.rule_id) ?? 0;
27424
+ if (have >= want) continue;
27425
+ taken.set(r.rule_id, have + 1);
27426
+ rows.push(r);
27427
+ remaining -= 1;
27428
+ if (remaining === 0) break;
27429
+ }
27430
+ return rows;
27431
+ }
27432
+ /**
27433
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27248
27434
  *
27249
27435
  * A generator so a caller streams the scope without it ever being an array:
27250
27436
  * the flat list counts and facets the whole filtered scope, which on a large
27251
- * store is far more rows than any page. Each batch advances the same keyset
27252
- * predicate the page read uses, so the scan is a sequence of bounded reads
27253
- * rather than one unbounded result set.
27254
- *
27255
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27256
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27257
- * makes it a point lookup per row, and the derived table would re-materialize
27258
- * a window over the whole resolution table once per batch.
27437
+ * store is far more rows than any page. The rows come off ONE statement,
27438
+ * iterated rather than materialized, in the index order `findingScanSql`
27439
+ * arranges so the scan is a single pass with a block sort of the id
27440
+ * tie-break only, never a sort of the scope, where a sequence of
27441
+ * keyset-bounded batches re-sorted everything below the cursor on every
27442
+ * batch and cost the square of the scope.
27259
27443
  *
27260
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27261
- * would be missing from its own facet, which is computed by excluding that
27262
- * dimension see listFindingInstances.
27444
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27445
+ * dimension narrowed here would be missing from its own facet, which is
27446
+ * computed by excluding that dimension (see listFindingInstances). There is
27447
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27448
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27449
+ * narrower statement, since the counting pass already visits every row a
27450
+ * page-2+ request would otherwise re-seek for.
27263
27451
  */
27264
27452
  *scanFindingRows(scope) {
27265
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27453
+ const { sql, params } = this.findingScanSql(scope);
27454
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27455
+ yield toFlatFindingRow(r);
27456
+ }
27457
+ }
27458
+ /**
27459
+ * The one statement both instance-level scans run: every finding in scope,
27460
+ * joined to its event and definition, newest first.
27461
+ *
27462
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27463
+ * the same two `recentFindings` documents at length, for the same reason:
27464
+ *
27465
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27466
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27467
+ * yields `started_at` order per event type, not across the four, so
27468
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27469
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27470
+ * `idx_audit_session` for a session scope, which is also `started_at`
27471
+ * ordered within the session — and the order falls out of the index.
27472
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27473
+ * JOINs the planner drives from the findings and sorts everything.
27474
+ *
27475
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27476
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27477
+ * index probe per keyed row, and a derived table over the whole resolution
27478
+ * table would be materialized before the first row streamed.
27479
+ */
27480
+ findingScanSql(scope) {
27481
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27266
27482
  const params = [];
27267
27483
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27268
27484
  conditions.push("e.root_session_id = ?");
@@ -27276,58 +27492,24 @@ var SqliteFindingsRepository = class {
27276
27492
  d.severity AS severity, f.masked_match AS masked_match,
27277
27493
  f.action_taken AS action_taken, f.confidence AS confidence,
27278
27494
  e.started_at AS occurred_at,
27279
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27280
- json_extract(e.attributes, '$.repo') AS repo,
27281
- json_extract(e.attributes, '$.file_path') AS file,
27282
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27495
+ e.source_tool AS source_tool,
27496
+ e.repo AS repo,
27497
+ e.file_path AS file,
27498
+ e.tool_name AS tool_name,
27283
27499
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27284
27500
  e.event_type AS kind, f.finding_key AS finding_key,
27285
27501
  ${latestResolutionStatusSql("f")} AS latest_status
27286
- FROM inspection_findings f
27287
- JOIN audit_events e ON e.id = f.audit_event_id
27288
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27502
+ FROM audit_events e
27503
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27504
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27289
27505
  WHERE ${conditions.join(" AND ")}
27290
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27291
- ORDER BY e.started_at DESC, f.id DESC
27292
- LIMIT ?`;
27293
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27294
- for (; ; ) {
27295
- const rows = allRows(this.db.prepare(sql), [
27296
- ...params,
27297
- after.startedAtMs,
27298
- after.startedAtMs,
27299
- after.id,
27300
- SCAN_BATCH_ROWS
27301
- ]);
27302
- for (const r of rows) {
27303
- yield {
27304
- id: r.id,
27305
- ruleId: r.rule_id,
27306
- category: r.category,
27307
- severity: r.severity,
27308
- maskedMatch: r.masked_match,
27309
- actionTaken: r.action_taken,
27310
- confidence: r.confidence,
27311
- occurredAt: epochMillisToIso(r.occurred_at),
27312
- sourceTool: r.source_tool,
27313
- repo: r.repo ?? "",
27314
- file: r.file ?? "",
27315
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27316
- eventId: r.event_id,
27317
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27318
- status: deriveInstanceStatus(r)
27319
- };
27320
- }
27321
- if (rows.length < SCAN_BATCH_ROWS) return;
27322
- const lastRow = rows[rows.length - 1];
27323
- if (lastRow === void 0) return;
27324
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27325
- }
27506
+ ORDER BY e.started_at DESC, f.id DESC`;
27507
+ return { sql, params };
27326
27508
  }
27327
27509
  groupAggregates(withSearchText, scope) {
27328
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27329
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27330
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27510
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27511
+ group_concat(DISTINCT e.file_path) AS files,
27512
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27331
27513
  const rows = this.db.prepare(
27332
27514
  `SELECT rule_id,
27333
27515
  sum(tuple_count) AS instance_count,
@@ -27345,7 +27527,7 @@ var SqliteFindingsRepository = class {
27345
27527
  coalesce(latest.status, '') AS status_tuple,
27346
27528
  count(*) AS tuple_count,
27347
27529
  max(e.started_at) AS latest_at,
27348
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27530
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27349
27531
  group_concat(DISTINCT f.action_taken) AS actions_taken
27350
27532
  ${innerSearchColumns}
27351
27533
  FROM inspection_findings f
@@ -27476,6 +27658,8 @@ function isoDay(ms) {
27476
27658
  // ../../packages/persistence/src/repositories/history-sync.ts
27477
27659
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27478
27660
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27661
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27662
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27479
27663
  var SKIPPED = -1;
27480
27664
  var ROW_COLUMNS = `id,
27481
27665
  parent_id AS parentId,
@@ -27515,6 +27699,20 @@ var SqliteHistorySyncRepository = class {
27515
27699
  ORDER BY (event_type = 'session') DESC, started_at
27516
27700
  LIMIT :limit`
27517
27701
  );
27702
+ this.captureRowsStmt = db.prepare(
27703
+ `SELECT ${ROW_COLUMNS}
27704
+ FROM audit_events
27705
+ WHERE synced_at IS NULL
27706
+ AND sync_claimed_at IS NULL
27707
+ AND outbox_owed = 1
27708
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27709
+ AND started_at < :before
27710
+ ORDER BY started_at
27711
+ LIMIT :limit`
27712
+ );
27713
+ this.markOwedStmt = db.prepare(
27714
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27715
+ );
27518
27716
  this.stampStmt = db.prepare(
27519
27717
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27520
27718
  );
@@ -27546,6 +27744,12 @@ var SqliteHistorySyncRepository = class {
27546
27744
  FROM audit_events
27547
27745
  WHERE event_type IN (${TYPE_LIST})`
27548
27746
  );
27747
+ this.captureSkipCountStmt = db.prepare(
27748
+ `SELECT COUNT(*) AS skipped
27749
+ FROM audit_events
27750
+ WHERE synced_at = ${String(SKIPPED)}
27751
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27752
+ );
27549
27753
  this.fingerprintStmt = db.prepare(
27550
27754
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27551
27755
  FROM history_sync WHERE id = 1`
@@ -27555,6 +27759,10 @@ var SqliteHistorySyncRepository = class {
27555
27759
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27556
27760
  WHERE id = 1`
27557
27761
  );
27762
+ this.disownCapturesStmt = db.prepare(
27763
+ `UPDATE audit_events SET outbox_owed = NULL
27764
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27765
+ );
27558
27766
  this.rearmStmt = db.prepare(
27559
27767
  `UPDATE audit_events SET synced_at = NULL
27560
27768
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27627,6 +27835,10 @@ var SqliteHistorySyncRepository = class {
27627
27835
  closeWindowStmt;
27628
27836
  releaseBoundaryStmt;
27629
27837
  freezeBoundaryStmt;
27838
+ captureRowsStmt;
27839
+ markOwedStmt;
27840
+ captureSkipCountStmt;
27841
+ disownCapturesStmt;
27630
27842
  partitionStmt;
27631
27843
  claimRowStmt;
27632
27844
  releaseRowStmt;
@@ -27660,6 +27872,34 @@ var SqliteHistorySyncRepository = class {
27660
27872
  pendingRows(sessionId, limit, before) {
27661
27873
  return allRows(this.rowsStmt, { sessionId, limit, before });
27662
27874
  }
27875
+ /**
27876
+ * Captures this machine still owes the deployment, oldest first.
27877
+ *
27878
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27879
+ * by a time window — see captureRowsStmt for why a window could not express
27880
+ * this. `before` is the grace window that leaves a just-recorded capture to
27881
+ * the live path.
27882
+ */
27883
+ pendingCaptureRows(limit, before) {
27884
+ return allRows(this.captureRowsStmt, { limit, before });
27885
+ }
27886
+ /**
27887
+ * Record that a capture is OWED to the deployment.
27888
+ *
27889
+ * Written by the attached forward path when a live send did not confirm
27890
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27891
+ * a fact rather than an inference: the machine was attached, the send did not
27892
+ * land, so the row is owed — which no time window can state, because the same
27893
+ * window that holds the rows a past attachment left owed also holds every
27894
+ * capture recorded while the machine was DETACHED, and those were never
27895
+ * offered to anyone.
27896
+ *
27897
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27898
+ * out of the drain's read.
27899
+ */
27900
+ markCaptureOwed(id) {
27901
+ this.markOwedStmt.run({ id });
27902
+ }
27663
27903
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27664
27904
  markSynced(ids, atMs) {
27665
27905
  this.stampAll(ids, atMs);
@@ -27743,10 +27983,12 @@ var SqliteHistorySyncRepository = class {
27743
27983
  this.countsStmt,
27744
27984
  { before }
27745
27985
  );
27986
+ const captures = getRow(this.captureSkipCountStmt);
27746
27987
  return {
27747
27988
  pending: row?.pending ?? 0,
27748
27989
  sent: row?.sent ?? 0,
27749
- skipped: row?.skipped ?? 0
27990
+ skipped: row?.skipped ?? 0,
27991
+ capturesSkipped: captures?.skipped ?? 0
27750
27992
  };
27751
27993
  }
27752
27994
  /**
@@ -27787,7 +28029,11 @@ var SqliteHistorySyncRepository = class {
27787
28029
  withTransaction(
27788
28030
  this.db,
27789
28031
  () => {
28032
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27790
28033
  this.rearmStmt.run();
28034
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28035
+ this.disownCapturesStmt.run();
28036
+ }
27791
28037
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27792
28038
  },
27793
28039
  "IMMEDIATE"
@@ -27984,7 +28230,259 @@ var SqliteInspectionFindingsRepository = class {
27984
28230
  };
27985
28231
 
27986
28232
  // ../../packages/persistence/src/repositories/installed-packs.ts
27987
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28233
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28234
+
28235
+ // ../../packages/persistence/src/policy-floor.ts
28236
+ import { readFileSync as readFileSync5 } from "fs";
28237
+ import { join as join6 } from "path";
28238
+
28239
+ // ../../packages/persistence/src/local-layout.ts
28240
+ import { renameSync as renameSync3 } from "fs";
28241
+ import { mkdir } from "fs/promises";
28242
+ import { homedir } from "os";
28243
+ import { join as join4 } from "path";
28244
+ function defaultDataDir() {
28245
+ return join4(homedir(), ".aka");
28246
+ }
28247
+ function settingsDir(base = defaultDataDir()) {
28248
+ return join4(base, "settings");
28249
+ }
28250
+ function dataDir(base = defaultDataDir()) {
28251
+ return join4(base, "data");
28252
+ }
28253
+ function dbPath(base = defaultDataDir()) {
28254
+ return join4(dataDir(base), "aka.db");
28255
+ }
28256
+ function keysDir(base = defaultDataDir()) {
28257
+ return join4(base, "keys");
28258
+ }
28259
+ async function ensureDataDir(dir = defaultDataDir()) {
28260
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
28261
+ tightenDir(dir);
28262
+ }
28263
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28264
+ ensureDataDirSync(dir);
28265
+ }
28266
+ function migrateLegacyLayout(base = defaultDataDir()) {
28267
+ const moves = [
28268
+ { name: "config.json", dest: settingsDir(base) },
28269
+ { name: "policy-cache.json", dest: dataDir(base) }
28270
+ ];
28271
+ for (const { name, dest } of moves) {
28272
+ try {
28273
+ ensureDataDirSync(dest);
28274
+ const moved = join4(dest, name);
28275
+ renameSync3(join4(base, name), moved);
28276
+ tightenFile(moved);
28277
+ } catch {
28278
+ }
28279
+ }
28280
+ }
28281
+
28282
+ // ../../packages/persistence/src/settings.ts
28283
+ import { readFileSync as readFileSync4 } from "fs";
28284
+ import { join as join5 } from "path";
28285
+
28286
+ // ../../packages/persistence/src/file-lock.ts
28287
+ import { randomUUID as randomUUID3 } from "crypto";
28288
+ import {
28289
+ closeSync,
28290
+ existsSync as existsSync2,
28291
+ openSync,
28292
+ readFileSync as readFileSync2,
28293
+ rmSync as rmSync5,
28294
+ statSync as statSync3,
28295
+ writeFileSync as writeFileSync2
28296
+ } from "fs";
28297
+ import { hostname as hostname3 } from "os";
28298
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28299
+
28300
+ // ../../packages/persistence/src/managed-settings.ts
28301
+ import { readFileSync as readFileSync3 } from "fs";
28302
+ import { posix, win32 } from "path";
28303
+ function managedSettingsPaths(platform2 = process.platform) {
28304
+ if (platform2 === "darwin") {
28305
+ return [
28306
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28307
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28308
+ ];
28309
+ }
28310
+ if (platform2 === "win32") {
28311
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28312
+ }
28313
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28314
+ }
28315
+ function readManagedSettings(paths = managedSettingsPaths()) {
28316
+ for (const path of paths) {
28317
+ let text;
28318
+ try {
28319
+ text = readFileSync3(path, "utf8");
28320
+ } catch {
28321
+ continue;
28322
+ }
28323
+ const record2 = parseJsonObject(text);
28324
+ if (!record2) continue;
28325
+ const parsed2 = ManagedSettings.safeParse(record2);
28326
+ if (parsed2.success) return parsed2.data;
28327
+ }
28328
+ return null;
28329
+ }
28330
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28331
+ if (!managed) return settings;
28332
+ const { values } = managed;
28333
+ const merged = { ...settings };
28334
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28335
+ if (values.controlPlane !== void 0) {
28336
+ merged.controlPlane = {
28337
+ ...values.controlPlane,
28338
+ // The administrator pinned WHICH deployment, not WHEN this machine
28339
+ // joined it. Keep the user's own attach time when the endpoint is
28340
+ // unchanged, so a managed machine does not appear to re-attach on every
28341
+ // read; stamp a fresh one when the administrator moved it.
28342
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28343
+ };
28344
+ }
28345
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28346
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28347
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28348
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28349
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28350
+ if (values.vaultConsent !== void 0) {
28351
+ merged.vaultConsent = values.vaultConsent ? (
28352
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28353
+ // at the current version otherwise.
28354
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28355
+ ) : void 0;
28356
+ }
28357
+ if (values.modelJudgeConsent !== void 0) {
28358
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28359
+ acknowledgedAt: now().toISOString(),
28360
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28361
+ } : void 0;
28362
+ }
28363
+ return merged;
28364
+ }
28365
+
28366
+ // ../../packages/persistence/src/settings.ts
28367
+ var SETTINGS_FILENAME = "settings.json";
28368
+ function readWorkspaceSettings(base = defaultDataDir()) {
28369
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28370
+ }
28371
+ function readUserSettings(base) {
28372
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28373
+ if (!record2) return defaultWorkspaceSettings();
28374
+ try {
28375
+ return WorkspaceSettings.parse(record2);
28376
+ } catch {
28377
+ return defaultWorkspaceSettings();
28378
+ }
28379
+ }
28380
+ function readJson(file2) {
28381
+ let text;
28382
+ try {
28383
+ text = readFileSync4(file2, "utf8");
28384
+ } catch {
28385
+ return null;
28386
+ }
28387
+ return parseJsonObject(text) ?? null;
28388
+ }
28389
+
28390
+ // ../../packages/persistence/src/policy-floor.ts
28391
+ function refusalMessage(pack, attempted, floor, refusal) {
28392
+ switch (refusal) {
28393
+ case "lock":
28394
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28395
+ case "disable":
28396
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28397
+ case "floor":
28398
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28399
+ }
28400
+ }
28401
+ var PolicyFloorError = class extends Error {
28402
+ /** `namespace/packId` of the detection whose write was refused. */
28403
+ pack;
28404
+ /**
28405
+ * The archetype the caller asked for, or null when the write named none —
28406
+ * clearing the assignment, or switching the detection off.
28407
+ */
28408
+ attempted;
28409
+ /** The weakest archetype the control plane permits for this pack. */
28410
+ floor;
28411
+ refusal;
28412
+ constructor(pack, attempted, floor, refusal) {
28413
+ super(refusalMessage(pack, attempted, floor, refusal));
28414
+ this.name = "PolicyFloorError";
28415
+ this.pack = pack;
28416
+ this.attempted = attempted;
28417
+ this.floor = floor;
28418
+ this.refusal = refusal;
28419
+ }
28420
+ };
28421
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28422
+ try {
28423
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28424
+ const parsed2 = JSON.parse(raw);
28425
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28426
+ return PolicyBundle.parse(parsed2.bundle);
28427
+ } catch {
28428
+ return null;
28429
+ }
28430
+ }
28431
+ function indexEnabled(policies) {
28432
+ const byRuleId = /* @__PURE__ */ new Map();
28433
+ const byCategory = /* @__PURE__ */ new Map();
28434
+ for (const policy of policies) {
28435
+ if (!policy.enabled) continue;
28436
+ if ("ruleId" in policy.target) {
28437
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28438
+ } else if (!byCategory.has(policy.target.category)) {
28439
+ byCategory.set(policy.target.category, policy.action);
28440
+ }
28441
+ }
28442
+ return { byRuleId, byCategory };
28443
+ }
28444
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28445
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28446
+ const categories = new Set(rules.map((rule) => rule.category));
28447
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28448
+ return policies.some((policy) => {
28449
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28450
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28451
+ });
28452
+ }
28453
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28454
+ const floors = openControlPlaneFloors(base);
28455
+ return floors === null ? null : floors.floorFor(rules);
28456
+ }
28457
+ function openControlPlaneFloors(base = defaultDataDir()) {
28458
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28459
+ const bundle = readCachedPolicyBundle(base);
28460
+ if (bundle === null) return null;
28461
+ const indexes = indexEnabled(bundle.policies);
28462
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28463
+ }
28464
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28465
+ let action = null;
28466
+ for (const rule of rules) {
28467
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28468
+ if (resolved === void 0) continue;
28469
+ action = action === null ? resolved : strongerAction(action, resolved);
28470
+ }
28471
+ if (action === null) return null;
28472
+ return {
28473
+ floor: weakestBuiltinAtLeast(action),
28474
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28475
+ };
28476
+ }
28477
+ function policyAssignmentRefusal(policyId, floor) {
28478
+ if (floor.locked) return "lock";
28479
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28480
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28481
+ }
28482
+ function packEnablementRefusal(enabled, floor) {
28483
+ if (floor === null || enabled) return null;
28484
+ return "disable";
28485
+ }
27988
28486
 
27989
28487
  // ../../packages/persistence/src/semver.ts
27990
28488
  function parse3(version2) {
@@ -28078,8 +28576,19 @@ function ruleIdsOf(rulesJson) {
28078
28576
  return ids;
28079
28577
  }
28080
28578
  var SqliteInstalledPacksRepository = class {
28081
- constructor(db) {
28579
+ /**
28580
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28581
+ * floor needs both halves of it (settings/ says whether this machine is
28582
+ * attached, data/ holds the cached bundle). It is optional because a caller
28583
+ * holding only a DatabaseSync — every test construction site, and any embedder
28584
+ * that opens the store itself — has no layout to point at, and such a caller
28585
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28586
+ * from `openLocalDatabase`, which is the single construction site that owns a
28587
+ * real `~/.aka`.
28588
+ */
28589
+ constructor(db, baseDir) {
28082
28590
  this.db = db;
28591
+ this.baseDir = baseDir;
28083
28592
  this.insertMissingStmt = db.prepare(
28084
28593
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
28085
28594
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28101,11 +28610,17 @@ var SqliteInstalledPacksRepository = class {
28101
28610
  this.signatureStmt = db.prepare(
28102
28611
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28103
28612
  );
28613
+ this.packRulesStmt = db.prepare(
28614
+ `SELECT rules_json AS rulesJson FROM installed_packs
28615
+ WHERE namespace = ? AND pack_id = ?`
28616
+ );
28104
28617
  }
28105
28618
  db;
28619
+ baseDir;
28106
28620
  insertMissingStmt;
28107
28621
  upsertAvailableStmt;
28108
28622
  signatureStmt;
28623
+ packRulesStmt;
28109
28624
  /**
28110
28625
  * Record the running binary's detection inventory. Refreshes the
28111
28626
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28147,7 +28662,7 @@ var SqliteInstalledPacksRepository = class {
28147
28662
  let behind = false;
28148
28663
  for (const row of rows) {
28149
28664
  const params = {
28150
- id: randomUUID3(),
28665
+ id: randomUUID4(),
28151
28666
  namespace: row.namespace,
28152
28667
  packId: row.packId,
28153
28668
  version: row.version,
@@ -28159,7 +28674,7 @@ var SqliteInstalledPacksRepository = class {
28159
28674
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28160
28675
  this.upsertAvailableStmt.run({
28161
28676
  ...params,
28162
- id: randomUUID3(),
28677
+ id: randomUUID4(),
28163
28678
  recordedBy: meta4?.recordedBy ?? null
28164
28679
  });
28165
28680
  } else {
@@ -28405,9 +28920,65 @@ var SqliteInstalledPacksRepository = class {
28405
28920
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28406
28921
  // caller rather than swallowing them. Each returns whether a row matched, so the
28407
28922
  // caller can tell an edit from a no-such-detection.
28923
+ /**
28924
+ * The rules one installed pack owns, reduced to what a floor computation
28925
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28926
+ * unreadable contributes no rules to a scan either, so it is not a detection
28927
+ * the control plane can be governing, and an empty list correctly imposes no
28928
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28929
+ * the user can re-enable, and its assignment stays governed meanwhile.
28930
+ */
28931
+ packFloorRules(namespace, packId) {
28932
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28933
+ if (!row) return [];
28934
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28935
+ }
28936
+ /**
28937
+ * What the connected control plane imposes on one installed pack, or null on a
28938
+ * machine that is its own authority (standalone, no cached bundle, or a
28939
+ * repository constructed without a layout base).
28940
+ *
28941
+ * Exposed as a READ so a surface can render the constraint — grey out the
28942
+ * choices below the floor, mark a locked detection as locked — rather than
28943
+ * offer the user a picker whose selections it will then be told it may not
28944
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28945
+ */
28946
+ policyFloor(namespace, packId) {
28947
+ if (this.baseDir === void 0) return null;
28948
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28949
+ }
28950
+ /**
28951
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28952
+ * entry only for a pack the control plane actually governs.
28953
+ *
28954
+ * A surface listing every detection asks per pack, and asking through
28955
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28956
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28957
+ * answer, repeated for each row, on every render. This reads all of that once.
28958
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28959
+ * exactly as the single-pack read returns null for them.
28960
+ */
28961
+ policyFloors(packs2) {
28962
+ const floors = /* @__PURE__ */ new Map();
28963
+ if (this.baseDir === void 0) return floors;
28964
+ const source = openControlPlaneFloors(this.baseDir);
28965
+ if (source === null) return floors;
28966
+ for (const pack of packs2) {
28967
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28968
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28969
+ }
28970
+ return floors;
28971
+ }
28408
28972
  /**
28409
28973
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28410
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28974
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28975
+ *
28976
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28977
+ * write below, and a detection the organization has authored a policy for is
28978
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28979
+ * a throw rather than a silently substituted value. This is the one device-local
28980
+ * write path for the assignment, so the check belongs here rather than on any
28981
+ * surface that offers the choice.
28411
28982
  */
28412
28983
  setPolicy(namespace, packId, policyId) {
28413
28984
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28415,14 +28986,38 @@ var SqliteInstalledPacksRepository = class {
28415
28986
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28416
28987
  );
28417
28988
  }
28989
+ const requested = policyId;
28990
+ const floor = this.policyFloor(namespace, packId);
28991
+ if (floor !== null) {
28992
+ const refusal = policyAssignmentRefusal(requested, floor);
28993
+ if (refusal !== null) {
28994
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28995
+ }
28996
+ }
28418
28997
  const res = this.db.prepare(
28419
28998
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28420
28999
  WHERE namespace = :namespace AND pack_id = :packId`
28421
29000
  ).run({ policyId, now: Date.now(), namespace, packId });
28422
29001
  return Number(res.changes) > 0;
28423
29002
  }
28424
- /** Enable or disable one installed pack. */
29003
+ /**
29004
+ * Enable or disable one installed pack.
29005
+ *
29006
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29007
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29008
+ * merely another point below the floor, and why re-enabling stays open. Like
29009
+ * the assignment above, the check belongs at this write path rather than on a
29010
+ * surface: this is the one device-local writer of the column, and a refusal
29011
+ * that lived in a page would leave the CLI free.
29012
+ */
28425
29013
  setEnabled(namespace, packId, enabled) {
29014
+ const floor = this.policyFloor(namespace, packId);
29015
+ if (floor !== null) {
29016
+ const refusal = packEnablementRefusal(enabled, floor);
29017
+ if (refusal !== null) {
29018
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29019
+ }
29020
+ }
28426
29021
  const res = this.db.prepare(
28427
29022
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28428
29023
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28508,7 +29103,7 @@ var SqliteInventoryRepository = class {
28508
29103
  };
28509
29104
 
28510
29105
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28511
- import { randomUUID as randomUUID4 } from "crypto";
29106
+ import { randomUUID as randomUUID5 } from "crypto";
28512
29107
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28513
29108
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28514
29109
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28997,7 +29592,7 @@ var SqliteInventoryAssetsRepository = class {
28997
29592
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28998
29593
  VALUES (:id, :projectId, :path, :access, :now, :now)
28999
29594
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
29000
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29595
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
29001
29596
  }
29002
29597
  return true;
29003
29598
  }
@@ -29018,7 +29613,7 @@ var SqliteInventoryAssetsRepository = class {
29018
29613
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
29019
29614
  VALUES (:id, :assetId, :trust, :now, :now)
29020
29615
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
29021
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29616
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
29022
29617
  }
29023
29618
  this.configRowsCache = void 0;
29024
29619
  return "ok";
@@ -29315,7 +29910,7 @@ var SqliteInventoryAssetsRepository = class {
29315
29910
  };
29316
29911
 
29317
29912
  // ../../packages/persistence/src/repositories/policies.ts
29318
- import { randomUUID as randomUUID5 } from "crypto";
29913
+ import { randomUUID as randomUUID6 } from "crypto";
29319
29914
  var SqlitePoliciesRepository = class {
29320
29915
  constructor(db) {
29321
29916
  this.db = db;
@@ -29350,7 +29945,7 @@ var SqlitePoliciesRepository = class {
29350
29945
  failOpenTransaction(this.db, () => {
29351
29946
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29352
29947
  stmt.run({
29353
- id: randomUUID5(),
29948
+ id: randomUUID6(),
29354
29949
  target: JSON.stringify({ category }),
29355
29950
  action,
29356
29951
  now: Date.now()
@@ -29370,7 +29965,7 @@ var SqlitePoliciesRepository = class {
29370
29965
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29371
29966
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29372
29967
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29373
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29968
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29374
29969
  }
29375
29970
  // Caps every global per-category policy currently set to block/redact down
29376
29971
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29438,7 +30033,7 @@ var SqlitePolicyCatalogRepository = class {
29438
30033
  };
29439
30034
 
29440
30035
  // ../../packages/persistence/src/repositories/project-files.ts
29441
- import { randomUUID as randomUUID6 } from "crypto";
30036
+ import { randomUUID as randomUUID7 } from "crypto";
29442
30037
  var SqliteProjectFilesRepository = class {
29443
30038
  constructor(db) {
29444
30039
  this.db = db;
@@ -29470,7 +30065,7 @@ var SqliteProjectFilesRepository = class {
29470
30065
  const stamp = Math.max(now, maxStamp + 1);
29471
30066
  for (const file2 of scan2.files) {
29472
30067
  this.upsertStmt.run({
29473
- id: randomUUID6(),
30068
+ id: randomUUID7(),
29474
30069
  projectId,
29475
30070
  path: file2.path,
29476
30071
  name: file2.name,
@@ -29484,9 +30079,9 @@ var SqliteProjectFilesRepository = class {
29484
30079
  };
29485
30080
 
29486
30081
  // ../../packages/persistence/src/repositories/resolutions.ts
29487
- import { randomUUID as randomUUID7 } from "crypto";
30082
+ import { randomUUID as randomUUID8 } from "crypto";
29488
30083
  var SqliteResolutionsRepository = class {
29489
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30084
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29490
30085
  this.db = db;
29491
30086
  this.now = now;
29492
30087
  this.newId = newId;
@@ -29699,7 +30294,7 @@ var SqliteScanLedgerRepository = class {
29699
30294
  };
29700
30295
 
29701
30296
  // ../../packages/persistence/src/repositories/secret-vault.ts
29702
- import { randomUUID as randomUUID8 } from "crypto";
30297
+ import { randomUUID as randomUUID9 } from "crypto";
29703
30298
  function pageLimit(requested, fallback) {
29704
30299
  if (requested === void 0) return fallback;
29705
30300
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29745,12 +30340,14 @@ var SELECT_COLUMNS = `
29745
30340
  ciphertext,
29746
30341
  nonce,
29747
30342
  auth_tag AS authTag,
30343
+ user_authorized AS userAuthorized,
29748
30344
  occurrence_count AS occurrenceCount,
29749
30345
  first_seen AS firstSeen,
29750
30346
  last_seen AS lastSeen`;
29751
30347
  function toRow(raw) {
29752
- const { provider, ...rest } = raw;
29753
- return provider === null ? rest : { ...rest, provider };
30348
+ const { provider, userAuthorized, ...rest } = raw;
30349
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30350
+ return provider === null ? row : { ...row, provider };
29754
30351
  }
29755
30352
  var SqliteSecretVaultRepository = class {
29756
30353
  constructor(db) {
@@ -29760,17 +30357,18 @@ var SqliteSecretVaultRepository = class {
29760
30357
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29761
30358
  format_version, category, rule_id, masked_match, provider,
29762
30359
  ciphertext, nonce, auth_tag,
29763
- occurrence_count, first_seen, last_seen
30360
+ user_authorized, occurrence_count, first_seen, last_seen
29764
30361
  ) VALUES (
29765
30362
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29766
30363
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29767
30364
  :ciphertext, :nonce, :authTag,
29768
- 1, :now, :now
30365
+ :userAuthorized, 1, :now, :now
29769
30366
  )`
29770
30367
  );
29771
30368
  this.bumpStmt = db.prepare(
29772
30369
  `UPDATE secret_vault
29773
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30370
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30371
+ user_authorized = max(user_authorized, :userAuthorized)
29774
30372
  WHERE value_fingerprint = :valueFingerprint`
29775
30373
  );
29776
30374
  this.byPointerStmt = db.prepare(
@@ -29790,6 +30388,7 @@ var SqliteSecretVaultRepository = class {
29790
30388
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29791
30389
  WHERE pointer_id = :pointerId`
29792
30390
  );
30391
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29793
30392
  this.derefStmt = db.prepare(
29794
30393
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29795
30394
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29803,6 +30402,7 @@ var SqliteSecretVaultRepository = class {
29803
30402
  listStmt;
29804
30403
  replaceCiphertextStmt;
29805
30404
  refreshFingerprintStmt;
30405
+ deleteByPointerStmt;
29806
30406
  derefStmt;
29807
30407
  /**
29808
30408
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29811,6 +30411,11 @@ var SqliteSecretVaultRepository = class {
29811
30411
  * pointer, category and ciphertext, so the same secret always resolves to one
29812
30412
  * wire token. `minted` is true only when this call created the row.
29813
30413
  *
30414
+ * `userAuthorized` is the one field a repeat call may still change, and only
30415
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30416
+ * the row is shared with every automatic path that vaults the same value. See
30417
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30418
+ *
29814
30419
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29815
30420
  * writers cannot both decide they are minting.
29816
30421
  */
@@ -29837,13 +30442,18 @@ var SqliteSecretVaultRepository = class {
29837
30442
  ciphertext: input2.ciphertext,
29838
30443
  nonce: input2.nonce,
29839
30444
  authTag: input2.authTag,
30445
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29840
30446
  now
29841
30447
  })
29842
30448
  );
29843
30449
  minted = true;
29844
30450
  return;
29845
30451
  }
29846
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30452
+ this.bumpStmt.run({
30453
+ valueFingerprint: input2.valueFingerprint,
30454
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30455
+ now
30456
+ });
29847
30457
  },
29848
30458
  "IMMEDIATE"
29849
30459
  );
@@ -29903,6 +30513,42 @@ var SqliteSecretVaultRepository = class {
29903
30513
  );
29904
30514
  return destroyed;
29905
30515
  }
30516
+ /**
30517
+ * Destroy the named entries and report WHICH ones went — the scoped
30518
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30519
+ * values back where they came from. Ids the store does not hold are absent
30520
+ * from the answer rather than an error, so a set assembled from a stale read
30521
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30522
+ * it.
30523
+ *
30524
+ * The ids come back rather than a count because the caller's next act is to
30525
+ * write a purge row per destroyed entry, and a record of destruction has to
30526
+ * be a record of what was really destroyed: a selection is a claim about a
30527
+ * read that has since gone stale, and auditing from it invents a purge for an
30528
+ * entry still sitting in the vault.
30529
+ *
30530
+ * One transaction over the whole set rather than a statement per id: the
30531
+ * caller hands this the result of a restore pass it has completed, and a
30532
+ * fault partway through must leave the vault as it was found rather than
30533
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30534
+ * stands for, so half a delete is not a state anything can recover from.
30535
+ */
30536
+ deleteByPointerIds(pointerIds) {
30537
+ if (pointerIds.length === 0) return [];
30538
+ const deleted = [];
30539
+ withTransaction(
30540
+ this.db,
30541
+ () => {
30542
+ for (const pointerId of pointerIds) {
30543
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30544
+ deleted.push(pointerId);
30545
+ }
30546
+ }
30547
+ },
30548
+ "IMMEDIATE"
30549
+ );
30550
+ return deleted;
30551
+ }
29906
30552
  /**
29907
30553
  * Record (or re-stamp) one place a pointer has been written. One row per
29908
30554
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29915,7 +30561,7 @@ var SqliteSecretVaultRepository = class {
29915
30561
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29916
30562
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29917
30563
  ).run({
29918
- id: randomUUID8(),
30564
+ id: randomUUID9(),
29919
30565
  pointerId: entry.pointerId,
29920
30566
  location: entry.location,
29921
30567
  kind: entry.kind,
@@ -30428,15 +31074,15 @@ var SqliteSecurityRepository = class {
30428
31074
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30429
31075
  const rows = allRows(
30430
31076
  this.db.prepare(
30431
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31077
+ `SELECT e.repo AS repo, count(*) AS c
30432
31078
  FROM inspection_findings f
30433
31079
  JOIN audit_events e ON e.id = f.audit_event_id
30434
31080
  WHERE e.started_at >= :from AND e.started_at < :to
30435
31081
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30436
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30437
- AND json_extract(e.attributes, '$.repo') != ''
30438
- GROUP BY repo
30439
- ORDER BY c DESC, repo
31082
+ AND e.repo IS NOT NULL
31083
+ AND e.repo != ''
31084
+ GROUP BY e.repo
31085
+ ORDER BY c DESC, e.repo
30440
31086
  LIMIT :limit`
30441
31087
  ),
30442
31088
  { from, to: now, limit }
@@ -30498,7 +31144,7 @@ var SqliteSecurityRepository = class {
30498
31144
  `SELECT f.finding_key AS finding_key,
30499
31145
  d.rule_id AS rule_id,
30500
31146
  d.severity AS severity,
30501
- json_extract(e.attributes, '$.file_path') AS path,
31147
+ e.file_path AS path,
30502
31148
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30503
31149
  latest.resolved_at AS latest_resolved_at
30504
31150
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30551,7 +31197,7 @@ var SqliteSecurityRepository = class {
30551
31197
  };
30552
31198
 
30553
31199
  // ../../packages/persistence/src/repositories/shares.ts
30554
- import { randomUUID as randomUUID9 } from "crypto";
31200
+ import { randomUUID as randomUUID10 } from "crypto";
30555
31201
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30556
31202
  var IN_CHUNK = 500;
30557
31203
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30639,7 +31285,7 @@ function buildSummary(dest, endpoints) {
30639
31285
  callSiteCount,
30640
31286
  transports: distinctTransports(transports),
30641
31287
  dataClasses: distinctDataClasses(dataClasses),
30642
- review: buildReviewInfo(dest.trust, transports),
31288
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30643
31289
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30644
31290
  endpoints: endpoints.map(toEndpointSummary)
30645
31291
  };
@@ -30666,7 +31312,7 @@ function buildDetail(dest, endpoints, callSites) {
30666
31312
  lastSeen: new Date(lastSeenMs).toISOString(),
30667
31313
  transports: distinctTransports(transports),
30668
31314
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30669
- review: buildReviewInfo(dest.trust, transports),
31315
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30670
31316
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30671
31317
  note: dest.note,
30672
31318
  endpoints: endpoints.map((ep) => ({
@@ -30695,7 +31341,11 @@ var SqliteSharesRepository = class {
30695
31341
  FROM share_destination d
30696
31342
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30697
31343
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30698
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31344
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31345
+ AND NOT EXISTS (
31346
+ SELECT 1 FROM egress_decision_override o
31347
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31348
+ )`
30699
31349
  );
30700
31350
  const kindCounts = countBy(
30701
31351
  this.db,
@@ -30807,7 +31457,7 @@ var SqliteSharesRepository = class {
30807
31457
  (id, destination_id, host, decision, created_at, updated_at)
30808
31458
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30809
31459
  ).run({
30810
- id: randomUUID9(),
31460
+ id: randomUUID10(),
30811
31461
  destinationId,
30812
31462
  host: dest.host,
30813
31463
  decision,
@@ -30956,7 +31606,7 @@ var SqliteSharesRepository = class {
30956
31606
  let destinationId = destIds.get(hit.host);
30957
31607
  if (destinationId === void 0) {
30958
31608
  destStmt.run({
30959
- id: randomUUID9(),
31609
+ id: randomUUID10(),
30960
31610
  kind: hit.kind,
30961
31611
  name: hit.name,
30962
31612
  host: hit.host,
@@ -30972,7 +31622,7 @@ var SqliteSharesRepository = class {
30972
31622
  let endpointId = endpointIds.get(endpointKey);
30973
31623
  if (endpointId === void 0) {
30974
31624
  endpointStmt.run({
30975
- id: randomUUID9(),
31625
+ id: randomUUID10(),
30976
31626
  destinationId,
30977
31627
  method: hit.method,
30978
31628
  transport: hit.transport,
@@ -30985,7 +31635,7 @@ var SqliteSharesRepository = class {
30985
31635
  endpointIds.set(endpointKey, endpointId);
30986
31636
  }
30987
31637
  siteStmt.run({
30988
- id: randomUUID9(),
31638
+ id: randomUUID10(),
30989
31639
  endpointId,
30990
31640
  project: input2.project,
30991
31641
  projectKey: input2.projectKey,
@@ -31350,6 +32000,7 @@ function purgeSampleData(db) {
31350
32000
  }
31351
32001
 
31352
32002
  // ../../packages/persistence/src/database.ts
32003
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31353
32004
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31354
32005
  "aka.persistence.unsafeTestOnlyRawHandle"
31355
32006
  );
@@ -31397,7 +32048,7 @@ function backupLegacyStore(db, file2) {
31397
32048
  discardStore(file2, backup);
31398
32049
  return backup;
31399
32050
  }
31400
- function openAndInitialize(file2) {
32051
+ function openAndInitialize(file2, base) {
31401
32052
  let db = openWithPragmas(file2);
31402
32053
  try {
31403
32054
  if (isForeignSqliteLineage(db)) {
@@ -31410,7 +32061,7 @@ function openAndInitialize(file2) {
31410
32061
  applyMigrations(db, file2);
31411
32062
  tightenPerms(file2);
31412
32063
  const policies = new SqlitePoliciesRepository(db);
31413
- const installedPacks = new SqliteInstalledPacksRepository(db);
32064
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31414
32065
  const repositories = {
31415
32066
  events: new SqliteEventsRepository(db),
31416
32067
  findings: new SqliteFindingsRepository(db),
@@ -31446,7 +32097,7 @@ function openAndInitialize(file2) {
31446
32097
  }
31447
32098
  function openLocalDatabase(dir) {
31448
32099
  ensureDataDirSync(dir);
31449
- const file2 = join4(dir, DB_FILENAME);
32100
+ const file2 = join7(dir, DB_FILENAME);
31450
32101
  reapStalePartials(file2);
31451
32102
  const {
31452
32103
  db,
@@ -31474,7 +32125,13 @@ function openLocalDatabase(dir) {
31474
32125
  inspectionDefinitions,
31475
32126
  inspectionFindings,
31476
32127
  configInventory
31477
- } = openAndInitialize(file2);
32128
+ } = openAndInitialize(
32129
+ file2,
32130
+ // `dir` is always `<base>/data` — every caller resolves it through
32131
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32132
+ // settings/ and data/, and the pack-policy floor needs both halves.
32133
+ dirname2(dir)
32134
+ );
31478
32135
  function captureRowId(event) {
31479
32136
  return captureId(
31480
32137
  event.metadata?.sessionId ?? null,
@@ -31487,6 +32144,21 @@ function openLocalDatabase(dir) {
31487
32144
  historySync.markSynced([captureRowId(event)], atMs);
31488
32145
  });
31489
32146
  }
32147
+ function markCaptureOwed(event) {
32148
+ failOpenTransaction(db, () => {
32149
+ historySync.markCaptureOwed(captureRowId(event));
32150
+ });
32151
+ }
32152
+ function markAuditEventsDelivered(events2, atMs) {
32153
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32154
+ if (stampable.length === 0) return;
32155
+ failOpenTransaction(db, () => {
32156
+ historySync.markSynced(
32157
+ stampable.map((event) => event.id),
32158
+ atMs
32159
+ );
32160
+ });
32161
+ }
31490
32162
  function recordCapture(event, detected) {
31491
32163
  failOpenTransaction(db, () => {
31492
32164
  const sessionId = event.metadata?.sessionId;
@@ -31573,7 +32245,7 @@ function openLocalDatabase(dir) {
31573
32245
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31574
32246
  if (!definitionId) continue;
31575
32247
  inspectionFindings.insertFinding({
31576
- id: randomUUID10(),
32248
+ id: randomUUID11(),
31577
32249
  auditEventId: record2.scanEvent.id,
31578
32250
  inspectionDefinitionId: definitionId,
31579
32251
  span: finding.span,
@@ -31669,6 +32341,8 @@ function openLocalDatabase(dir) {
31669
32341
  inspectionFindings,
31670
32342
  recordCapture,
31671
32343
  markCaptureDelivered,
32344
+ markCaptureOwed,
32345
+ markAuditEventsDelivered,
31672
32346
  ensureInventory,
31673
32347
  recordConfigScan,
31674
32348
  recordProjectFiles,
@@ -31707,20 +32381,6 @@ var UserGrantPolicyProvider = class {
31707
32381
  }
31708
32382
  };
31709
32383
 
31710
- // ../../packages/persistence/src/file-lock.ts
31711
- import { randomUUID as randomUUID11 } from "crypto";
31712
- import {
31713
- closeSync,
31714
- existsSync as existsSync2,
31715
- openSync,
31716
- readFileSync as readFileSync2,
31717
- rmSync as rmSync5,
31718
- statSync as statSync3,
31719
- writeFileSync as writeFileSync2
31720
- } from "fs";
31721
- import { hostname as hostname3 } from "os";
31722
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31723
-
31724
32384
  // ../../packages/persistence/src/finding-key.ts
31725
32385
  import { createHash as createHash3 } from "crypto";
31726
32386
  function normalizeFilePath(filePath) {
@@ -31733,13 +32393,13 @@ function computeFindingKey(input2) {
31733
32393
 
31734
32394
  // ../../packages/persistence/src/fingerprint.ts
31735
32395
  import { createHmac, randomBytes } from "crypto";
31736
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31737
- import { join as join5 } from "path";
32396
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32397
+ import { join as join8 } from "path";
31738
32398
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31739
32399
  var EXCEPTION_KEY_FILENAME = "exception.key";
31740
32400
  var KEY_MATERIAL_BYTES = 32;
31741
32401
  function keyFilePath(dataDir2) {
31742
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32402
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31743
32403
  }
31744
32404
  function parseKeyFile(raw) {
31745
32405
  const parsed2 = JSON.parse(raw);
@@ -31777,7 +32437,7 @@ var FloorUnreadableError = class extends Error {
31777
32437
  }
31778
32438
  };
31779
32439
  function storedKeyVersionFloor(dataDir2) {
31780
- const file2 = join5(dataDir2, DB_FILENAME);
32440
+ const file2 = join8(dataDir2, DB_FILENAME);
31781
32441
  if (!existsSync3(file2)) return 0;
31782
32442
  let db;
31783
32443
  try {
@@ -31832,7 +32492,7 @@ function occupantMessage(file2, kind) {
31832
32492
  function readFingerprintKey(dataDir2) {
31833
32493
  let raw;
31834
32494
  try {
31835
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32495
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31836
32496
  } catch (err) {
31837
32497
  if (err.code === "ENOENT") return null;
31838
32498
  throw err instanceof Error ? err : new Error(String(err));
@@ -31856,146 +32516,12 @@ function fingerprintValue(key, raw) {
31856
32516
 
31857
32517
  // ../../packages/persistence/src/history-preview.ts
31858
32518
  import { existsSync as existsSync4 } from "fs";
31859
- import { join as join6 } from "path";
32519
+ import { join as join9 } from "path";
31860
32520
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31861
32521
 
31862
- // ../../packages/persistence/src/local-layout.ts
31863
- import { renameSync as renameSync3 } from "fs";
31864
- import { mkdir } from "fs/promises";
31865
- import { homedir } from "os";
31866
- import { join as join7 } from "path";
31867
- function defaultDataDir() {
31868
- return join7(homedir(), ".aka");
31869
- }
31870
- function settingsDir(base = defaultDataDir()) {
31871
- return join7(base, "settings");
31872
- }
31873
- function dataDir(base = defaultDataDir()) {
31874
- return join7(base, "data");
31875
- }
31876
- function dbPath(base = defaultDataDir()) {
31877
- return join7(dataDir(base), "aka.db");
31878
- }
31879
- function keysDir(base = defaultDataDir()) {
31880
- return join7(base, "keys");
31881
- }
31882
- async function ensureDataDir(dir = defaultDataDir()) {
31883
- await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
31884
- tightenDir(dir);
31885
- }
31886
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31887
- ensureDataDirSync(dir);
31888
- }
31889
- function migrateLegacyLayout(base = defaultDataDir()) {
31890
- const moves = [
31891
- { name: "config.json", dest: settingsDir(base) },
31892
- { name: "policy-cache.json", dest: dataDir(base) }
31893
- ];
31894
- for (const { name, dest } of moves) {
31895
- try {
31896
- ensureDataDirSync(dest);
31897
- const moved = join7(dest, name);
31898
- renameSync3(join7(base, name), moved);
31899
- tightenFile(moved);
31900
- } catch {
31901
- }
31902
- }
31903
- }
31904
-
31905
- // ../../packages/persistence/src/managed-settings.ts
31906
- import { readFileSync as readFileSync4 } from "fs";
31907
- import { posix, win32 } from "path";
31908
- function managedSettingsPaths(platform2 = process.platform) {
31909
- if (platform2 === "darwin") {
31910
- return [
31911
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31912
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31913
- ];
31914
- }
31915
- if (platform2 === "win32") {
31916
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31917
- }
31918
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31919
- }
31920
- function readManagedSettings(paths = managedSettingsPaths()) {
31921
- for (const path of paths) {
31922
- let text;
31923
- try {
31924
- text = readFileSync4(path, "utf8");
31925
- } catch {
31926
- continue;
31927
- }
31928
- const record2 = parseJsonObject(text);
31929
- if (!record2) continue;
31930
- const parsed2 = ManagedSettings.safeParse(record2);
31931
- if (parsed2.success) return parsed2.data;
31932
- }
31933
- return null;
31934
- }
31935
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31936
- if (!managed) return settings;
31937
- const { values } = managed;
31938
- const merged = { ...settings };
31939
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31940
- if (values.controlPlane !== void 0) {
31941
- merged.controlPlane = {
31942
- ...values.controlPlane,
31943
- // The administrator pinned WHICH deployment, not WHEN this machine
31944
- // joined it. Keep the user's own attach time when the endpoint is
31945
- // unchanged, so a managed machine does not appear to re-attach on every
31946
- // read; stamp a fresh one when the administrator moved it.
31947
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31948
- };
31949
- }
31950
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31951
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31952
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31953
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31954
- if (values.vaultConsent !== void 0) {
31955
- merged.vaultConsent = values.vaultConsent ? (
31956
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31957
- // at the current version otherwise.
31958
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31959
- ) : void 0;
31960
- }
31961
- if (values.modelJudgeConsent !== void 0) {
31962
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31963
- acknowledgedAt: now().toISOString(),
31964
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31965
- } : void 0;
31966
- }
31967
- return merged;
31968
- }
31969
-
31970
- // ../../packages/persistence/src/settings.ts
31971
- import { readFileSync as readFileSync5 } from "fs";
31972
- import { join as join8 } from "path";
31973
- var SETTINGS_FILENAME = "settings.json";
31974
- function readWorkspaceSettings(base = defaultDataDir()) {
31975
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31976
- }
31977
- function readUserSettings(base) {
31978
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31979
- if (!record2) return defaultWorkspaceSettings();
31980
- try {
31981
- return WorkspaceSettings.parse(record2);
31982
- } catch {
31983
- return defaultWorkspaceSettings();
31984
- }
31985
- }
31986
- function readJson(file2) {
31987
- let text;
31988
- try {
31989
- text = readFileSync5(file2, "utf8");
31990
- } catch {
31991
- return null;
31992
- }
31993
- return parseJsonObject(text) ?? null;
31994
- }
31995
-
31996
32522
  // ../../packages/persistence/src/store-symlinks.ts
31997
32523
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31998
- import { dirname as dirname2, join as join9, resolve } from "path";
32524
+ import { dirname as dirname3, join as join10, resolve } from "path";
31999
32525
  var STORE_DB = "the store database (including the prompt corpus)";
32000
32526
  var STORE_SETTINGS = "your settings file";
32001
32527
  function storeContents(home) {
@@ -32004,7 +32530,7 @@ function storeContents(home) {
32004
32530
  [settingsDir(home), STORE_SETTINGS],
32005
32531
  [dataDir(home), STORE_DB],
32006
32532
  [keysDir(home), "the vault key"],
32007
- [join9(settingsDir(home), "settings.json"), STORE_SETTINGS],
32533
+ [join10(settingsDir(home), "settings.json"), STORE_SETTINGS],
32008
32534
  [dbPath(home), STORE_DB]
32009
32535
  ]);
32010
32536
  }
@@ -32032,7 +32558,7 @@ function linkTarget(path) {
32032
32558
  try {
32033
32559
  return realpathSync(path);
32034
32560
  } catch {
32035
- return resolve(dirname2(path), readlinkSync(path));
32561
+ return resolve(dirname3(path), readlinkSync(path));
32036
32562
  }
32037
32563
  }
32038
32564
  function targetMode(path, platform2) {
@@ -32155,8 +32681,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32155
32681
  // ../../packages/persistence/src/vault/key-provider.ts
32156
32682
  import { execFileSync } from "child_process";
32157
32683
  import { randomBytes as randomBytes2 } from "crypto";
32158
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32159
- import { join as join10 } from "path";
32684
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32685
+ import { join as join11 } from "path";
32160
32686
  var VAULT_OCCUPANT_REASON = {
32161
32687
  symlink: "the path is a symlink; remove it so a keyring can be created",
32162
32688
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32255,7 +32781,7 @@ function claimRotationLock(lock, owner) {
32255
32781
  throw asError(err);
32256
32782
  }
32257
32783
  try {
32258
- writeFileSync3(join10(lock, LOCK_OWNER_FILE), `${owner}
32784
+ writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
32259
32785
  `, { mode: DATA_FILE_MODE });
32260
32786
  return true;
32261
32787
  } catch (err) {
@@ -32264,7 +32790,7 @@ function claimRotationLock(lock, owner) {
32264
32790
  }
32265
32791
  }
32266
32792
  function acquireRotationLock(keysDir2) {
32267
- const lock = join10(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32793
+ const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32268
32794
  const owner = randomBytes2(16).toString("hex");
32269
32795
  if (claimRotationLock(lock, owner)) return { lock, owner };
32270
32796
  let held;
@@ -32291,7 +32817,7 @@ function acquireRotationLock(keysDir2) {
32291
32817
  }
32292
32818
  function releaseRotationLock(lease) {
32293
32819
  try {
32294
- if (readFileSync6(join10(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32820
+ if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32295
32821
  } catch {
32296
32822
  return;
32297
32823
  }
@@ -32312,7 +32838,7 @@ var FileKeyProvider = class {
32312
32838
  this.#keysDir = keysDir2;
32313
32839
  }
32314
32840
  get filePath() {
32315
- return join10(this.#keysDir, VAULT_KEY_FILENAME);
32841
+ return join11(this.#keysDir, VAULT_KEY_FILENAME);
32316
32842
  }
32317
32843
  loadOrCreate() {
32318
32844
  return asAsync(() => {
@@ -32342,7 +32868,7 @@ var FileKeyProvider = class {
32342
32868
  #read() {
32343
32869
  let raw;
32344
32870
  try {
32345
- raw = readFileSync6(this.filePath, "utf8");
32871
+ raw = readFileSync7(this.filePath, "utf8");
32346
32872
  } catch (err) {
32347
32873
  if (err.code === "ENOENT") return null;
32348
32874
  throw err instanceof Error ? err : new Error(String(err));
@@ -32629,7 +33155,14 @@ var SecretVault = class {
32629
33155
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
32630
33156
  const now = this.#now();
32631
33157
  if (existing) {
32632
- this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
33158
+ this.#repo.upsert(
33159
+ {
33160
+ ...existing,
33161
+ provider: existing.provider ?? void 0,
33162
+ userAuthorized: meta4.userAuthorized === true
33163
+ },
33164
+ now
33165
+ );
32633
33166
  return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
32634
33167
  }
32635
33168
  const { material, version: version2 } = await this.#keys.loadOrCreate();
@@ -32651,6 +33184,7 @@ var SecretVault = class {
32651
33184
  ruleId: meta4.ruleId,
32652
33185
  maskedMatch: meta4.maskedMatch,
32653
33186
  provider: meta4.provider,
33187
+ userAuthorized: meta4.userAuthorized === true,
32654
33188
  ciphertext: sealed.ciphertext.toString("base64"),
32655
33189
  nonce: sealed.nonce.toString("base64"),
32656
33190
  authTag: sealed.authTag.toString("base64")
@@ -32970,11 +33504,11 @@ var SecretVault = class {
32970
33504
 
32971
33505
  // ../../packages/persistence/src/warn-era-cap.ts
32972
33506
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32973
- import { join as join11 } from "path";
33507
+ import { join as join12 } from "path";
32974
33508
  var MARKER = "warn-era-capped";
32975
33509
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32976
33510
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32977
- const marker = join11(dataDir2, MARKER);
33511
+ const marker = join12(dataDir2, MARKER);
32978
33512
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32979
33513
  const capped = db.policies.capCategoryActions();
32980
33514
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -33018,6 +33552,307 @@ function toEgressIngestRequest(input2) {
33018
33552
  };
33019
33553
  }
33020
33554
 
33555
+ // ../../packages/remote/src/http.ts
33556
+ import { request as httpRequest } from "http";
33557
+ import { request as httpsRequest } from "https";
33558
+ var DEFAULT_TIMEOUT_MS = 1e4;
33559
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
33560
+ var RemoteRequestError = class extends Error {
33561
+ constructor(status) {
33562
+ super(`control-plane request failed with status ${String(status)}`);
33563
+ this.status = status;
33564
+ this.name = "RemoteRequestError";
33565
+ }
33566
+ status;
33567
+ };
33568
+ var RemoteRouteAbsent = class extends Error {
33569
+ constructor(route) {
33570
+ super(`control plane does not serve ${route}`);
33571
+ this.route = route;
33572
+ this.name = "RemoteRouteAbsent";
33573
+ }
33574
+ route;
33575
+ };
33576
+ var RemoteRequestInvalid = class extends Error {
33577
+ constructor(route, cause) {
33578
+ super(`refusing to send a malformed body to ${route}`);
33579
+ this.cause = cause;
33580
+ this.name = "RemoteRequestInvalid";
33581
+ }
33582
+ cause;
33583
+ };
33584
+ var RemoteResponseInvalid = class extends Error {
33585
+ constructor(route, detail) {
33586
+ super(`control plane answered ${route} with ${detail}`);
33587
+ this.name = "RemoteResponseInvalid";
33588
+ }
33589
+ };
33590
+ var RemoteTransportError = class extends Error {
33591
+ /**
33592
+ * The status the peer sent, when headers arrived and only the BODY was
33593
+ * refused.
33594
+ *
33595
+ * Undefined for the ordinary case this class was written for — no answer at
33596
+ * all. It exists because two paths reject after a status has already been
33597
+ * delivered: an oversized body and an aborted response. Discarding it there
33598
+ * reported a deployment answering 401 with a verbose body as a network
33599
+ * outage, which sends the reader to look at their network instead of their
33600
+ * credential.
33601
+ */
33602
+ constructor(reason, status) {
33603
+ super(`control-plane request did not complete: ${reason}`);
33604
+ this.status = status;
33605
+ this.name = "RemoteTransportError";
33606
+ }
33607
+ status;
33608
+ };
33609
+ async function send(options) {
33610
+ const url2 = new URL(options.url);
33611
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33612
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
33613
+ const requestOptions = {
33614
+ method: options.method,
33615
+ headers: {
33616
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
33617
+ // last they win, and two of the values below are ones no caller may
33618
+ // replace: `x-api-key` is the credential, and `content-length` is the
33619
+ // byte count that stops a multi-byte body being truncated by the
33620
+ // receiver. `SendOptions.headers` is a free-form record on an exported
33621
+ // function, so "no caller does that today" is not the guarantee to rely
33622
+ // on. The one header any caller actually passes — `if-none-match` on the
33623
+ // conditional GET — is untouched by this order.
33624
+ ...options.headers,
33625
+ // The credential. One header, matching what the deployment authenticates
33626
+ // on; a second copy in an `Authorization` header would be one more place
33627
+ // it can be logged by an intermediary for no gain.
33628
+ //
33629
+ // Spread conditionally rather than assigned as `undefined`: Node's header
33630
+ // handling and `content-length` bookkeeping treat a present-but-undefined
33631
+ // key differently from an absent one, and "the header is not there" is
33632
+ // the property the attach flow needs.
33633
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
33634
+ accept: "application/json",
33635
+ ...options.body === void 0 ? {} : {
33636
+ "content-type": "application/json",
33637
+ // Byte length, not string length: a multi-byte body sent with a
33638
+ // character count is truncated by the receiver.
33639
+ "content-length": String(Buffer.byteLength(options.body))
33640
+ }
33641
+ }
33642
+ };
33643
+ return new Promise((resolve2, reject) => {
33644
+ let settled = false;
33645
+ const fail = (reason, status) => {
33646
+ if (settled) return;
33647
+ settled = true;
33648
+ reject(new RemoteTransportError(reason, status));
33649
+ };
33650
+ const req = send_(url2, requestOptions, (res) => {
33651
+ const chunks = [];
33652
+ let size = 0;
33653
+ res.on("data", (chunk) => {
33654
+ size += chunk.length;
33655
+ if (size > MAX_RESPONSE_BYTES) {
33656
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
33657
+ res.destroy();
33658
+ req.destroy();
33659
+ return;
33660
+ }
33661
+ chunks.push(chunk);
33662
+ });
33663
+ res.on("aborted", () => {
33664
+ fail("the response was aborted", res.statusCode);
33665
+ });
33666
+ res.on("end", () => {
33667
+ if (settled) return;
33668
+ settled = true;
33669
+ resolve2({
33670
+ status: res.statusCode ?? 0,
33671
+ headers: res.headers,
33672
+ body: Buffer.concat(chunks).toString("utf8")
33673
+ });
33674
+ });
33675
+ });
33676
+ const deadline = setTimeout(() => {
33677
+ fail(`no response within ${String(timeoutMs)}ms`);
33678
+ req.destroy();
33679
+ }, timeoutMs);
33680
+ deadline.unref();
33681
+ req.on("upgrade", (_res, socket) => {
33682
+ fail("the deployment answered with a protocol upgrade");
33683
+ socket.destroy();
33684
+ });
33685
+ req.on("close", () => {
33686
+ fail("the connection closed before a response was read");
33687
+ clearTimeout(deadline);
33688
+ });
33689
+ req.on("error", (err) => {
33690
+ fail(err.message);
33691
+ });
33692
+ if (options.body !== void 0) req.write(options.body);
33693
+ req.end();
33694
+ });
33695
+ }
33696
+
33697
+ // ../../packages/remote/src/client.ts
33698
+ var ROUTES = {
33699
+ events: "/v1/events",
33700
+ auditEvents: "/v1/audit-events",
33701
+ auditEventsBatch: "/v1/audit-events/batch",
33702
+ inventory: "/v1/inventory",
33703
+ storePosture: "/v1/store-posture",
33704
+ policyBundle: "/v1/policy-bundle",
33705
+ whoami: "/v1/plugin/whoami",
33706
+ shares: "/v1/shares",
33707
+ commands: "/v1/plugin/commands"
33708
+ };
33709
+ function ackRoute(id) {
33710
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
33711
+ }
33712
+ function headerValue(response, name) {
33713
+ const raw = response.headers[name];
33714
+ if (raw === void 0) return void 0;
33715
+ return Array.isArray(raw) ? raw[0] : raw;
33716
+ }
33717
+ function okBody(response) {
33718
+ if (response.status < 200 || response.status >= 300) {
33719
+ throw new RemoteRequestError(response.status);
33720
+ }
33721
+ return response.body;
33722
+ }
33723
+ function parsed(schema, body, route) {
33724
+ let json2;
33725
+ try {
33726
+ json2 = JSON.parse(body);
33727
+ } catch {
33728
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
33729
+ }
33730
+ const result = schema.safeParse(json2);
33731
+ if (!result.success) {
33732
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
33733
+ }
33734
+ return result.data;
33735
+ }
33736
+ function withoutTrailingSlashes(endpoint) {
33737
+ let end = endpoint.length;
33738
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
33739
+ return endpoint.slice(0, end);
33740
+ }
33741
+ var SLASH = "/".charCodeAt(0);
33742
+ function createRemoteClient(options) {
33743
+ const base = withoutTrailingSlashes(options.endpoint);
33744
+ const url2 = (route) => `${base}${route}`;
33745
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
33746
+ const sendOne = async (event) => {
33747
+ const validated = RecordAuditEventRequest.safeParse(event);
33748
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
33749
+ const response = await send({
33750
+ ...common,
33751
+ method: "POST",
33752
+ url: url2(ROUTES.auditEvents),
33753
+ body: JSON.stringify(validated.data)
33754
+ });
33755
+ okBody(response);
33756
+ };
33757
+ return {
33758
+ async ingestEvents(batch) {
33759
+ const response = await send({
33760
+ ...common,
33761
+ method: "POST",
33762
+ url: url2(ROUTES.events),
33763
+ body: JSON.stringify(batch)
33764
+ });
33765
+ return parsed(IngestAck, okBody(response), ROUTES.events);
33766
+ },
33767
+ async ingestInventory(context) {
33768
+ const response = await send({
33769
+ ...common,
33770
+ method: "POST",
33771
+ url: url2(ROUTES.inventory),
33772
+ body: JSON.stringify(context)
33773
+ });
33774
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
33775
+ },
33776
+ async recordAuditEvent(event) {
33777
+ await sendOne(event);
33778
+ },
33779
+ async recordAuditEvents(events, opts) {
33780
+ const validated = RecordAuditEventBatch.safeParse({ events });
33781
+ if (!validated.success) {
33782
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
33783
+ }
33784
+ const response = await send({
33785
+ ...common,
33786
+ method: "POST",
33787
+ url: url2(ROUTES.auditEventsBatch),
33788
+ body: JSON.stringify(validated.data)
33789
+ });
33790
+ if (response.status === 404) {
33791
+ if (opts?.fallbackToSingleEvents !== true) {
33792
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
33793
+ }
33794
+ for (const event of validated.data.events) await sendOne(event);
33795
+ return { accepted: validated.data.events.length };
33796
+ }
33797
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
33798
+ },
33799
+ async reportStorePosture(snapshot) {
33800
+ const response = await send({
33801
+ ...common,
33802
+ method: "POST",
33803
+ url: url2(ROUTES.storePosture),
33804
+ body: JSON.stringify(snapshot)
33805
+ });
33806
+ okBody(response);
33807
+ },
33808
+ async getPolicyBundle(etag) {
33809
+ const response = await send({
33810
+ ...common,
33811
+ method: "GET",
33812
+ url: url2(ROUTES.policyBundle),
33813
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
33814
+ });
33815
+ if (response.status === 304) {
33816
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
33817
+ }
33818
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
33819
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
33820
+ },
33821
+ async whoami() {
33822
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
33823
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
33824
+ },
33825
+ async recordProjectEgress(request) {
33826
+ const validated = EgressIngestRequest.safeParse(request);
33827
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
33828
+ const response = await send({
33829
+ ...common,
33830
+ method: "POST",
33831
+ url: url2(ROUTES.shares),
33832
+ body: JSON.stringify(validated.data)
33833
+ });
33834
+ okBody(response);
33835
+ },
33836
+ async pollCommand() {
33837
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
33838
+ if (response.status === 404) return null;
33839
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
33840
+ },
33841
+ async ackCommand(id, body) {
33842
+ const validated = DeviceCommandAckBody.safeParse(body);
33843
+ const route = ackRoute(id);
33844
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
33845
+ const response = await send({
33846
+ ...common,
33847
+ method: "POST",
33848
+ url: url2(route),
33849
+ body: JSON.stringify(validated.data)
33850
+ });
33851
+ okBody(response);
33852
+ }
33853
+ };
33854
+ }
33855
+
33021
33856
  // ../../packages/plugin-runtime/src/attached/failure.ts
33022
33857
  function statusOf(err) {
33023
33858
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -33036,12 +33871,27 @@ function classifyFailure(err) {
33036
33871
  }
33037
33872
  }
33038
33873
 
33874
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
33875
+ var REQUEST_TIMEOUT_MS = 2e3;
33876
+ function withTimeout(promise2, ms) {
33877
+ let timer;
33878
+ const timeout = new Promise((_, reject) => {
33879
+ timer = setTimeout(() => {
33880
+ reject(new Error("attached gateway request timed out"));
33881
+ }, ms);
33882
+ });
33883
+ promise2.catch(() => void 0);
33884
+ return Promise.race([promise2, timeout]).finally(() => {
33885
+ clearTimeout(timer);
33886
+ });
33887
+ }
33888
+
33039
33889
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
33040
- import { readFileSync as readFileSync7 } from "fs";
33041
- import { join as join12 } from "path";
33890
+ import { readFileSync as readFileSync8 } from "fs";
33891
+ import { join as join13 } from "path";
33042
33892
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
33043
33893
  function forwardDropsPath(dataDir2) {
33044
- return join12(dataDir2, FORWARD_DROPS_FILENAME);
33894
+ return join13(dataDir2, FORWARD_DROPS_FILENAME);
33045
33895
  }
33046
33896
  function recordForwardDrops(dataDir2, count, nowMs) {
33047
33897
  if (count <= 0) return;
@@ -33059,7 +33909,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
33059
33909
  }
33060
33910
  function readForwardDrops(dataDir2) {
33061
33911
  try {
33062
- const parsed2 = JSON.parse(readFileSync7(forwardDropsPath(dataDir2), "utf8"));
33912
+ const parsed2 = JSON.parse(readFileSync8(forwardDropsPath(dataDir2), "utf8"));
33063
33913
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
33064
33914
  const record2 = parsed2;
33065
33915
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -33077,13 +33927,13 @@ function readForwardDrops(dataDir2) {
33077
33927
 
33078
33928
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
33079
33929
  import { randomUUID as randomUUID15 } from "crypto";
33080
- import { readFileSync as readFileSync13 } from "fs";
33930
+ import { readFileSync as readFileSync14 } from "fs";
33081
33931
  import { readFile, rename, writeFile } from "fs/promises";
33082
- import { join as join21 } from "path";
33932
+ import { join as join22 } from "path";
33083
33933
 
33084
33934
  // ../../packages/plugin-sdk/src/config.ts
33085
33935
  import { existsSync as existsSync7 } from "fs";
33086
- import { join as join13 } from "path";
33936
+ import { join as join14 } from "path";
33087
33937
 
33088
33938
  // ../../packages/plugin-sdk/src/provider-env.ts
33089
33939
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -33137,7 +33987,7 @@ function resolveProvider() {
33137
33987
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33138
33988
  try {
33139
33989
  ensureLayoutDirSync(base);
33140
- const settingsFile = join13(settingsDir(base), "settings.json");
33990
+ const settingsFile = join14(settingsDir(base), "settings.json");
33141
33991
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
33142
33992
  } catch {
33143
33993
  }
@@ -33161,9 +34011,9 @@ function resolveProviderSafe(resolveProviderFn) {
33161
34011
  }
33162
34012
 
33163
34013
  // ../../packages/plugin-sdk/src/config-inventory.ts
33164
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
34014
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33165
34015
  import { homedir as homedir2 } from "os";
33166
- import { basename as basename3, join as join15 } from "path";
34016
+ import { basename as basename3, join as join16 } from "path";
33167
34017
 
33168
34018
  // ../../packages/detections/src/egress/registry.ts
33169
34019
  var EXTRACTOR_VERSION = "1";
@@ -36255,8 +37105,8 @@ function uniqueRuleIds(findings) {
36255
37105
  }
36256
37106
 
36257
37107
  // ../../packages/plugin-sdk/src/repo.ts
36258
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36259
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join14, sep as sep2 } from "path";
37108
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
37109
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
36260
37110
  function resolveRepo(cwd) {
36261
37111
  try {
36262
37112
  const root = findGitRoot(cwd);
@@ -36271,36 +37121,36 @@ function resolveRepo(cwd) {
36271
37121
  function findGitRoot(start) {
36272
37122
  let dir = start;
36273
37123
  for (; ; ) {
36274
- if (existsSync8(join14(dir, ".git"))) return dir;
36275
- const parent = dirname3(dir);
37124
+ if (existsSync8(join15(dir, ".git"))) return dir;
37125
+ const parent = dirname4(dir);
36276
37126
  if (parent === dir) return void 0;
36277
37127
  dir = parent;
36278
37128
  }
36279
37129
  }
36280
37130
  function resolveGitContext(root) {
36281
- const dotGit = join14(root, ".git");
37131
+ const dotGit = join15(root, ".git");
36282
37132
  try {
36283
37133
  if (statSync6(dotGit).isDirectory()) {
36284
- return { configPath: join14(dotGit, "config"), headRoot: root };
37134
+ return { configPath: join15(dotGit, "config"), headRoot: root };
36285
37135
  }
36286
37136
  } catch {
36287
37137
  return void 0;
36288
37138
  }
36289
37139
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
36290
37140
  if (!target) return void 0;
36291
- const gitdir = isAbsolute(target) ? target : join14(root, target);
36292
- if (existsSync8(join14(gitdir, "config"))) {
36293
- return { configPath: join14(gitdir, "config"), headRoot: root };
37141
+ const gitdir = isAbsolute(target) ? target : join15(root, target);
37142
+ if (existsSync8(join15(gitdir, "config"))) {
37143
+ return { configPath: join15(gitdir, "config"), headRoot: root };
36294
37144
  }
36295
- const commonRaw = safeRead(join14(gitdir, "commondir"))?.trim();
37145
+ const commonRaw = safeRead(join15(gitdir, "commondir"))?.trim();
36296
37146
  if (!commonRaw) return void 0;
36297
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join14(gitdir, commonRaw);
36298
- const headRoot = basename2(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
36299
- return { configPath: join14(commonGitDir, "config"), headRoot };
37147
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join15(gitdir, commonRaw);
37148
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
37149
+ return { configPath: join15(commonGitDir, "config"), headRoot };
36300
37150
  }
36301
37151
  function safeRead(path) {
36302
37152
  try {
36303
- return readFileSync8(path, "utf8");
37153
+ return readFileSync9(path, "utf8");
36304
37154
  } catch {
36305
37155
  return void 0;
36306
37156
  }
@@ -36845,8 +37695,8 @@ function createGuardedScanner(partition, gateway, opts) {
36845
37695
 
36846
37696
  // ../../packages/plugin-sdk/src/ignore-layers.ts
36847
37697
  var import_ignore = __toESM(require_ignore(), 1);
36848
- import { readFileSync as readFileSync10 } from "fs";
36849
- import { join as join16 } from "path";
37698
+ import { readFileSync as readFileSync11 } from "fs";
37699
+ import { join as join17 } from "path";
36850
37700
 
36851
37701
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
36852
37702
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -36857,24 +37707,59 @@ import {
36857
37707
  fstatSync,
36858
37708
  mkdirSync as mkdirSync2,
36859
37709
  openSync as openSync2,
36860
- readFileSync as readFileSync11,
37710
+ readFileSync as readFileSync12,
36861
37711
  readSync,
36862
37712
  writeFileSync as writeFileSync5
36863
37713
  } from "fs";
36864
- import { join as join17 } from "path";
37714
+ import { join as join18 } from "path";
36865
37715
  var TAIL_BYTES = 256 * 1024;
36866
37716
 
36867
37717
  // ../../packages/plugin-sdk/src/nudge.ts
36868
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
36869
- import { join as join18 } from "path";
37718
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
37719
+ import { join as join19 } from "path";
36870
37720
 
36871
37721
  // ../../packages/plugin-sdk/src/paths.ts
36872
37722
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
36873
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
37723
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
37724
+
37725
+ // ../../packages/plugin-sdk/src/policy-resolver.ts
37726
+ function createPolicyResolver(bundle) {
37727
+ const byRule = /* @__PURE__ */ new Map();
37728
+ const byCategory = /* @__PURE__ */ new Map();
37729
+ let reversible = /* @__PURE__ */ new Set();
37730
+ try {
37731
+ for (const policy of bundle.policies) {
37732
+ if (!policy.enabled) continue;
37733
+ if ("ruleId" in policy.target) {
37734
+ if (!byRule.has(policy.target.ruleId)) byRule.set(policy.target.ruleId, policy.action);
37735
+ } else if (!byCategory.has(policy.target.category)) {
37736
+ byCategory.set(policy.target.category, policy.action);
37737
+ }
37738
+ }
37739
+ reversible = new Set(bundle.reversibleRuleIds ?? []);
37740
+ } catch {
37741
+ byRule.clear();
37742
+ byCategory.clear();
37743
+ reversible = /* @__PURE__ */ new Set();
37744
+ }
37745
+ return {
37746
+ actionFor(ruleId, category) {
37747
+ const byRuleAction = byRule.get(ruleId);
37748
+ if (byRuleAction !== void 0) return byRuleAction;
37749
+ const byCategoryAction = byCategory.get(category);
37750
+ if (byCategoryAction !== void 0) return byCategoryAction;
37751
+ const fallback = DEFAULT_ACTIONS[category];
37752
+ return fallback ?? "log";
37753
+ },
37754
+ isReversible(ruleId) {
37755
+ return reversible.has(ruleId);
37756
+ }
37757
+ };
37758
+ }
36874
37759
 
36875
37760
  // ../../packages/plugin-sdk/src/project-files.ts
36876
37761
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
36877
- import { basename as basename5, join as join19 } from "path";
37762
+ import { basename as basename5, join as join20 } from "path";
36878
37763
 
36879
37764
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
36880
37765
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -36905,7 +37790,6 @@ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
36905
37790
  // ../../packages/plugin-sdk/src/runtime.ts
36906
37791
  import { randomUUID as randomUUID14 } from "crypto";
36907
37792
  var ENFORCEMENT_CEILING_ENABLED = false;
36908
- var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
36909
37793
  function startTiming() {
36910
37794
  try {
36911
37795
  return performance.now();
@@ -36942,28 +37826,22 @@ function createPluginRuntime(gateway, settings, opts) {
36942
37826
  bundlesPacked = true;
36943
37827
  }
36944
37828
  const policyMode = settings.policy;
37829
+ const redactFallback = settings.redactFallback;
36945
37830
  const dataDir2 = opts?.dataDir;
36946
- let policies = [];
36947
37831
  let rules = [];
36948
37832
  let scanner;
36949
37833
  let bundleExceptions = [];
36950
37834
  let initialized = false;
36951
- const ruleActionIndex = /* @__PURE__ */ new Map();
36952
- const categoryActionIndex = /* @__PURE__ */ new Map();
36953
- let reversibleRuleIndex = /* @__PURE__ */ new Set();
37835
+ let resolver = createPolicyResolver({
37836
+ version: "",
37837
+ policies: [],
37838
+ customKeywords: [],
37839
+ fetchedAt: ""
37840
+ });
36954
37841
  async function ensureInitialized() {
36955
37842
  if (initialized) return;
36956
37843
  const bundle = await gateway.getPolicyBundle();
36957
- policies = bundle.policies;
36958
- for (const p of policies) {
36959
- if (!p.enabled) continue;
36960
- if ("ruleId" in p.target) {
36961
- if (!ruleActionIndex.has(p.target.ruleId)) ruleActionIndex.set(p.target.ruleId, p.action);
36962
- } else if (!categoryActionIndex.has(p.target.category)) {
36963
- categoryActionIndex.set(p.target.category, p.action);
36964
- }
36965
- }
36966
- reversibleRuleIndex = new Set(bundle.reversibleRuleIds ?? []);
37844
+ resolver = createPolicyResolver(bundle);
36967
37845
  const bundledProbeKeys = new Set(
36968
37846
  getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
36969
37847
  );
@@ -37016,33 +37894,28 @@ function createPluginRuntime(gateway, settings, opts) {
37016
37894
  return cachedKey;
37017
37895
  }
37018
37896
  function resolveAction(ruleId, category) {
37019
- const byRule = ruleActionIndex.get(ruleId);
37020
- if (byRule !== void 0) return byRule;
37021
- const byCategory = categoryActionIndex.get(category);
37022
- if (byCategory !== void 0) return byCategory;
37023
- const fallback = DEFAULT_ACTIONS[category];
37024
- return fallback ?? "log";
37025
- }
37026
- function actionForFinding(finding, excepted) {
37897
+ return resolver.actionFor(ruleId, category);
37898
+ }
37899
+ function actionForFinding(finding, excepted, rewritable = true) {
37027
37900
  if (excepted?.has(finding)) return "allow";
37028
37901
  const action = resolveAction(finding.ruleId, finding.category);
37902
+ if (!rewritable && action === "redact") return builtinPolicyToAction(redactFallback);
37029
37903
  if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
37030
37904
  return "warn";
37031
37905
  }
37032
37906
  return action;
37033
37907
  }
37034
- function decide(findings, text, excepted) {
37908
+ function decide(findings, text, excepted, rewritable = true) {
37035
37909
  if (findings.length === 0) return { action: "log", text, findings: [] };
37036
- const actionFor = (finding) => actionForFinding(finding, excepted);
37910
+ const actionFor = (finding) => actionForFinding(finding, excepted, rewritable);
37037
37911
  let worst = "log";
37038
37912
  for (const finding of findings) {
37039
- const action = actionFor(finding);
37040
- if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
37913
+ worst = strongerAction(worst, actionFor(finding));
37041
37914
  }
37042
37915
  if (worst === "block") return { action: "block", text: null, findings };
37043
37916
  if (worst === "redact") {
37044
37917
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
37045
- const reversibleFindings = redactFindings.filter((f) => reversibleRuleIndex.has(f.ruleId));
37918
+ const reversibleFindings = redactFindings.filter((f) => resolver.isReversible(f.ruleId));
37046
37919
  return {
37047
37920
  action: "redact",
37048
37921
  text: redact(text, redactFindings),
@@ -37118,7 +37991,7 @@ function createPluginRuntime(gateway, settings, opts) {
37118
37991
  return { excepted: /* @__PURE__ */ new Set(), exceptionIds: [] };
37119
37992
  }
37120
37993
  }
37121
- async function recordBlockedDetections(decision, excepted, ctx, fpCache) {
37994
+ async function recordBlockedDetections(decision, excepted, ctx, fpCache, rewritable = true) {
37122
37995
  const references = [];
37123
37996
  try {
37124
37997
  if (decision.action !== "block" && decision.action !== "redact") return references;
@@ -37126,7 +37999,7 @@ function createPluginRuntime(gateway, settings, opts) {
37126
37999
  if (!key) return references;
37127
38000
  const seen = /* @__PURE__ */ new Set();
37128
38001
  for (const finding of decision.findings) {
37129
- const action = actionForFinding(finding, excepted);
38002
+ const action = actionForFinding(finding, excepted, rewritable);
37130
38003
  if (action !== "block" && action !== "redact") continue;
37131
38004
  const fp = fingerprintOf(key, finding, fpCache);
37132
38005
  const pair = `${finding.ruleId}:${fp}`;
@@ -37153,7 +38026,7 @@ function createPluginRuntime(gateway, settings, opts) {
37153
38026
  }
37154
38027
  return references;
37155
38028
  }
37156
- async function evaluate(text, context, ctx) {
38029
+ async function evaluate(text, context, ctx, rewritable = true) {
37157
38030
  try {
37158
38031
  await ensureInitialized();
37159
38032
  if (!scanner) throw new Error("the runtime initialized without a scanner");
@@ -37162,8 +38035,14 @@ function createPluginRuntime(gateway, settings, opts) {
37162
38035
  const findings = dropShieldedFindings(matched, shielded.spans);
37163
38036
  const fpCache = /* @__PURE__ */ new Map();
37164
38037
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
37165
- const decision = decide(findings, text, excepted);
37166
- const blockedReferences = await recordBlockedDetections(decision, excepted, ctx, fpCache);
38038
+ const decision = decide(findings, text, excepted, rewritable);
38039
+ const blockedReferences = await recordBlockedDetections(
38040
+ decision,
38041
+ excepted,
38042
+ ctx,
38043
+ fpCache,
38044
+ rewritable
38045
+ );
37167
38046
  if (blockedReferences.length > 0) decision.blockedReferences = blockedReferences;
37168
38047
  return { decision, excepted, exceptionIds };
37169
38048
  } catch {
@@ -37187,12 +38066,16 @@ function createPluginRuntime(gateway, settings, opts) {
37187
38066
  sourceTool: input2.sourceTool,
37188
38067
  metadata: input2.metadata,
37189
38068
  preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
37190
- }
38069
+ },
38070
+ opts2.rewritable
37191
38071
  );
37192
38072
  if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
37193
38073
  try {
37194
38074
  const contentHash = contentHashOf(input2.text);
37195
- const storedContent = decision.findings.length > 0 ? redact(input2.text, decision.findings) : input2.text;
38075
+ const maskedFindings = decision.findings.filter(
38076
+ (match) => isActionAtLeast(actionForFinding(match, excepted, opts2.rewritable), "redact")
38077
+ );
38078
+ const storedContent = maskedFindings.length > 0 ? redact(input2.text, maskedFindings) : input2.text;
37196
38079
  const inspectionMs = elapsedMs(timingStartedAt);
37197
38080
  const metadata = exceptionIds.length > 0 || inspectionMs !== void 0 ? {
37198
38081
  ...input2.metadata,
@@ -37229,7 +38112,7 @@ function createPluginRuntime(gateway, settings, opts) {
37229
38112
  severity: match.severity,
37230
38113
  span: match.span,
37231
38114
  maskedMatch,
37232
- actionTaken: actionForFinding(match, excepted),
38115
+ actionTaken: actionForFinding(match, excepted, opts2.rewritable),
37233
38116
  confidence: match.confidence,
37234
38117
  ...findingKey ? { findingKey } : {}
37235
38118
  };
@@ -37269,7 +38152,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
37269
38152
 
37270
38153
  // ../../packages/plugin-sdk/src/throttle.ts
37271
38154
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
37272
- import { join as join20 } from "path";
38155
+ import { join as join21 } from "path";
37273
38156
 
37274
38157
  // ../../packages/plugin-sdk/src/tokenize.ts
37275
38158
  function redactedPlaceholder(category) {
@@ -37331,14 +38214,26 @@ var SecretVaultGlue = class {
37331
38214
  }
37332
38215
  async tokenizeText(text, opts) {
37333
38216
  try {
37334
- const findings = opts?.findings ?? this.#selfScan(text);
37335
- const reversible = opts?.reversible;
37336
- const keeps = (finding) => reversible === void 0 || reversible.has(finding);
37337
- if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
37338
- if (findings.length === 0) return { text, pointers: [], degraded: [] };
38217
+ const supplied = opts?.findings;
38218
+ const resolver = opts?.resolver;
38219
+ const scanned = supplied ?? this.#selfScan(text);
38220
+ if (scanned === null) {
38221
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
38222
+ }
38223
+ const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
38224
+ (f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
38225
+ ) : scanned;
38226
+ let reversible = opts?.reversible;
38227
+ if (resolver !== void 0 && reversible === void 0) {
38228
+ reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
38229
+ }
38230
+ const reversibleSet = reversible;
38231
+ const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
38232
+ if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
37339
38233
  const groups = groupSpans(text, findings);
37340
38234
  const pointers = [];
37341
38235
  const degraded = [];
38236
+ const redacted = [];
37342
38237
  let out = text;
37343
38238
  for (const group of [...groups].reverse()) {
37344
38239
  const original = text.slice(group.start, group.end);
@@ -37352,6 +38247,7 @@ var SecretVaultGlue = class {
37352
38247
  degraded.unshift({ category: group.category });
37353
38248
  } else if (!keeps(finding)) {
37354
38249
  replacement = redactedPlaceholder(finding.category);
38250
+ redacted.unshift({ category: finding.category });
37355
38251
  } else {
37356
38252
  replacement = await this.tokenizeValue(finding.rawMatch, {
37357
38253
  ruleId: finding.ruleId,
@@ -37372,9 +38268,9 @@ var SecretVaultGlue = class {
37372
38268
  }
37373
38269
  }
37374
38270
  }
37375
- return { text: out, pointers, degraded };
38271
+ return { text: out, pointers, degraded, redacted };
37376
38272
  } catch {
37377
- return { text: "[REDACTED]", pointers: [], degraded: [] };
38273
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
37378
38274
  }
37379
38275
  }
37380
38276
  async detokenizeText(text, opts) {
@@ -37571,25 +38467,17 @@ var UNOPENABLE_VAULT = {
37571
38467
  resolvePointerIdentity: () => Promise.resolve(null)
37572
38468
  };
37573
38469
 
37574
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
37575
- var REQUEST_TIMEOUT_MS = 2e3;
37576
- function withTimeout(promise2, ms) {
37577
- let timer;
37578
- const timeout = new Promise((_, reject) => {
37579
- timer = setTimeout(() => {
37580
- reject(new Error("attached gateway request timed out"));
37581
- }, ms);
37582
- });
37583
- promise2.catch(() => void 0);
37584
- return Promise.race([promise2, timeout]).finally(() => {
37585
- clearTimeout(timer);
37586
- });
37587
- }
37588
-
37589
38470
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
37590
38471
  function isInvalidRequest(err) {
37591
38472
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
37592
38473
  }
38474
+ function isRouteAbsent(err) {
38475
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
38476
+ }
38477
+ function isServerRejection(err) {
38478
+ const status = statusOf(err);
38479
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
38480
+ }
37593
38481
  var FORWARD_BUDGET_MS = 1500;
37594
38482
  var DECISION_PATH_BUDGET_MS = 800;
37595
38483
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -37617,7 +38505,7 @@ function parseBreakerState(raw, nowMs) {
37617
38505
  }
37618
38506
  function createForwardPolicy(deps) {
37619
38507
  const now = deps.now ?? (() => Date.now());
37620
- const file2 = join21(deps.dir, STATE_FILENAME);
38508
+ const file2 = join22(deps.dir, STATE_FILENAME);
37621
38509
  let state = null;
37622
38510
  let loading = null;
37623
38511
  async function readState() {
@@ -37657,6 +38545,20 @@ function createForwardPolicy(deps) {
37657
38545
  } catch {
37658
38546
  current = { ...CLOSED };
37659
38547
  }
38548
+ const restoreOpenedAtMs = (openedAtMs) => persist({
38549
+ consecutiveFailures: current.consecutiveFailures,
38550
+ openedAtMs,
38551
+ lastFailure: current.lastFailure
38552
+ });
38553
+ const recordFailure = (cause) => {
38554
+ const failures = current.consecutiveFailures + 1;
38555
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
38556
+ return persist({
38557
+ consecutiveFailures: failures,
38558
+ openedAtMs: shouldOpen ? now() : null,
38559
+ lastFailure: cause
38560
+ });
38561
+ };
37660
38562
  const at = now();
37661
38563
  if (current.openedAtMs !== null) {
37662
38564
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -37675,15 +38577,20 @@ function createForwardPolicy(deps) {
37675
38577
  }
37676
38578
  return { ok: true, value };
37677
38579
  } catch (err) {
37678
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
38580
+ if (isInvalidRequest(err)) {
38581
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
38582
+ return { ok: false, reason: "invalid-request" };
38583
+ }
38584
+ if (isRouteAbsent(err)) {
38585
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
38586
+ return { ok: false, reason: "route-absent" };
38587
+ }
38588
+ if (isServerRejection(err)) {
38589
+ await recordFailure("unreachable");
38590
+ return { ok: false, reason: "rejected" };
38591
+ }
37679
38592
  const reason = classifyFailure(err);
37680
- const failures = current.consecutiveFailures + 1;
37681
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
37682
- await persist({
37683
- consecutiveFailures: failures,
37684
- openedAtMs: shouldOpen ? now() : null,
37685
- lastFailure: reason
37686
- });
38593
+ await recordFailure(reason);
37687
38594
  return { ok: false, reason };
37688
38595
  }
37689
38596
  }
@@ -37691,13 +38598,11 @@ function createForwardPolicy(deps) {
37691
38598
  }
37692
38599
 
37693
38600
  // ../../packages/plugin-runtime/src/attached/gateway.ts
37694
- var ACTION_STRENGTH = {
37695
- allow: 0,
37696
- log: 1,
37697
- warn: 2,
37698
- redact: 3,
37699
- block: 4
37700
- };
38601
+ function strongerOf(a, b) {
38602
+ if (a === null) return b;
38603
+ if (b === null) return a;
38604
+ return strongerAction(a, b);
38605
+ }
37701
38606
  function ruleCategoryMap(wireRules, localRules) {
37702
38607
  const map2 = /* @__PURE__ */ new Map();
37703
38608
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -37707,11 +38612,6 @@ function ruleCategoryMap(wireRules, localRules) {
37707
38612
  }
37708
38613
  return map2;
37709
38614
  }
37710
- function strongerOf(a, b) {
37711
- if (a === null) return b;
37712
- if (b === null) return a;
37713
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
37714
- }
37715
38615
  function policyKey(policy) {
37716
38616
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
37717
38617
  }
@@ -37730,7 +38630,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
37730
38630
  const floor = floorFor(policy, categoryByRuleId);
37731
38631
  remoteCategoryAction.set(
37732
38632
  policy.target.category,
37733
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
38633
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
37734
38634
  );
37735
38635
  }
37736
38636
  for (const policy of localPolicies) {
@@ -37747,7 +38647,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
37747
38647
  }
37748
38648
  merged.set(
37749
38649
  key,
37750
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
38650
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
37751
38651
  );
37752
38652
  }
37753
38653
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -37767,13 +38667,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
37767
38667
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
37768
38668
  }
37769
38669
  const effectiveFloor = strongerOf(floor, localFloor);
37770
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
38670
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
37771
38671
  const existing = merged.get(key);
37772
38672
  if (existing === void 0) {
37773
38673
  merged.set(key, clamped);
37774
38674
  continue;
37775
38675
  }
37776
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
38676
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
37777
38677
  merged.set(key, clamped);
37778
38678
  }
37779
38679
  }
@@ -37806,6 +38706,8 @@ var AttachedDataGateway = class {
37806
38706
  );
37807
38707
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
37808
38708
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
38709
+ } else {
38710
+ this.deps.local.markCaptureOwed(record2.event);
37809
38711
  }
37810
38712
  }
37811
38713
  async ensureInventory(ctx) {
@@ -37842,9 +38744,10 @@ var AttachedDataGateway = class {
37842
38744
  // a retried tool_call, exactly this path — can never stomp a populated row.
37843
38745
  async recordAuditEvent(event) {
37844
38746
  await this.deps.local.recordAuditEvent(event);
37845
- await this.deps.forward.run(
38747
+ const forwarded = await this.deps.forward.run(
37846
38748
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
37847
38749
  );
38750
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
37848
38751
  }
37849
38752
  // Attached `llm_call` is written locally by the inner gateway, then routed to
37850
38753
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -37853,44 +38756,170 @@ var AttachedDataGateway = class {
37853
38756
  // which would write the event to the local store a second time.
37854
38757
  async recordLlmCall(input2) {
37855
38758
  await this.deps.local.recordLlmCall(input2);
37856
- await this.deps.forward.run(
37857
- () => this.deps.client.recordAuditEvent(
37858
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
37859
- )
38759
+ const event = llmAuditEvent(input2);
38760
+ const forwarded = await this.deps.forward.run(
38761
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
37860
38762
  );
38763
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
37861
38764
  }
37862
38765
  /**
37863
- * Forward one batch, item by item, under ONE aggregate deadline.
38766
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
38767
+ *
38768
+ * This used to send one HTTP request per event, which is what made the batch
38769
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
38770
+ * away everything after them. The same rows now cross 50 at a time over
38771
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
38772
+ * used — so the same budget admits ~750. The wire cap is the server's own
38773
+ * constant, sized against server cost, and the client REFUSES a longer array
38774
+ * client-side, so the chunking here is not a convention.
38775
+ *
38776
+ * Still serial, and still for the original reason: firing N requests at once
38777
+ * would trade a latency problem for a burst the plane's per-key rate limiting
38778
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
38779
+ * the fix; more concurrent ones is not.
38780
+ *
38781
+ * When the deadline passes the remainder is dropped rather than sent: the
38782
+ * local write has already succeeded, so every caller has a correct result to
38783
+ * return. What is dropped is COUNTED, everywhere it can happen — this path
38784
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
38785
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
38786
+ * the breaker closed, renders a healthy block, and discards the tail of every
38787
+ * batch indefinitely. The SAME tally also covers a single that fails inside
38788
+ * the per-item retry below — the breaker opening mid-retry is a failure the
38789
+ * breaker's own state DOES capture, but the events still in this chunk once
38790
+ * that happens are neither delivered nor otherwise counted anywhere, which is
38791
+ * the same invisibility with a different cause.
38792
+ *
38793
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
38794
+ * single-event ack and at fifty times the blast radius here:
38795
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
38796
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
38797
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
38798
+ * delivered and never re-offer the twenty the plane did not take. So success
38799
+ * is checked against `chunk.length`; anything short of it falls into the same
38800
+ * per-item pass as a refused chunk, which is the only way to recover the
38801
+ * rows that did not land, since the ack carries no per-row verdict to
38802
+ * resend by.
38803
+ *
38804
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
38805
+ * no-op rather than a second cost — an assumption this file cannot verify.
38806
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
38807
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
38808
+ * the batch size as the invariant `recordCapture` reads), so whether a
38809
+ * duplicate counts toward THIS route's `accepted` is not expressed
38810
+ * anywhere in this repo. If it follows its sibling's convention and does
38811
+ * NOT, a chunk containing even one already-delivered row — the ordinary
38812
+ * consequence of a lost stamp, which this file already treats as cheap —
38813
+ * answers short forever and enters the per-item pass on every pass it is
38814
+ * offered again. The cost of that is bounded rather than silent: the
38815
+ * pass converges (every row lands and stamps), so it is one wasted round
38816
+ * of singles rather than a stall, and it errs toward an extra resend
38817
+ * rather than toward the lost row the alternative risks.
37864
38818
  *
37865
- * Per-item budgets bound each request and nothing bounded their sum see
37866
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
37867
- * rather than sent: the local write has already succeeded, so every caller
37868
- * has a correct result to return, and a drop is the outcome this path is
37869
- * built to accept (G8) where a blown hook timeout is not.
38819
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
38820
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
38821
+ * settles none which is why the whole chunk is stamped together on a FULL
38822
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
38823
+ * treatment, alongside a short accept, and all are re-sent one event at a
38824
+ * time:
37870
38825
  *
37871
- * Serial rather than concurrent on purpose. Firing N requests at once would
37872
- * trade a latency problem for a burst the plane's own per-key rate limiting
37873
- * would answer with the refusals the breaker then counts.
38826
+ * `invalid-request` a chunk the client refused to send at all. One malformed
38827
+ * event would otherwise cost the 49 good ones beside it
38828
+ * a new way to lose data introduced by the very change
38829
+ * meant to stop losing it.
38830
+ * `route-absent` a deployment that predates the batch route. The
38831
+ * single-event route is the one it serves, and re-sending
38832
+ * here rather than inside the client is what gives each
38833
+ * request its own budget instead of 50 inside one.
38834
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
38835
+ * a 4xx body refusal from schema drift on the other side
38836
+ * of the wire. Settlement is batch-atomic on this reason
38837
+ * exactly as on the others, so leaving it out would cost
38838
+ * the whole chunk for one event the DEPLOYMENT considers
38839
+ * malformed, where the per-item form cost only that one.
37874
38840
  *
37875
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
37876
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
37877
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
37878
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
37879
- * plane produces no failures, keeps the breaker closed, renders a healthy
37880
- * block, and discards the tail of every batch indefinitely.
38841
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
38842
+ * chunk, and re-sending it item by item would just spend the budget failing 50
38843
+ * more times for those, the blast radius stays exactly what it was before
38844
+ * batching.
37881
38845
  */
37882
38846
  async forwardBatch(inputs, toEvent) {
37883
38847
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
37884
- for (let i = 0; i < inputs.length; i += 1) {
37885
- const now = Date.now();
37886
- if (now >= deadline) {
37887
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
37888
- return;
38848
+ const delivered = [];
38849
+ try {
38850
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
38851
+ const now = Date.now();
38852
+ if (now >= deadline) {
38853
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
38854
+ return;
38855
+ }
38856
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
38857
+ const forwarded = await this.deps.forward.run(
38858
+ () => this.deps.client.recordAuditEvents(
38859
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
38860
+ )
38861
+ );
38862
+ if (forwarded.ok) {
38863
+ if (forwarded.value.accepted === chunk.length) {
38864
+ delivered.push(...chunk);
38865
+ continue;
38866
+ }
38867
+ } else if (
38868
+ // THREE reasons are worth a second pass, one at a time, and they are
38869
+ // the three settled BEFORE the control plane refused anything, or
38870
+ // (for `rejected`) refused the BODY rather than the connection.
38871
+ //
38872
+ // `invalid-request` — the CLIENT refused the body before any request
38873
+ // went out: a defect in one event, not an outage. Re-sending singly
38874
+ // isolates the bad one instead of charging its 49 neighbours for it.
38875
+ //
38876
+ // `route-absent` — the deployment predates the batch route and serves
38877
+ // only the single-event one. The retry IS the compatibility path, and
38878
+ // it has to live HERE rather than inside the client: each single gets
38879
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
38880
+ // fallback would spend 50 sequential round trips inside the ONE
38881
+ // budget wrapping this call — turning a working older deployment into
38882
+ // a timeout, three of those into an open breaker, and every row into
38883
+ // a silent drop while the status surface called an answering
38884
+ // deployment down.
38885
+ //
38886
+ // `rejected` — the deployment's own 4xx refusal of the body, the
38887
+ // server-side twin of `invalid-request`: isolating it the same way
38888
+ // costs one event instead of the whole chunk for a defect the
38889
+ // deployment considers local to one row.
38890
+ //
38891
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
38892
+ // the whole chunk; re-sending it item by item would just spend the
38893
+ // budget failing 50 more times.
38894
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
38895
+ ) {
38896
+ continue;
38897
+ }
38898
+ for (const [j, event] of chunk.entries()) {
38899
+ const at = Date.now();
38900
+ if (at >= deadline) {
38901
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
38902
+ return;
38903
+ }
38904
+ const single = await this.deps.forward.run(
38905
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
38906
+ );
38907
+ if (single.ok) {
38908
+ delivered.push(event);
38909
+ continue;
38910
+ }
38911
+ if (single.reason === "breaker-open") {
38912
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
38913
+ return;
38914
+ }
38915
+ recordForwardDrops(this.deps.dataDir, 1, at);
38916
+ }
38917
+ }
38918
+ } finally {
38919
+ try {
38920
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
38921
+ } catch {
37889
38922
  }
37890
- const input2 = inputs[i];
37891
- await this.deps.forward.run(
37892
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
37893
- );
37894
38923
  }
37895
38924
  }
37896
38925
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -37933,9 +38962,10 @@ var AttachedDataGateway = class {
37933
38962
  // local store.
37934
38963
  async recordConfigScan(record2) {
37935
38964
  await this.deps.local.recordConfigScan(record2);
37936
- await this.deps.forward.run(
38965
+ const forwarded = await this.deps.forward.run(
37937
38966
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
37938
38967
  );
38968
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
37939
38969
  }
37940
38970
  async recordBlockedDetection(entry) {
37941
38971
  return this.deps.local.recordBlockedDetection(entry);
@@ -38069,6 +39099,18 @@ var AttachedDataGateway = class {
38069
39099
  // exactly what it did, leaving the whole control inert on every device
38070
39100
  // while every test around it stayed green.
38071
39101
  prohibitedModels: cached2.prohibitedModels
39102
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
39103
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
39104
+ // it emits, so an 'authored' policy arriving from the control plane
39105
+ // keeps that marker even where the clamp rebuilds it with a stronger
39106
+ // action. The device reads it in exactly one direction — the rules such a
39107
+ // policy targets are not locally re-assignable — so it sits on the
39108
+ // `prohibitedModels` side of the line for the same reason that field
39109
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
39110
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
39111
+ // would be the silent failure rather than the safe one — the action would
39112
+ // still be enforced while the local override the organization authored
39113
+ // away quietly came back.
38072
39114
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
38073
39115
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
38074
39116
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -38108,10 +39150,10 @@ var AttachedDataGateway = class {
38108
39150
  //
38109
39151
  // Implementing these is what actually closes the skipped-local-maintenance
38110
39152
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
38111
- // any object carrying all five, so the composite qualifies and SessionStart
39153
+ // any object carrying them all, so the composite qualifies and SessionStart
38112
39154
  // runs maintenance on the device's real store.
38113
39155
  //
38114
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
39156
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
38115
39157
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
38116
39158
  // return value directly; declaring them `async` here would hand those call
38117
39159
  // sites a Promise and silently break both.
@@ -38134,9 +39176,15 @@ var AttachedDataGateway = class {
38134
39176
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
38135
39177
  // gives: `recordCapture` calls it after the forward has already settled, on a
38136
39178
  // path that has nothing left to await.
39179
+ markCaptureOwed(event) {
39180
+ this.deps.local.markCaptureOwed(event);
39181
+ }
38137
39182
  markCaptureDelivered(event, atMs) {
38138
39183
  this.deps.local.markCaptureDelivered(event, atMs);
38139
39184
  }
39185
+ markAuditEventsDelivered(events, atMs) {
39186
+ this.deps.local.markAuditEventsDelivered(events, atMs);
39187
+ }
38140
39188
  };
38141
39189
  function reKeyForForward(event, remote) {
38142
39190
  if (remote === null) {
@@ -38179,281 +39227,17 @@ function toolAuditEvent(input2) {
38179
39227
  }
38180
39228
 
38181
39229
  // ../../packages/plugin-runtime/src/attached/history-state.ts
38182
- import { readFileSync as readFileSync14 } from "fs";
38183
- import { join as join22 } from "path";
39230
+ import { readFileSync as readFileSync15 } from "fs";
39231
+ import { join as join23 } from "path";
38184
39232
 
38185
39233
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
38186
39234
  import { createHash as createHash6 } from "crypto";
38187
39235
  import { hostname as hostname5 } from "os";
38188
39236
 
38189
- // ../../packages/remote/src/http.ts
38190
- import { request as httpRequest } from "http";
38191
- import { request as httpsRequest } from "https";
38192
- var DEFAULT_TIMEOUT_MS = 1e4;
38193
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
38194
- var RemoteRequestError = class extends Error {
38195
- constructor(status) {
38196
- super(`control-plane request failed with status ${String(status)}`);
38197
- this.status = status;
38198
- this.name = "RemoteRequestError";
38199
- }
38200
- status;
38201
- };
38202
- var RemoteRequestInvalid = class extends Error {
38203
- constructor(route, cause) {
38204
- super(`refusing to send a malformed body to ${route}`);
38205
- this.cause = cause;
38206
- this.name = "RemoteRequestInvalid";
38207
- }
38208
- cause;
38209
- };
38210
- var RemoteResponseInvalid = class extends Error {
38211
- constructor(route, detail) {
38212
- super(`control plane answered ${route} with ${detail}`);
38213
- this.name = "RemoteResponseInvalid";
38214
- }
38215
- };
38216
- var RemoteTransportError = class extends Error {
38217
- /**
38218
- * The status the peer sent, when headers arrived and only the BODY was
38219
- * refused.
38220
- *
38221
- * Undefined for the ordinary case this class was written for — no answer at
38222
- * all. It exists because two paths reject after a status has already been
38223
- * delivered: an oversized body and an aborted response. Discarding it there
38224
- * reported a deployment answering 401 with a verbose body as a network
38225
- * outage, which sends the reader to look at their network instead of their
38226
- * credential.
38227
- */
38228
- constructor(reason, status) {
38229
- super(`control-plane request did not complete: ${reason}`);
38230
- this.status = status;
38231
- this.name = "RemoteTransportError";
38232
- }
38233
- status;
38234
- };
38235
- async function send(options) {
38236
- const url2 = new URL(options.url);
38237
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38238
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
38239
- const requestOptions = {
38240
- method: options.method,
38241
- headers: {
38242
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
38243
- // last they win, and two of the values below are ones no caller may
38244
- // replace: `x-api-key` is the credential, and `content-length` is the
38245
- // byte count that stops a multi-byte body being truncated by the
38246
- // receiver. `SendOptions.headers` is a free-form record on an exported
38247
- // function, so "no caller does that today" is not the guarantee to rely
38248
- // on. The one header any caller actually passes — `if-none-match` on the
38249
- // conditional GET — is untouched by this order.
38250
- ...options.headers,
38251
- // The credential. One header, matching what the deployment authenticates
38252
- // on; a second copy in an `Authorization` header would be one more place
38253
- // it can be logged by an intermediary for no gain.
38254
- //
38255
- // Spread conditionally rather than assigned as `undefined`: Node's header
38256
- // handling and `content-length` bookkeeping treat a present-but-undefined
38257
- // key differently from an absent one, and "the header is not there" is
38258
- // the property the attach flow needs.
38259
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
38260
- accept: "application/json",
38261
- ...options.body === void 0 ? {} : {
38262
- "content-type": "application/json",
38263
- // Byte length, not string length: a multi-byte body sent with a
38264
- // character count is truncated by the receiver.
38265
- "content-length": String(Buffer.byteLength(options.body))
38266
- }
38267
- }
38268
- };
38269
- return new Promise((resolve2, reject) => {
38270
- let settled = false;
38271
- const fail = (reason, status) => {
38272
- if (settled) return;
38273
- settled = true;
38274
- reject(new RemoteTransportError(reason, status));
38275
- };
38276
- const req = send_(url2, requestOptions, (res) => {
38277
- const chunks = [];
38278
- let size = 0;
38279
- res.on("data", (chunk) => {
38280
- size += chunk.length;
38281
- if (size > MAX_RESPONSE_BYTES) {
38282
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
38283
- res.destroy();
38284
- req.destroy();
38285
- return;
38286
- }
38287
- chunks.push(chunk);
38288
- });
38289
- res.on("aborted", () => {
38290
- fail("the response was aborted", res.statusCode);
38291
- });
38292
- res.on("end", () => {
38293
- if (settled) return;
38294
- settled = true;
38295
- resolve2({
38296
- status: res.statusCode ?? 0,
38297
- headers: res.headers,
38298
- body: Buffer.concat(chunks).toString("utf8")
38299
- });
38300
- });
38301
- });
38302
- const deadline = setTimeout(() => {
38303
- fail(`no response within ${String(timeoutMs)}ms`);
38304
- req.destroy();
38305
- }, timeoutMs);
38306
- deadline.unref();
38307
- req.on("upgrade", (_res, socket) => {
38308
- fail("the deployment answered with a protocol upgrade");
38309
- socket.destroy();
38310
- });
38311
- req.on("close", () => {
38312
- fail("the connection closed before a response was read");
38313
- clearTimeout(deadline);
38314
- });
38315
- req.on("error", (err) => {
38316
- fail(err.message);
38317
- });
38318
- if (options.body !== void 0) req.write(options.body);
38319
- req.end();
38320
- });
38321
- }
38322
-
38323
- // ../../packages/remote/src/client.ts
38324
- var ROUTES = {
38325
- events: "/v1/events",
38326
- auditEvents: "/v1/audit-events",
38327
- auditEventsBatch: "/v1/audit-events/batch",
38328
- inventory: "/v1/inventory",
38329
- storePosture: "/v1/store-posture",
38330
- policyBundle: "/v1/policy-bundle",
38331
- whoami: "/v1/plugin/whoami",
38332
- shares: "/v1/shares"
38333
- };
38334
- function headerValue(response, name) {
38335
- const raw = response.headers[name];
38336
- if (raw === void 0) return void 0;
38337
- return Array.isArray(raw) ? raw[0] : raw;
38338
- }
38339
- function okBody(response) {
38340
- if (response.status < 200 || response.status >= 300) {
38341
- throw new RemoteRequestError(response.status);
38342
- }
38343
- return response.body;
38344
- }
38345
- function parsed(schema, body, route) {
38346
- let json2;
38347
- try {
38348
- json2 = JSON.parse(body);
38349
- } catch {
38350
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
38351
- }
38352
- const result = schema.safeParse(json2);
38353
- if (!result.success) {
38354
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
38355
- }
38356
- return result.data;
38357
- }
38358
- function withoutTrailingSlashes(endpoint) {
38359
- let end = endpoint.length;
38360
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
38361
- return endpoint.slice(0, end);
38362
- }
38363
- var SLASH = "/".charCodeAt(0);
38364
- function createRemoteClient(options) {
38365
- const base = withoutTrailingSlashes(options.endpoint);
38366
- const url2 = (route) => `${base}${route}`;
38367
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
38368
- const sendOne = async (event) => {
38369
- const validated = RecordAuditEventRequest.safeParse(event);
38370
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
38371
- const response = await send({
38372
- ...common,
38373
- method: "POST",
38374
- url: url2(ROUTES.auditEvents),
38375
- body: JSON.stringify(validated.data)
38376
- });
38377
- okBody(response);
38378
- };
38379
- return {
38380
- async ingestEvents(batch) {
38381
- const response = await send({
38382
- ...common,
38383
- method: "POST",
38384
- url: url2(ROUTES.events),
38385
- body: JSON.stringify(batch)
38386
- });
38387
- return parsed(IngestAck, okBody(response), ROUTES.events);
38388
- },
38389
- async ingestInventory(context) {
38390
- const response = await send({
38391
- ...common,
38392
- method: "POST",
38393
- url: url2(ROUTES.inventory),
38394
- body: JSON.stringify(context)
38395
- });
38396
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
38397
- },
38398
- async recordAuditEvent(event) {
38399
- await sendOne(event);
38400
- },
38401
- async recordAuditEvents(events) {
38402
- const validated = RecordAuditEventBatch.safeParse({ events });
38403
- if (!validated.success) {
38404
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
38405
- }
38406
- const response = await send({
38407
- ...common,
38408
- method: "POST",
38409
- url: url2(ROUTES.auditEventsBatch),
38410
- body: JSON.stringify(validated.data)
38411
- });
38412
- if (response.status === 404) {
38413
- for (const event of validated.data.events) await sendOne(event);
38414
- return { accepted: validated.data.events.length };
38415
- }
38416
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
38417
- },
38418
- async reportStorePosture(snapshot) {
38419
- const response = await send({
38420
- ...common,
38421
- method: "POST",
38422
- url: url2(ROUTES.storePosture),
38423
- body: JSON.stringify(snapshot)
38424
- });
38425
- okBody(response);
38426
- },
38427
- async getPolicyBundle(etag) {
38428
- const response = await send({
38429
- ...common,
38430
- method: "GET",
38431
- url: url2(ROUTES.policyBundle),
38432
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
38433
- });
38434
- if (response.status === 304) {
38435
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
38436
- }
38437
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
38438
- return { changed: true, bundle, etag: headerValue(response, "etag") };
38439
- },
38440
- async whoami() {
38441
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
38442
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
38443
- },
38444
- async recordProjectEgress(request) {
38445
- const validated = EgressIngestRequest.safeParse(request);
38446
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
38447
- const response = await send({
38448
- ...common,
38449
- method: "POST",
38450
- url: url2(ROUTES.shares),
38451
- body: JSON.stringify(validated.data)
38452
- });
38453
- okBody(response);
38454
- }
38455
- };
38456
- }
39237
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
39238
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
39239
+ var TRACE_ID = EventMetadata.shape.traceId;
39240
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
38457
39241
 
38458
39242
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
38459
39243
  import { spawn } from "child_process";
@@ -38461,7 +39245,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
38461
39245
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
38462
39246
 
38463
39247
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
38464
- import { readFileSync as readFileSync15 } from "fs";
39248
+ import { readFileSync as readFileSync16 } from "fs";
38465
39249
  function createPluginBlock(build, policyStore) {
38466
39250
  return async () => {
38467
39251
  const cached2 = await policyStore.read();
@@ -38480,7 +39264,7 @@ function createPluginBlock(build, policyStore) {
38480
39264
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38481
39265
  import { randomUUID as randomUUID16 } from "crypto";
38482
39266
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
38483
- import { join as join23 } from "path";
39267
+ import { join as join24 } from "path";
38484
39268
 
38485
39269
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
38486
39270
  import { rename as rename2 } from "fs/promises";
@@ -38504,7 +39288,7 @@ async function publishByRename(tmp, file2, move = rename2) {
38504
39288
 
38505
39289
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38506
39290
  function createPolicyStore(dir = dataDir()) {
38507
- const file2 = join23(dir, "policy-cache.json");
39291
+ const file2 = join24(dir, "policy-cache.json");
38508
39292
  async function read() {
38509
39293
  try {
38510
39294
  const raw = await readFile2(file2, "utf8");
@@ -38513,22 +39297,32 @@ function createPolicyStore(dir = dataDir()) {
38513
39297
  const record2 = parsed2;
38514
39298
  const bundle = PolicyBundle.parse(record2.bundle);
38515
39299
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
38516
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
39300
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
39301
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
39302
+ const etag = replayable ? stored : void 0;
38517
39303
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
38518
39304
  } catch {
38519
39305
  return null;
38520
39306
  }
38521
39307
  }
38522
- async function write(bundle, etag) {
38523
- await ensureDataDir(dir);
38524
- const stored = {
38525
- bundle,
38526
- fetchedAtMs: Date.now(),
38527
- ...etag === void 0 ? {} : { etag }
38528
- };
39308
+ function knowsMoreThanThisBuild(shapeId) {
39309
+ if (typeof shapeId !== "string" || shapeId === "") return false;
39310
+ const theirs = new Set(shapeId.split(","));
39311
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
39312
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
39313
+ }
39314
+ async function priorRecord() {
39315
+ try {
39316
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
39317
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
39318
+ } catch {
39319
+ return null;
39320
+ }
39321
+ }
39322
+ async function publishRecord(record2) {
38529
39323
  const tmp = `${file2}.${randomUUID16()}.tmp`;
38530
39324
  try {
38531
- await writeFile2(tmp, JSON.stringify(stored), {
39325
+ await writeFile2(tmp, JSON.stringify(record2), {
38532
39326
  encoding: "utf8",
38533
39327
  mode: DATA_FILE_MODE,
38534
39328
  flag: "wx"
@@ -38539,6 +39333,27 @@ function createPolicyStore(dir = dataDir()) {
38539
39333
  throw err;
38540
39334
  }
38541
39335
  }
39336
+ async function write(bundle, etag) {
39337
+ await ensureDataDir(dir);
39338
+ const prior = await priorRecord();
39339
+ const priorVersion = prior?.bundle?.version;
39340
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
39341
+ await publishRecord({
39342
+ ...prior,
39343
+ fetchedAtMs: Date.now()
39344
+ });
39345
+ return;
39346
+ }
39347
+ await publishRecord({
39348
+ bundle,
39349
+ fetchedAtMs: Date.now(),
39350
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
39351
+ // bundle it already holds, and the point of the stamp is to describe the
39352
+ // build that last narrowed those bytes, which is this one.
39353
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
39354
+ ...etag === void 0 ? {} : { etag }
39355
+ });
39356
+ }
38542
39357
  return { read, write, file: file2 };
38543
39358
  }
38544
39359
 
@@ -38704,11 +39519,11 @@ function readStorePosture(dbPath2) {
38704
39519
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
38705
39520
  import { randomUUID as randomUUID17 } from "crypto";
38706
39521
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
38707
- import { join as join24 } from "path";
39522
+ import { join as join25 } from "path";
38708
39523
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
38709
39524
  function createPostureStore(dir = settingsDir(), legacyDir) {
38710
- const file2 = join24(dir, "posture-state.json");
38711
- const legacyFile = legacyDir === void 0 ? null : join24(legacyDir, "posture-state.json");
39525
+ const file2 = join25(dir, "posture-state.json");
39526
+ const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
38712
39527
  async function persist(state) {
38713
39528
  await ensureDataDir(dir);
38714
39529
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -38776,8 +39591,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
38776
39591
  }
38777
39592
 
38778
39593
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
38779
- import { readFileSync as readFileSync16 } from "fs";
38780
- import { join as join25 } from "path";
39594
+ import { readFileSync as readFileSync17 } from "fs";
39595
+ import { join as join26 } from "path";
38781
39596
 
38782
39597
  // ../../packages/plugin-runtime/src/attached/status.ts
38783
39598
  var REFUSAL_LINES = {
@@ -39095,9 +39910,21 @@ var StandaloneDataGateway = class {
39095
39910
  // for the whole of it, so a member that threw would make that answer a lie
39096
39911
  // the moment a composite delegated to it. A store-level no-op is the honest
39097
39912
  // shape — a standalone machine has nothing delivered to record.
39913
+ markCaptureOwed(event) {
39914
+ this.db.markCaptureOwed(event);
39915
+ }
39098
39916
  markCaptureDelivered(event, atMs) {
39099
39917
  this.db.markCaptureDelivered(event, atMs);
39100
39918
  }
39919
+ // Implemented, not stubbed, for the same reason its sibling above is: the
39920
+ // attached gateway is a DECORATOR over an instance of this class
39921
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
39922
+ // stamp the live forward makes lands here with a non-empty array. This is the
39923
+ // production write path for that feature, not a shape-satisfying no-op — a
39924
+ // machine that is merely standalone simply never calls it.
39925
+ markAuditEventsDelivered(events, atMs) {
39926
+ this.db.markAuditEventsDelivered(events, atMs);
39927
+ }
39101
39928
  staleBinaryNotice(currentVersion) {
39102
39929
  try {
39103
39930
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -39237,17 +40064,17 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
39237
40064
 
39238
40065
  // src/protocol/marker.ts
39239
40066
  import { randomBytes as randomBytes4 } from "crypto";
39240
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
39241
- import { join as join26 } from "path";
40067
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync18, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
40068
+ import { join as join27 } from "path";
39242
40069
  var MARKER_FILE = "protocol-marker";
39243
40070
  function mintMarker() {
39244
40071
  return randomBytes4(8).toString("hex");
39245
40072
  }
39246
40073
  function sessionProtocolMarker(dataDir2, sessionId) {
39247
40074
  if (!sessionId) return mintMarker();
39248
- const path = join26(dataDir2, MARKER_FILE);
40075
+ const path = join27(dataDir2, MARKER_FILE);
39249
40076
  try {
39250
- const stored = JSON.parse(readFileSync17(path, "utf8"));
40077
+ const stored = JSON.parse(readFileSync18(path, "utf8"));
39251
40078
  if (stored.sessionId === sessionId && typeof stored.marker === "string" && /^[0-9a-f]{16}$/.test(stored.marker)) {
39252
40079
  return stored.marker;
39253
40080
  }
@@ -39256,7 +40083,7 @@ function sessionProtocolMarker(dataDir2, sessionId) {
39256
40083
  const marker = mintMarker();
39257
40084
  try {
39258
40085
  mkdirSync5(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
39259
- const tmp = join26(dataDir2, `${MARKER_FILE}.tmp`);
40086
+ const tmp = join27(dataDir2, `${MARKER_FILE}.tmp`);
39260
40087
  writeFileSync8(tmp, JSON.stringify({ sessionId, marker }), { mode: DATA_FILE_MODE });
39261
40088
  renameSync5(tmp, path);
39262
40089
  } catch {
@@ -39562,16 +40389,16 @@ function baseMetadata(input2) {
39562
40389
  }
39563
40390
 
39564
40391
  // src/hooks/store-health.ts
39565
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
39566
- import { dirname as dirname5, join as join27 } from "path";
40392
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
40393
+ import { dirname as dirname6, join as join28 } from "path";
39567
40394
  var STORE_REDIRECT_MARKER = "store-redirect-last-session";
39568
40395
  function markerDirs(dataDir2) {
39569
- return [dataDir2, dirname5(dataDir2)];
40396
+ return [dataDir2, dirname6(dataDir2)];
39570
40397
  }
39571
40398
  function alreadyClaimed(dirs, marker, sessionId) {
39572
40399
  return dirs.some((dir) => {
39573
40400
  try {
39574
- return readFileSync18(join27(dir, marker), "utf8") === sessionId;
40401
+ return readFileSync19(join28(dir, marker), "utf8") === sessionId;
39575
40402
  } catch {
39576
40403
  return false;
39577
40404
  }
@@ -39581,7 +40408,7 @@ function recordClaim(dirs, marker, sessionId) {
39581
40408
  for (const dir of dirs) {
39582
40409
  try {
39583
40410
  mkdirSync6(dir, { recursive: true, mode: DATA_DIR_MODE });
39584
- writeFileSync9(join27(dir, marker), sessionId, { mode: DATA_FILE_MODE });
40411
+ writeFileSync9(join28(dir, marker), sessionId, { mode: DATA_FILE_MODE });
39585
40412
  return;
39586
40413
  } catch {
39587
40414
  }
@@ -39606,7 +40433,7 @@ function formatMode(mode) {
39606
40433
  }
39607
40434
  function warnIfStoreRedirected(config2, sessionId, write = (message) => void process.stderr.write(message)) {
39608
40435
  try {
39609
- const paths = symlinkedStorePaths(dirname5(config2.dataDir));
40436
+ const paths = symlinkedStorePaths(dirname6(config2.dataDir));
39610
40437
  if (paths.length === 0) return;
39611
40438
  if (!sessionId) {
39612
40439
  write(storeRedirectedMessage(paths));