@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.
@@ -492,12 +492,13 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // src/remediation/entry.ts
495
- import { readFileSync as readFileSync21 } from "fs";
495
+ import { readFileSync as readFileSync22 } from "fs";
496
496
  import { fileURLToPath as fileURLToPath5 } from "url";
497
497
 
498
498
  // ../../packages/persistence/src/attached-derived.ts
499
499
  import { rmSync } from "fs";
500
500
  import { join } from "path";
501
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
501
502
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
502
503
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
503
504
 
@@ -598,6 +599,30 @@ var SQLITE_MIGRATIONS = [
598
599
  {
599
600
  tag: "0022_audit_inspection_ms",
600
601
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
602
+ },
603
+ {
604
+ tag: "0023_secret_vault_user_authorized",
605
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
606
+ },
607
+ {
608
+ tag: "0024_finding_resolution_key_created_index",
609
+ sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
610
+ },
611
+ {
612
+ tag: "0025_audit_capture_attribute_columns",
613
+ sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
614
+ },
615
+ {
616
+ tag: "0026_audit_llm_call_usage_columns",
617
+ sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
618
+ },
619
+ {
620
+ tag: "0027_audit_llm_usage_index",
621
+ sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
622
+ },
623
+ {
624
+ tag: "0028_activity_session_probe_indexes",
625
+ sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
601
626
  }
602
627
  ];
603
628
 
@@ -22137,6 +22162,26 @@ var AttachTokenResponse = external_exports.union([
22137
22162
  AttachTokenExpired,
22138
22163
  external_exports.object({ status: printable(64) })
22139
22164
  ]);
22165
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22166
+ var DeviceCommand = external_exports.object({
22167
+ id: printable(128).min(1),
22168
+ kind: DeviceCommandKind,
22169
+ issuedAt: printable(64).min(1),
22170
+ expiresAt: printable(64).min(1)
22171
+ }).strict();
22172
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22173
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22174
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22175
+ external_exports.object({
22176
+ outcome: external_exports.literal("reported"),
22177
+ projectsScanned: external_exports.number().int().nonnegative()
22178
+ }).strict(),
22179
+ external_exports.object({
22180
+ outcome: external_exports.literal("failed"),
22181
+ reason: DeviceCommandFailureReason,
22182
+ projectsScanned: external_exports.number().int().nonnegative()
22183
+ }).strict()
22184
+ ]);
22140
22185
 
22141
22186
  // ../../packages/schema/src/zod/registry.ts
22142
22187
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22303,7 +22348,7 @@ var PackManifest = external_exports.object({
22303
22348
  }).meta({ id: "PackManifest" });
22304
22349
 
22305
22350
  // ../../packages/schema/src/zod/detection.ts
22306
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22351
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22307
22352
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22308
22353
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22309
22354
  var DetectionCounts = external_exports.object({
@@ -22440,14 +22485,17 @@ function optional2(key, parsed2, raw) {
22440
22485
  function isStringArray(value) {
22441
22486
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22442
22487
  }
22488
+ var ORIGIN_VALUES = { library: true, custom: true };
22489
+ function resolveOrigin(origin) {
22490
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22491
+ }
22443
22492
  function summaryToDetectionListItem(s) {
22444
22493
  return {
22445
22494
  id: `${s.namespace}/${s.packId}`,
22446
22495
  name: s.name,
22447
22496
  version: s.version,
22448
22497
  enabled: s.enabled,
22449
- origin: "library",
22450
- // v1: every installed pack is library origin
22498
+ origin: resolveOrigin(s.origin),
22451
22499
  namespace: s.namespace,
22452
22500
  packId: s.packId,
22453
22501
  ruleCount: s.ruleCount,
@@ -22499,7 +22547,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22499
22547
  name: row.name,
22500
22548
  version: row.version,
22501
22549
  enabled: row.enabled,
22502
- origin: "library",
22550
+ origin: resolveOrigin(row.origin),
22503
22551
  namespace: row.namespace,
22504
22552
  packId: row.packId,
22505
22553
  ruleCount: row.rules.length,
@@ -22519,16 +22567,20 @@ function splitDetectionId(id) {
22519
22567
  }
22520
22568
  function buildDetectionsList(summaries, query) {
22521
22569
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22570
+ const originOf = (s) => resolveOrigin(s.origin);
22522
22571
  const counts = {
22523
22572
  all: summaries.length,
22524
- library: summaries.length,
22525
- // all origin=library in v1
22526
- custom: 0,
22573
+ library: summaries.filter((s) => originOf(s) === "library").length,
22574
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22575
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22576
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22577
+ // place, and that state does not exist — editing a library pack forks it. See
22578
+ // OriginEnum.
22527
22579
  customized: 0,
22528
22580
  updates: withUpdate.length
22529
22581
  };
22530
22582
  const filter = query.filter;
22531
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22583
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22532
22584
  if (query.q) {
22533
22585
  const q = query.q.toLowerCase();
22534
22586
  filtered = filtered.filter(
@@ -22608,8 +22660,9 @@ var Event = external_exports.object({
22608
22660
  metadata: EventMetadata.optional()
22609
22661
  }).meta({ id: "Event" });
22610
22662
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22663
+ var INGEST_BATCH_MAX = 100;
22611
22664
  var IngestBatch = external_exports.object({
22612
- events: external_exports.array(IngestEvent).min(1).max(100),
22665
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22613
22666
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22614
22667
  // additionally rejects any event whose contentHash the store has already
22615
22668
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23165,382 +23218,11 @@ var PatchInstalledPackRequest = external_exports.object({
23165
23218
  message: "At least one field must be provided"
23166
23219
  }).meta({ id: "PatchInstalledPackRequest" });
23167
23220
 
23168
- // ../../packages/schema/src/zod/vault.ts
23169
- var POINTER_FORMAT_VERSION = 2;
23170
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23171
- var POINTER_TOKEN_PATTERN = new RegExp(
23172
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23173
- );
23174
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23175
- function pointerTokenScanner() {
23176
- return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23177
- }
23178
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23179
- var ParsedPointer = external_exports.object({
23180
- category: DetectionCategory,
23181
- keyVersion: external_exports.number().int().positive(),
23182
- pointerId: external_exports.string(),
23183
- tag: external_exports.string()
23184
- });
23185
- var VaultEntry = external_exports.object({
23186
- pointerId: external_exports.string(),
23187
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23188
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23189
- // independently of the vault encryption key below.
23190
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23191
- fingerprintKeyVersion: external_exports.number().int().positive(),
23192
- // The vault-key epoch this row's ciphertext was sealed under.
23193
- keyVersion: external_exports.number().int().positive(),
23194
- // Fixed at first mint and never updated: the same value detected later under a
23195
- // different rule's category keeps the category it was minted with, so one
23196
- // value always produces exactly one wire token.
23197
- category: DetectionCategory,
23198
- ruleId: external_exports.string(),
23199
- // Partial-reveal preview for badges and listings. Never the raw value.
23200
- maskedMatch: external_exports.string(),
23201
- provider: external_exports.string().optional(),
23202
- ciphertext: external_exports.string(),
23203
- nonce: external_exports.string(),
23204
- authTag: external_exports.string(),
23205
- // How many times this value has been detected on this machine — the reuse
23206
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23207
- occurrenceCount: external_exports.number().int().nonnegative(),
23208
- firstSeen: external_exports.string(),
23209
- lastSeen: external_exports.string()
23210
- });
23211
- var PointerDescriptor = external_exports.object({
23212
- category: DetectionCategory,
23213
- provider: external_exports.string().optional(),
23214
- maskedMatch: external_exports.string(),
23215
- occurrences: external_exports.number().int().nonnegative(),
23216
- firstSeen: external_exports.string(),
23217
- lastSeen: external_exports.string()
23218
- });
23219
- var PointerIdentity = external_exports.object({
23220
- ruleId: external_exports.string(),
23221
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23222
- fingerprintKeyVersion: external_exports.number().int().positive()
23223
- });
23224
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23225
- var VaultDerefReason = external_exports.enum([
23226
- "display",
23227
- "explicit-reveal",
23228
- "view-render",
23229
- "model-input",
23230
- "remediation",
23231
- "purge"
23232
- ]);
23233
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23234
- var BATCHED_DEREF_REASONS = ["display", "view-render"];
23235
- function isBatchedDerefReason(reason) {
23236
- return BATCHED_DEREF_REASONS.includes(reason);
23237
- }
23238
- var VaultDeref = external_exports.object({
23239
- id: external_exports.guid(),
23240
- pointerId: external_exports.string(),
23241
- at: external_exports.string(),
23242
- target: DetokenizeTarget,
23243
- reason: VaultDerefReason,
23244
- outcome: VaultDerefOutcome,
23245
- // Present only on a model-target crossing that a reveal grant authorized.
23246
- grantId: external_exports.string().optional(),
23247
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23248
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23249
- pointerCount: external_exports.number().int().positive().default(1)
23250
- });
23251
- var VaultSightingKind = external_exports.enum([
23252
- "prompt",
23253
- "tool-input",
23254
- "tool-output",
23255
- "file",
23256
- "transcript"
23257
- ]);
23258
- var VaultSighting = external_exports.object({
23259
- location: external_exports.string(),
23260
- kind: VaultSightingKind,
23261
- firstSeen: external_exports.string(),
23262
- lastSeen: external_exports.string()
23263
- });
23264
- var VaultInventoryEntry = external_exports.object({
23265
- pointerId: external_exports.string(),
23266
- category: DetectionCategory,
23267
- provider: external_exports.string().optional(),
23268
- maskedMatch: external_exports.string(),
23269
- occurrences: external_exports.number().int().nonnegative(),
23270
- firstSeen: external_exports.string(),
23271
- lastSeen: external_exports.string(),
23272
- // The active reveal-to-model grant covering this value, when one exists —
23273
- // the inventory badges it, the row links to revocation.
23274
- revealGrantId: external_exports.string().nullable(),
23275
- sightings: external_exports.array(VaultSighting)
23276
- });
23277
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23278
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23279
- var MAX_VAULT_PAGE_LIMIT = 200;
23280
- var ListVaultInventoryQuery = external_exports.object({
23281
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23282
- // Opaque; names the last row of the page just served.
23283
- cursor: external_exports.string().optional()
23284
- });
23285
- var ListVaultInventoryResponse = external_exports.object({
23286
- // Vaulted values across the whole store, not just this page — cursor-
23287
- // independent, so paging never changes what the count claims.
23288
- totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23289
- items: external_exports.array(VaultInventoryEntry),
23290
- // `null` once the last page is reached.
23291
- nextCursor: external_exports.string().nullable()
23292
- });
23293
- var ListVaultReuseQuery = external_exports.object({
23294
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23295
- cursor: external_exports.string().optional()
23296
- });
23297
- var ListVaultReuseResponse = external_exports.object({
23298
- // Reused values across the whole store — the number the section's claim
23299
- // ("values detected in more than one place") is about.
23300
- totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23301
- items: external_exports.array(VaultInventoryEntry),
23302
- nextCursor: external_exports.string().nullable()
23303
- });
23304
- var ListVaultDerefsQuery = external_exports.object({
23305
- // Include the batched, high-volume reasons (display, view-render). Omitted
23306
- // hides them and counts them into `hiddenBatched` instead, so the model
23307
- // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23308
- // over a Server Action, which preserves the type, never as a URL param.
23309
- includeBatched: external_exports.boolean().optional(),
23310
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23311
- cursor: external_exports.string().optional()
23312
- });
23313
- var ListVaultDerefsResponse = external_exports.object({
23314
- items: external_exports.array(VaultDeref),
23315
- nextCursor: external_exports.string().nullable(),
23316
- // Display/view-render rows the query hid, over the WHOLE trail rather than
23317
- // this page — it is the count the "N hidden" line and its toggle speak for.
23318
- // Always 0 when `includeBatched` was set, since nothing was hidden.
23319
- hiddenBatched: external_exports.number().int().nonnegative()
23320
- });
23321
- var VaultKeyCustody = external_exports.string();
23322
- var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
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
23221
  // ../../packages/schema/src/zod/policy.ts
23542
23222
  var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23543
23223
  var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23224
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23225
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23544
23226
  var Policy = external_exports.object({
23545
23227
  id: external_exports.guid(),
23546
23228
  scope: PolicyScope,
@@ -23550,7 +23232,27 @@ var Policy = external_exports.object({
23550
23232
  customKeywords: external_exports.array(external_exports.string()).optional(),
23551
23233
  // Display name — optional so older policy rows without name still parse.
23552
23234
  // Added for the findings API (policy.name column migration).
23553
- name: external_exports.string().optional()
23235
+ name: external_exports.string().optional(),
23236
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23237
+ // which row this is. A producer that collapses several rows onto one target
23238
+ // must carry the marker onto whichever row survives, or the collapse decides
23239
+ // the answer; a survivor may therefore be a built-in expansion still marked
23240
+ // 'authored' because an authored sibling targeted the same thing.
23241
+ // Optional so an older producer — and an older on-disk cache — still parses;
23242
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23243
+ //
23244
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23245
+ // built-in archetype catalog entry a policy is, which every catalog surface
23246
+ // reads and which a caller may state. This one is a statement the PRODUCER
23247
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23248
+ // — the CRUD routes neither accept nor set it.
23249
+ //
23250
+ // A device consumes this in exactly one direction: an 'authored' policy
23251
+ // arriving from a control plane marks the rules it targets as not
23252
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23253
+ // which is what makes it safe to honour from an unsigned cache — the same
23254
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23255
+ provenance: PolicyProvenance.optional()
23554
23256
  }).meta({ id: "Policy" });
23555
23257
  var PolicyBundle = external_exports.object({
23556
23258
  version: external_exports.string(),
@@ -23602,6 +23304,12 @@ var PolicyBundle = external_exports.object({
23602
23304
  customKeywords: external_exports.array(external_exports.string()),
23603
23305
  fetchedAt: external_exports.iso.datetime()
23604
23306
  }).meta({ id: "PolicyBundle" });
23307
+ var POLICY_BUNDLE_SHAPE_ID = [
23308
+ ...Object.keys(PolicyBundle.shape),
23309
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23310
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23311
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23312
+ ].sort().join(",");
23605
23313
  var OBSERVE_ONLY_CATEGORIES = ["config"];
23606
23314
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23607
23315
  var CATEGORY_PEAK_SEVERITY = {
@@ -23622,9 +23330,11 @@ function severityFloorPolicy(category) {
23622
23330
  const peak = CATEGORY_PEAK_SEVERITY[category];
23623
23331
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23624
23332
  }
23625
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23626
23333
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23627
23334
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23335
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23336
+ id: "RedactFallback"
23337
+ });
23628
23338
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23629
23339
  var BUILTIN_POLICY_SPECS = {
23630
23340
  monitor: {
@@ -23661,6 +23371,42 @@ var BUILTIN_POLICY_SPECS = {
23661
23371
  function builtinPolicyToAction(id) {
23662
23372
  return BUILTIN_POLICY_SPECS[id].action;
23663
23373
  }
23374
+ var PALETTE_WEAKEST_FIRST = [
23375
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23376
+ ];
23377
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23378
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23379
+ );
23380
+ var ACTION_STRENGTH_ORDER = [
23381
+ ...BELOW_PALETTE,
23382
+ ...PALETTE_WEAKEST_FIRST
23383
+ ];
23384
+ function actionRank(action) {
23385
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23386
+ }
23387
+ function isActionAtLeast(action, floor) {
23388
+ return actionRank(action) >= actionRank(floor);
23389
+ }
23390
+ function strongerAction(a, b) {
23391
+ return actionRank(a) >= actionRank(b) ? a : b;
23392
+ }
23393
+ function weakestBuiltinAtLeast(floor) {
23394
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23395
+ }
23396
+ var PackPolicyFloor = external_exports.object({
23397
+ /**
23398
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23399
+ * rather than a raw ActionTaken because that is the vocabulary the user
23400
+ * picks from — a floor a UI cannot name is one it cannot explain.
23401
+ */
23402
+ floor: BuiltinPolicyId,
23403
+ /**
23404
+ * True when the organization AUTHORED a policy governing this pack rather
23405
+ * than stating a minimum: it gave the answer, so the pack is not
23406
+ * re-assignable locally in either direction.
23407
+ */
23408
+ locked: external_exports.boolean()
23409
+ }).describe("PackPolicyFloor");
23664
23410
  var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23665
23411
  (id) => !BUILTIN_POLICY_SPECS[id].reversible
23666
23412
  );
@@ -23718,6 +23464,404 @@ var PolicyStatsResponse = external_exports.object({
23718
23464
  detectionsGoverned: external_exports.number().int().nonnegative()
23719
23465
  }).meta({ id: "PolicyStatsResponse" });
23720
23466
 
23467
+ // ../../packages/schema/src/zod/vault.ts
23468
+ var POINTER_FORMAT_VERSION = 2;
23469
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23470
+ var POINTER_TOKEN_PATTERN = new RegExp(
23471
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23472
+ );
23473
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23474
+ function pointerTokenScanner() {
23475
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23476
+ }
23477
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23478
+ var ParsedPointer = external_exports.object({
23479
+ category: DetectionCategory,
23480
+ keyVersion: external_exports.number().int().positive(),
23481
+ pointerId: external_exports.string(),
23482
+ tag: external_exports.string()
23483
+ });
23484
+ var VaultEntry = external_exports.object({
23485
+ pointerId: external_exports.string(),
23486
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23487
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23488
+ // independently of the vault encryption key below.
23489
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23490
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23491
+ // The vault-key epoch this row's ciphertext was sealed under.
23492
+ keyVersion: external_exports.number().int().positive(),
23493
+ // Fixed at first mint and never updated: the same value detected later under a
23494
+ // different rule's category keeps the category it was minted with, so one
23495
+ // value always produces exactly one wire token.
23496
+ category: DetectionCategory,
23497
+ ruleId: external_exports.string(),
23498
+ // Partial-reveal preview for badges and listings. Never the raw value.
23499
+ maskedMatch: external_exports.string(),
23500
+ provider: external_exports.string().optional(),
23501
+ ciphertext: external_exports.string(),
23502
+ nonce: external_exports.string(),
23503
+ authTag: external_exports.string(),
23504
+ // How many times this value has been detected on this machine — the reuse
23505
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23506
+ occurrenceCount: external_exports.number().int().nonnegative(),
23507
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23508
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23509
+ // one row however many paths vault it, so this is what tells a policy sweep
23510
+ // that the row carries somebody's own instruction and not just an assignment
23511
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23512
+ // vaulting of the same value must never clear it — what the user said about
23513
+ // the value does not expire.
23514
+ userAuthorized: external_exports.boolean(),
23515
+ firstSeen: external_exports.string(),
23516
+ lastSeen: external_exports.string()
23517
+ });
23518
+ var PointerDescriptor = external_exports.object({
23519
+ category: DetectionCategory,
23520
+ provider: external_exports.string().optional(),
23521
+ maskedMatch: external_exports.string(),
23522
+ occurrences: external_exports.number().int().nonnegative(),
23523
+ firstSeen: external_exports.string(),
23524
+ lastSeen: external_exports.string()
23525
+ });
23526
+ var PointerIdentity = external_exports.object({
23527
+ ruleId: external_exports.string(),
23528
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23529
+ fingerprintKeyVersion: external_exports.number().int().positive()
23530
+ });
23531
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23532
+ var VaultDerefReason = external_exports.enum([
23533
+ "display",
23534
+ "explicit-reveal",
23535
+ "view-render",
23536
+ "model-input",
23537
+ "remediation",
23538
+ "purge"
23539
+ ]);
23540
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23541
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
23542
+ function isBatchedDerefReason(reason) {
23543
+ return BATCHED_DEREF_REASONS.includes(reason);
23544
+ }
23545
+ var VaultDeref = external_exports.object({
23546
+ id: external_exports.guid(),
23547
+ pointerId: external_exports.string(),
23548
+ at: external_exports.string(),
23549
+ target: DetokenizeTarget,
23550
+ reason: VaultDerefReason,
23551
+ outcome: VaultDerefOutcome,
23552
+ // Present only on a model-target crossing that a reveal grant authorized.
23553
+ grantId: external_exports.string().optional(),
23554
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23555
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23556
+ pointerCount: external_exports.number().int().positive().default(1)
23557
+ });
23558
+ var VaultSightingKind = external_exports.enum([
23559
+ "prompt",
23560
+ "tool-input",
23561
+ "tool-output",
23562
+ "file",
23563
+ "transcript"
23564
+ ]);
23565
+ var VaultSighting = external_exports.object({
23566
+ location: external_exports.string(),
23567
+ kind: VaultSightingKind,
23568
+ firstSeen: external_exports.string(),
23569
+ lastSeen: external_exports.string()
23570
+ });
23571
+ var VaultInventoryEntry = external_exports.object({
23572
+ pointerId: external_exports.string(),
23573
+ category: DetectionCategory,
23574
+ provider: external_exports.string().optional(),
23575
+ maskedMatch: external_exports.string(),
23576
+ occurrences: external_exports.number().int().nonnegative(),
23577
+ firstSeen: external_exports.string(),
23578
+ lastSeen: external_exports.string(),
23579
+ // The active reveal-to-model grant covering this value, when one exists —
23580
+ // the inventory badges it, the row links to revocation.
23581
+ revealGrantId: external_exports.string().nullable(),
23582
+ sightings: external_exports.array(VaultSighting)
23583
+ });
23584
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23585
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23586
+ var MAX_VAULT_PAGE_LIMIT = 200;
23587
+ var ListVaultInventoryQuery = external_exports.object({
23588
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23589
+ // Opaque; names the last row of the page just served.
23590
+ cursor: external_exports.string().optional()
23591
+ });
23592
+ var ListVaultInventoryResponse = external_exports.object({
23593
+ // Vaulted values across the whole store, not just this page — cursor-
23594
+ // independent, so paging never changes what the count claims.
23595
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23596
+ items: external_exports.array(VaultInventoryEntry),
23597
+ // `null` once the last page is reached.
23598
+ nextCursor: external_exports.string().nullable()
23599
+ });
23600
+ var ListVaultReuseQuery = external_exports.object({
23601
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23602
+ cursor: external_exports.string().optional()
23603
+ });
23604
+ var ListVaultReuseResponse = external_exports.object({
23605
+ // Reused values across the whole store — the number the section's claim
23606
+ // ("values detected in more than one place") is about.
23607
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23608
+ items: external_exports.array(VaultInventoryEntry),
23609
+ nextCursor: external_exports.string().nullable()
23610
+ });
23611
+ var ListVaultDerefsQuery = external_exports.object({
23612
+ // Include the batched, high-volume reasons (display, view-render). Omitted
23613
+ // hides them and counts them into `hiddenBatched` instead, so the model
23614
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23615
+ // over a Server Action, which preserves the type, never as a URL param.
23616
+ includeBatched: external_exports.boolean().optional(),
23617
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23618
+ cursor: external_exports.string().optional()
23619
+ });
23620
+ var ListVaultDerefsResponse = external_exports.object({
23621
+ items: external_exports.array(VaultDeref),
23622
+ nextCursor: external_exports.string().nullable(),
23623
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
23624
+ // this page — it is the count the "N hidden" line and its toggle speak for.
23625
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
23626
+ hiddenBatched: external_exports.number().int().nonnegative()
23627
+ });
23628
+ var VaultKeyCustody = external_exports.string();
23629
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
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, table2) {
25070
25220
  if (!columns.includes("sync_claimed_at")) {
25071
25221
  db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
25072
25222
  }
25223
+ if (!columns.includes("outbox_owed")) {
25224
+ db.exec(`ALTER TABLE ${table2} 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.
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.
27254
27443
  *
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.
27259
- *
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
 
32000
32526
  // ../../packages/persistence/src/vault/crypto.ts
32001
32527
  import {
@@ -32108,8 +32634,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32108
32634
  // ../../packages/persistence/src/vault/key-provider.ts
32109
32635
  import { execFileSync } from "child_process";
32110
32636
  import { randomBytes as randomBytes2 } from "crypto";
32111
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32112
- import { join as join10 } from "path";
32637
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32638
+ import { join as join11 } from "path";
32113
32639
  var VAULT_OCCUPANT_REASON = {
32114
32640
  symlink: "the path is a symlink; remove it so a keyring can be created",
32115
32641
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32208,7 +32734,7 @@ function claimRotationLock(lock, owner) {
32208
32734
  throw asError(err);
32209
32735
  }
32210
32736
  try {
32211
- writeFileSync3(join10(lock, LOCK_OWNER_FILE), `${owner}
32737
+ writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
32212
32738
  `, { mode: DATA_FILE_MODE });
32213
32739
  return true;
32214
32740
  } catch (err) {
@@ -32217,7 +32743,7 @@ function claimRotationLock(lock, owner) {
32217
32743
  }
32218
32744
  }
32219
32745
  function acquireRotationLock(keysDir2) {
32220
- const lock = join10(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32746
+ const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32221
32747
  const owner = randomBytes2(16).toString("hex");
32222
32748
  if (claimRotationLock(lock, owner)) return { lock, owner };
32223
32749
  let held;
@@ -32244,7 +32770,7 @@ function acquireRotationLock(keysDir2) {
32244
32770
  }
32245
32771
  function releaseRotationLock(lease) {
32246
32772
  try {
32247
- if (readFileSync6(join10(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32773
+ if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32248
32774
  } catch {
32249
32775
  return;
32250
32776
  }
@@ -32265,7 +32791,7 @@ var FileKeyProvider = class {
32265
32791
  this.#keysDir = keysDir2;
32266
32792
  }
32267
32793
  get filePath() {
32268
- return join10(this.#keysDir, VAULT_KEY_FILENAME);
32794
+ return join11(this.#keysDir, VAULT_KEY_FILENAME);
32269
32795
  }
32270
32796
  loadOrCreate() {
32271
32797
  return asAsync(() => {
@@ -32295,7 +32821,7 @@ var FileKeyProvider = class {
32295
32821
  #read() {
32296
32822
  let raw;
32297
32823
  try {
32298
- raw = readFileSync6(this.filePath, "utf8");
32824
+ raw = readFileSync7(this.filePath, "utf8");
32299
32825
  } catch (err) {
32300
32826
  if (err.code === "ENOENT") return null;
32301
32827
  throw err instanceof Error ? err : new Error(String(err));
@@ -32582,7 +33108,14 @@ var SecretVault = class {
32582
33108
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
32583
33109
  const now = this.#now();
32584
33110
  if (existing) {
32585
- this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
33111
+ this.#repo.upsert(
33112
+ {
33113
+ ...existing,
33114
+ provider: existing.provider ?? void 0,
33115
+ userAuthorized: meta4.userAuthorized === true
33116
+ },
33117
+ now
33118
+ );
32586
33119
  return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
32587
33120
  }
32588
33121
  const { material, version: version2 } = await this.#keys.loadOrCreate();
@@ -32604,6 +33137,7 @@ var SecretVault = class {
32604
33137
  ruleId: meta4.ruleId,
32605
33138
  maskedMatch: meta4.maskedMatch,
32606
33139
  provider: meta4.provider,
33140
+ userAuthorized: meta4.userAuthorized === true,
32607
33141
  ciphertext: sealed.ciphertext.toString("base64"),
32608
33142
  nonce: sealed.nonce.toString("base64"),
32609
33143
  authTag: sealed.authTag.toString("base64")
@@ -32923,11 +33457,11 @@ var SecretVault = class {
32923
33457
 
32924
33458
  // ../../packages/persistence/src/warn-era-cap.ts
32925
33459
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32926
- import { join as join11 } from "path";
33460
+ import { join as join12 } from "path";
32927
33461
  var MARKER = "warn-era-capped";
32928
33462
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32929
33463
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32930
- const marker = join11(dataDir2, MARKER);
33464
+ const marker = join12(dataDir2, MARKER);
32931
33465
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32932
33466
  const capped = db.policies.capCategoryActions();
32933
33467
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32937,7 +33471,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32937
33471
 
32938
33472
  // ../../packages/plugin-sdk/src/config.ts
32939
33473
  import { existsSync as existsSync7 } from "fs";
32940
- import { join as join12 } from "path";
33474
+ import { join as join13 } from "path";
32941
33475
 
32942
33476
  // ../../packages/plugin-sdk/src/provider-env.ts
32943
33477
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32991,7 +33525,7 @@ function resolveProvider() {
32991
33525
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32992
33526
  try {
32993
33527
  ensureLayoutDirSync(base);
32994
- const settingsFile = join12(settingsDir(base), "settings.json");
33528
+ const settingsFile = join13(settingsDir(base), "settings.json");
32995
33529
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
32996
33530
  } catch {
32997
33531
  }
@@ -33015,9 +33549,9 @@ function resolveProviderSafe(resolveProviderFn) {
33015
33549
  }
33016
33550
 
33017
33551
  // ../../packages/plugin-sdk/src/config-inventory.ts
33018
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33552
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33019
33553
  import { homedir as homedir2 } from "os";
33020
- import { basename as basename3, join as join14 } from "path";
33554
+ import { basename as basename3, join as join15 } from "path";
33021
33555
 
33022
33556
  // ../../packages/detections/src/egress/registry.ts
33023
33557
  var EXTRACTOR_VERSION = "1";
@@ -36106,8 +36640,8 @@ function bundledDetections() {
36106
36640
  }
36107
36641
 
36108
36642
  // ../../packages/plugin-sdk/src/repo.ts
36109
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
36110
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
36643
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36644
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
36111
36645
  function resolveRepo(cwd) {
36112
36646
  try {
36113
36647
  const root = findGitRoot(cwd);
@@ -36129,36 +36663,36 @@ function resolveWorktreeRoot(cwd) {
36129
36663
  function findGitRoot(start) {
36130
36664
  let dir = start;
36131
36665
  for (; ; ) {
36132
- if (existsSync8(join13(dir, ".git"))) return dir;
36133
- const parent = dirname3(dir);
36666
+ if (existsSync8(join14(dir, ".git"))) return dir;
36667
+ const parent = dirname4(dir);
36134
36668
  if (parent === dir) return void 0;
36135
36669
  dir = parent;
36136
36670
  }
36137
36671
  }
36138
36672
  function resolveGitContext(root) {
36139
- const dotGit = join13(root, ".git");
36673
+ const dotGit = join14(root, ".git");
36140
36674
  try {
36141
36675
  if (statSync6(dotGit).isDirectory()) {
36142
- return { configPath: join13(dotGit, "config"), headRoot: root };
36676
+ return { configPath: join14(dotGit, "config"), headRoot: root };
36143
36677
  }
36144
36678
  } catch {
36145
36679
  return void 0;
36146
36680
  }
36147
36681
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
36148
36682
  if (!target) return void 0;
36149
- const gitdir = isAbsolute(target) ? target : join13(root, target);
36150
- if (existsSync8(join13(gitdir, "config"))) {
36151
- return { configPath: join13(gitdir, "config"), headRoot: root };
36683
+ const gitdir = isAbsolute(target) ? target : join14(root, target);
36684
+ if (existsSync8(join14(gitdir, "config"))) {
36685
+ return { configPath: join14(gitdir, "config"), headRoot: root };
36152
36686
  }
36153
- const commonRaw = safeRead(join13(gitdir, "commondir"))?.trim();
36687
+ const commonRaw = safeRead(join14(gitdir, "commondir"))?.trim();
36154
36688
  if (!commonRaw) return void 0;
36155
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join13(gitdir, commonRaw);
36156
- const headRoot = basename2(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
36157
- return { configPath: join13(commonGitDir, "config"), headRoot };
36689
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join14(gitdir, commonRaw);
36690
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
36691
+ return { configPath: join14(commonGitDir, "config"), headRoot };
36158
36692
  }
36159
36693
  function safeRead(path) {
36160
36694
  try {
36161
- return readFileSync7(path, "utf8");
36695
+ return readFileSync8(path, "utf8");
36162
36696
  } catch {
36163
36697
  return void 0;
36164
36698
  }
@@ -36703,8 +37237,8 @@ function createGuardedScanner(partition, gateway, opts) {
36703
37237
 
36704
37238
  // ../../packages/plugin-sdk/src/ignore-layers.ts
36705
37239
  var import_ignore = __toESM(require_ignore(), 1);
36706
- import { readFileSync as readFileSync9 } from "fs";
36707
- import { join as join15 } from "path";
37240
+ import { readFileSync as readFileSync10 } from "fs";
37241
+ import { join as join16 } from "path";
36708
37242
 
36709
37243
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
36710
37244
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -36715,20 +37249,55 @@ import {
36715
37249
  fstatSync,
36716
37250
  mkdirSync as mkdirSync2,
36717
37251
  openSync as openSync2,
36718
- readFileSync as readFileSync10,
37252
+ readFileSync as readFileSync11,
36719
37253
  readSync,
36720
37254
  writeFileSync as writeFileSync5
36721
37255
  } from "fs";
36722
- import { join as join16 } from "path";
37256
+ import { join as join17 } from "path";
36723
37257
  var TAIL_BYTES = 256 * 1024;
36724
37258
 
36725
37259
  // ../../packages/plugin-sdk/src/nudge.ts
36726
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
36727
- import { join as join17 } from "path";
37260
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
37261
+ import { join as join18 } from "path";
36728
37262
 
36729
37263
  // ../../packages/plugin-sdk/src/paths.ts
36730
37264
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
36731
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
37265
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
37266
+
37267
+ // ../../packages/plugin-sdk/src/policy-resolver.ts
37268
+ function createPolicyResolver(bundle) {
37269
+ const byRule = /* @__PURE__ */ new Map();
37270
+ const byCategory = /* @__PURE__ */ new Map();
37271
+ let reversible = /* @__PURE__ */ new Set();
37272
+ try {
37273
+ for (const policy of bundle.policies) {
37274
+ if (!policy.enabled) continue;
37275
+ if ("ruleId" in policy.target) {
37276
+ if (!byRule.has(policy.target.ruleId)) byRule.set(policy.target.ruleId, policy.action);
37277
+ } else if (!byCategory.has(policy.target.category)) {
37278
+ byCategory.set(policy.target.category, policy.action);
37279
+ }
37280
+ }
37281
+ reversible = new Set(bundle.reversibleRuleIds ?? []);
37282
+ } catch {
37283
+ byRule.clear();
37284
+ byCategory.clear();
37285
+ reversible = /* @__PURE__ */ new Set();
37286
+ }
37287
+ return {
37288
+ actionFor(ruleId, category) {
37289
+ const byRuleAction = byRule.get(ruleId);
37290
+ if (byRuleAction !== void 0) return byRuleAction;
37291
+ const byCategoryAction = byCategory.get(category);
37292
+ if (byCategoryAction !== void 0) return byCategoryAction;
37293
+ const fallback = DEFAULT_ACTIONS[category];
37294
+ return fallback ?? "log";
37295
+ },
37296
+ isReversible(ruleId) {
37297
+ return reversible.has(ruleId);
37298
+ }
37299
+ };
37300
+ }
36732
37301
 
36733
37302
  // ../../packages/plugin-sdk/src/posture.ts
36734
37303
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
@@ -36742,7 +37311,7 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
36742
37311
 
36743
37312
  // ../../packages/plugin-sdk/src/project-files.ts
36744
37313
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
36745
- import { basename as basename5, join as join18 } from "path";
37314
+ import { basename as basename5, join as join19 } from "path";
36746
37315
 
36747
37316
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
36748
37317
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -36783,7 +37352,6 @@ function safeMaskedMatch(rawMatch) {
36783
37352
  // ../../packages/plugin-sdk/src/runtime.ts
36784
37353
  import { randomUUID as randomUUID14 } from "crypto";
36785
37354
  var ENFORCEMENT_CEILING_ENABLED = false;
36786
- var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
36787
37355
  function startTiming() {
36788
37356
  try {
36789
37357
  return performance.now();
@@ -36820,28 +37388,22 @@ function createPluginRuntime(gateway, settings, opts) {
36820
37388
  bundlesPacked = true;
36821
37389
  }
36822
37390
  const policyMode = settings.policy;
37391
+ const redactFallback = settings.redactFallback;
36823
37392
  const dataDir2 = opts?.dataDir;
36824
- let policies = [];
36825
37393
  let rules = [];
36826
37394
  let scanner;
36827
37395
  let bundleExceptions = [];
36828
37396
  let initialized = false;
36829
- const ruleActionIndex = /* @__PURE__ */ new Map();
36830
- const categoryActionIndex = /* @__PURE__ */ new Map();
36831
- let reversibleRuleIndex = /* @__PURE__ */ new Set();
37397
+ let resolver = createPolicyResolver({
37398
+ version: "",
37399
+ policies: [],
37400
+ customKeywords: [],
37401
+ fetchedAt: ""
37402
+ });
36832
37403
  async function ensureInitialized() {
36833
37404
  if (initialized) return;
36834
37405
  const bundle = await gateway.getPolicyBundle();
36835
- policies = bundle.policies;
36836
- for (const p of policies) {
36837
- if (!p.enabled) continue;
36838
- if ("ruleId" in p.target) {
36839
- if (!ruleActionIndex.has(p.target.ruleId)) ruleActionIndex.set(p.target.ruleId, p.action);
36840
- } else if (!categoryActionIndex.has(p.target.category)) {
36841
- categoryActionIndex.set(p.target.category, p.action);
36842
- }
36843
- }
36844
- reversibleRuleIndex = new Set(bundle.reversibleRuleIds ?? []);
37406
+ resolver = createPolicyResolver(bundle);
36845
37407
  const bundledProbeKeys = new Set(
36846
37408
  getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
36847
37409
  );
@@ -36894,33 +37456,28 @@ function createPluginRuntime(gateway, settings, opts) {
36894
37456
  return cachedKey;
36895
37457
  }
36896
37458
  function resolveAction(ruleId, category) {
36897
- const byRule = ruleActionIndex.get(ruleId);
36898
- if (byRule !== void 0) return byRule;
36899
- const byCategory = categoryActionIndex.get(category);
36900
- if (byCategory !== void 0) return byCategory;
36901
- const fallback = DEFAULT_ACTIONS[category];
36902
- return fallback ?? "log";
36903
- }
36904
- function actionForFinding(finding, excepted) {
37459
+ return resolver.actionFor(ruleId, category);
37460
+ }
37461
+ function actionForFinding(finding, excepted, rewritable = true) {
36905
37462
  if (excepted?.has(finding)) return "allow";
36906
37463
  const action = resolveAction(finding.ruleId, finding.category);
37464
+ if (!rewritable && action === "redact") return builtinPolicyToAction(redactFallback);
36907
37465
  if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
36908
37466
  return "warn";
36909
37467
  }
36910
37468
  return action;
36911
37469
  }
36912
- function decide(findings, text, excepted) {
37470
+ function decide(findings, text, excepted, rewritable = true) {
36913
37471
  if (findings.length === 0) return { action: "log", text, findings: [] };
36914
- const actionFor = (finding) => actionForFinding(finding, excepted);
37472
+ const actionFor = (finding) => actionForFinding(finding, excepted, rewritable);
36915
37473
  let worst = "log";
36916
37474
  for (const finding of findings) {
36917
- const action = actionFor(finding);
36918
- if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
37475
+ worst = strongerAction(worst, actionFor(finding));
36919
37476
  }
36920
37477
  if (worst === "block") return { action: "block", text: null, findings };
36921
37478
  if (worst === "redact") {
36922
37479
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
36923
- const reversibleFindings = redactFindings.filter((f) => reversibleRuleIndex.has(f.ruleId));
37480
+ const reversibleFindings = redactFindings.filter((f) => resolver.isReversible(f.ruleId));
36924
37481
  return {
36925
37482
  action: "redact",
36926
37483
  text: redact(text, redactFindings),
@@ -36996,7 +37553,7 @@ function createPluginRuntime(gateway, settings, opts) {
36996
37553
  return { excepted: /* @__PURE__ */ new Set(), exceptionIds: [] };
36997
37554
  }
36998
37555
  }
36999
- async function recordBlockedDetections(decision, excepted, ctx, fpCache) {
37556
+ async function recordBlockedDetections(decision, excepted, ctx, fpCache, rewritable = true) {
37000
37557
  const references = [];
37001
37558
  try {
37002
37559
  if (decision.action !== "block" && decision.action !== "redact") return references;
@@ -37004,7 +37561,7 @@ function createPluginRuntime(gateway, settings, opts) {
37004
37561
  if (!key) return references;
37005
37562
  const seen = /* @__PURE__ */ new Set();
37006
37563
  for (const finding of decision.findings) {
37007
- const action = actionForFinding(finding, excepted);
37564
+ const action = actionForFinding(finding, excepted, rewritable);
37008
37565
  if (action !== "block" && action !== "redact") continue;
37009
37566
  const fp = fingerprintOf(key, finding, fpCache);
37010
37567
  const pair = `${finding.ruleId}:${fp}`;
@@ -37031,7 +37588,7 @@ function createPluginRuntime(gateway, settings, opts) {
37031
37588
  }
37032
37589
  return references;
37033
37590
  }
37034
- async function evaluate(text, context, ctx) {
37591
+ async function evaluate(text, context, ctx, rewritable = true) {
37035
37592
  try {
37036
37593
  await ensureInitialized();
37037
37594
  if (!scanner) throw new Error("the runtime initialized without a scanner");
@@ -37040,8 +37597,14 @@ function createPluginRuntime(gateway, settings, opts) {
37040
37597
  const findings = dropShieldedFindings(matched, shielded.spans);
37041
37598
  const fpCache = /* @__PURE__ */ new Map();
37042
37599
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
37043
- const decision = decide(findings, text, excepted);
37044
- const blockedReferences = await recordBlockedDetections(decision, excepted, ctx, fpCache);
37600
+ const decision = decide(findings, text, excepted, rewritable);
37601
+ const blockedReferences = await recordBlockedDetections(
37602
+ decision,
37603
+ excepted,
37604
+ ctx,
37605
+ fpCache,
37606
+ rewritable
37607
+ );
37045
37608
  if (blockedReferences.length > 0) decision.blockedReferences = blockedReferences;
37046
37609
  return { decision, excepted, exceptionIds };
37047
37610
  } catch {
@@ -37065,12 +37628,16 @@ function createPluginRuntime(gateway, settings, opts) {
37065
37628
  sourceTool: input2.sourceTool,
37066
37629
  metadata: input2.metadata,
37067
37630
  preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
37068
- }
37631
+ },
37632
+ opts2.rewritable
37069
37633
  );
37070
37634
  if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
37071
37635
  try {
37072
37636
  const contentHash = contentHashOf(input2.text);
37073
- const storedContent = decision.findings.length > 0 ? redact(input2.text, decision.findings) : input2.text;
37637
+ const maskedFindings = decision.findings.filter(
37638
+ (match) => isActionAtLeast(actionForFinding(match, excepted, opts2.rewritable), "redact")
37639
+ );
37640
+ const storedContent = maskedFindings.length > 0 ? redact(input2.text, maskedFindings) : input2.text;
37074
37641
  const inspectionMs = elapsedMs(timingStartedAt);
37075
37642
  const metadata = exceptionIds.length > 0 || inspectionMs !== void 0 ? {
37076
37643
  ...input2.metadata,
@@ -37107,7 +37674,7 @@ function createPluginRuntime(gateway, settings, opts) {
37107
37674
  severity: match.severity,
37108
37675
  span: match.span,
37109
37676
  maskedMatch,
37110
- actionTaken: actionForFinding(match, excepted),
37677
+ actionTaken: actionForFinding(match, excepted, opts2.rewritable),
37111
37678
  confidence: match.confidence,
37112
37679
  ...findingKey ? { findingKey } : {}
37113
37680
  };
@@ -37147,7 +37714,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
37147
37714
 
37148
37715
  // ../../packages/plugin-sdk/src/throttle.ts
37149
37716
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
37150
- import { join as join19 } from "path";
37717
+ import { join as join20 } from "path";
37151
37718
 
37152
37719
  // ../../packages/plugin-sdk/src/tokenize.ts
37153
37720
  function redactedPlaceholder(category) {
@@ -37209,14 +37776,26 @@ var SecretVaultGlue = class {
37209
37776
  }
37210
37777
  async tokenizeText(text, opts) {
37211
37778
  try {
37212
- const findings = opts?.findings ?? this.#selfScan(text);
37213
- const reversible = opts?.reversible;
37214
- const keeps = (finding) => reversible === void 0 || reversible.has(finding);
37215
- if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
37216
- if (findings.length === 0) return { text, pointers: [], degraded: [] };
37779
+ const supplied = opts?.findings;
37780
+ const resolver = opts?.resolver;
37781
+ const scanned = supplied ?? this.#selfScan(text);
37782
+ if (scanned === null) {
37783
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
37784
+ }
37785
+ const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
37786
+ (f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
37787
+ ) : scanned;
37788
+ let reversible = opts?.reversible;
37789
+ if (resolver !== void 0 && reversible === void 0) {
37790
+ reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
37791
+ }
37792
+ const reversibleSet = reversible;
37793
+ const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
37794
+ if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
37217
37795
  const groups = groupSpans(text, findings);
37218
37796
  const pointers = [];
37219
37797
  const degraded = [];
37798
+ const redacted = [];
37220
37799
  let out = text;
37221
37800
  for (const group of [...groups].reverse()) {
37222
37801
  const original = text.slice(group.start, group.end);
@@ -37230,6 +37809,7 @@ var SecretVaultGlue = class {
37230
37809
  degraded.unshift({ category: group.category });
37231
37810
  } else if (!keeps(finding)) {
37232
37811
  replacement = redactedPlaceholder(finding.category);
37812
+ redacted.unshift({ category: finding.category });
37233
37813
  } else {
37234
37814
  replacement = await this.tokenizeValue(finding.rawMatch, {
37235
37815
  ruleId: finding.ruleId,
@@ -37250,9 +37830,9 @@ var SecretVaultGlue = class {
37250
37830
  }
37251
37831
  }
37252
37832
  }
37253
- return { text: out, pointers, degraded };
37833
+ return { text: out, pointers, degraded, redacted };
37254
37834
  } catch {
37255
- return { text: "[REDACTED]", pointers: [], degraded: [] };
37835
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
37256
37836
  }
37257
37837
  }
37258
37838
  async detokenizeText(text, opts) {
@@ -37490,7 +38070,7 @@ function routeRemediationOption(option, handlers) {
37490
38070
 
37491
38071
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
37492
38072
  import { writeFileSync as writeFileSync8 } from "fs";
37493
- import { join as join20 } from "path";
38073
+ import { join as join21 } from "path";
37494
38074
  var GENERIC_CONSOLE_PATH = "rotate via the provider's own console";
37495
38075
  var CONSOLE_PATHS = {
37496
38076
  anthropic: "console.anthropic.com \u2192 Settings \u2192 API keys",
@@ -37590,7 +38170,7 @@ function generateRotationChecklist(input2) {
37590
38170
  try {
37591
38171
  const target = resolveRotationChecklistTarget(input2.cwd);
37592
38172
  targetDirectory = target.directory;
37593
- const filePath = join20(target.directory, "rotation-checklist.md");
38173
+ const filePath = join21(target.directory, "rotation-checklist.md");
37594
38174
  writeRotationChecklist(input2.entries, target.directory);
37595
38175
  return {
37596
38176
  status: "written",
@@ -37701,9 +38281,9 @@ var RANK = Object.fromEntries(
37701
38281
  );
37702
38282
 
37703
38283
  // ../../packages/setup-wizard/src/triage/plan-file.ts
37704
- import { mkdtempSync, readFileSync as readFileSync12, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
38284
+ import { mkdtempSync, readFileSync as readFileSync13, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37705
38285
  import { tmpdir } from "os";
37706
- import { basename as basename6, dirname as dirname5, join as join21 } from "path";
38286
+ import { basename as basename6, dirname as dirname6, join as join22 } from "path";
37707
38287
  var SuppressionEntrySchema = external_exports.object({
37708
38288
  ruleId: external_exports.string(),
37709
38289
  category: DetectionCategory,
@@ -37907,7 +38487,7 @@ function renderRemediationDecision(findings, moreCount, registry2) {
37907
38487
  }
37908
38488
 
37909
38489
  // src/remediation/surfaced-redact.ts
37910
- import { readFileSync as readFileSync20 } from "fs";
38490
+ import { readFileSync as readFileSync21 } from "fs";
37911
38491
 
37912
38492
  // ../../packages/plugin-runtime/src/attached/egress-wire.ts
37913
38493
  import { createHash as createHash5 } from "crypto";
@@ -37946,6 +38526,307 @@ function toEgressIngestRequest(input2) {
37946
38526
  };
37947
38527
  }
37948
38528
 
38529
+ // ../../packages/remote/src/http.ts
38530
+ import { request as httpRequest } from "http";
38531
+ import { request as httpsRequest } from "https";
38532
+ var DEFAULT_TIMEOUT_MS = 1e4;
38533
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
38534
+ var RemoteRequestError = class extends Error {
38535
+ constructor(status) {
38536
+ super(`control-plane request failed with status ${String(status)}`);
38537
+ this.status = status;
38538
+ this.name = "RemoteRequestError";
38539
+ }
38540
+ status;
38541
+ };
38542
+ var RemoteRouteAbsent = class extends Error {
38543
+ constructor(route2) {
38544
+ super(`control plane does not serve ${route2}`);
38545
+ this.route = route2;
38546
+ this.name = "RemoteRouteAbsent";
38547
+ }
38548
+ route;
38549
+ };
38550
+ var RemoteRequestInvalid = class extends Error {
38551
+ constructor(route2, cause) {
38552
+ super(`refusing to send a malformed body to ${route2}`);
38553
+ this.cause = cause;
38554
+ this.name = "RemoteRequestInvalid";
38555
+ }
38556
+ cause;
38557
+ };
38558
+ var RemoteResponseInvalid = class extends Error {
38559
+ constructor(route2, detail) {
38560
+ super(`control plane answered ${route2} with ${detail}`);
38561
+ this.name = "RemoteResponseInvalid";
38562
+ }
38563
+ };
38564
+ var RemoteTransportError = class extends Error {
38565
+ /**
38566
+ * The status the peer sent, when headers arrived and only the BODY was
38567
+ * refused.
38568
+ *
38569
+ * Undefined for the ordinary case this class was written for — no answer at
38570
+ * all. It exists because two paths reject after a status has already been
38571
+ * delivered: an oversized body and an aborted response. Discarding it there
38572
+ * reported a deployment answering 401 with a verbose body as a network
38573
+ * outage, which sends the reader to look at their network instead of their
38574
+ * credential.
38575
+ */
38576
+ constructor(reason, status) {
38577
+ super(`control-plane request did not complete: ${reason}`);
38578
+ this.status = status;
38579
+ this.name = "RemoteTransportError";
38580
+ }
38581
+ status;
38582
+ };
38583
+ async function send(options) {
38584
+ const url2 = new URL(options.url);
38585
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38586
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
38587
+ const requestOptions = {
38588
+ method: options.method,
38589
+ headers: {
38590
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
38591
+ // last they win, and two of the values below are ones no caller may
38592
+ // replace: `x-api-key` is the credential, and `content-length` is the
38593
+ // byte count that stops a multi-byte body being truncated by the
38594
+ // receiver. `SendOptions.headers` is a free-form record on an exported
38595
+ // function, so "no caller does that today" is not the guarantee to rely
38596
+ // on. The one header any caller actually passes — `if-none-match` on the
38597
+ // conditional GET — is untouched by this order.
38598
+ ...options.headers,
38599
+ // The credential. One header, matching what the deployment authenticates
38600
+ // on; a second copy in an `Authorization` header would be one more place
38601
+ // it can be logged by an intermediary for no gain.
38602
+ //
38603
+ // Spread conditionally rather than assigned as `undefined`: Node's header
38604
+ // handling and `content-length` bookkeeping treat a present-but-undefined
38605
+ // key differently from an absent one, and "the header is not there" is
38606
+ // the property the attach flow needs.
38607
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
38608
+ accept: "application/json",
38609
+ ...options.body === void 0 ? {} : {
38610
+ "content-type": "application/json",
38611
+ // Byte length, not string length: a multi-byte body sent with a
38612
+ // character count is truncated by the receiver.
38613
+ "content-length": String(Buffer.byteLength(options.body))
38614
+ }
38615
+ }
38616
+ };
38617
+ return new Promise((resolve3, reject) => {
38618
+ let settled = false;
38619
+ const fail2 = (reason, status) => {
38620
+ if (settled) return;
38621
+ settled = true;
38622
+ reject(new RemoteTransportError(reason, status));
38623
+ };
38624
+ const req = send_(url2, requestOptions, (res) => {
38625
+ const chunks = [];
38626
+ let size = 0;
38627
+ res.on("data", (chunk) => {
38628
+ size += chunk.length;
38629
+ if (size > MAX_RESPONSE_BYTES) {
38630
+ fail2(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
38631
+ res.destroy();
38632
+ req.destroy();
38633
+ return;
38634
+ }
38635
+ chunks.push(chunk);
38636
+ });
38637
+ res.on("aborted", () => {
38638
+ fail2("the response was aborted", res.statusCode);
38639
+ });
38640
+ res.on("end", () => {
38641
+ if (settled) return;
38642
+ settled = true;
38643
+ resolve3({
38644
+ status: res.statusCode ?? 0,
38645
+ headers: res.headers,
38646
+ body: Buffer.concat(chunks).toString("utf8")
38647
+ });
38648
+ });
38649
+ });
38650
+ const deadline = setTimeout(() => {
38651
+ fail2(`no response within ${String(timeoutMs)}ms`);
38652
+ req.destroy();
38653
+ }, timeoutMs);
38654
+ deadline.unref();
38655
+ req.on("upgrade", (_res, socket) => {
38656
+ fail2("the deployment answered with a protocol upgrade");
38657
+ socket.destroy();
38658
+ });
38659
+ req.on("close", () => {
38660
+ fail2("the connection closed before a response was read");
38661
+ clearTimeout(deadline);
38662
+ });
38663
+ req.on("error", (err) => {
38664
+ fail2(err.message);
38665
+ });
38666
+ if (options.body !== void 0) req.write(options.body);
38667
+ req.end();
38668
+ });
38669
+ }
38670
+
38671
+ // ../../packages/remote/src/client.ts
38672
+ var ROUTES = {
38673
+ events: "/v1/events",
38674
+ auditEvents: "/v1/audit-events",
38675
+ auditEventsBatch: "/v1/audit-events/batch",
38676
+ inventory: "/v1/inventory",
38677
+ storePosture: "/v1/store-posture",
38678
+ policyBundle: "/v1/policy-bundle",
38679
+ whoami: "/v1/plugin/whoami",
38680
+ shares: "/v1/shares",
38681
+ commands: "/v1/plugin/commands"
38682
+ };
38683
+ function ackRoute(id) {
38684
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
38685
+ }
38686
+ function headerValue(response, name) {
38687
+ const raw = response.headers[name];
38688
+ if (raw === void 0) return void 0;
38689
+ return Array.isArray(raw) ? raw[0] : raw;
38690
+ }
38691
+ function okBody(response) {
38692
+ if (response.status < 200 || response.status >= 300) {
38693
+ throw new RemoteRequestError(response.status);
38694
+ }
38695
+ return response.body;
38696
+ }
38697
+ function parsed(schema, body, route2) {
38698
+ let json2;
38699
+ try {
38700
+ json2 = JSON.parse(body);
38701
+ } catch {
38702
+ throw new RemoteResponseInvalid(route2, "a body that is not JSON");
38703
+ }
38704
+ const result = schema.safeParse(json2);
38705
+ if (!result.success) {
38706
+ throw new RemoteResponseInvalid(route2, "a body this client cannot read");
38707
+ }
38708
+ return result.data;
38709
+ }
38710
+ function withoutTrailingSlashes(endpoint) {
38711
+ let end = endpoint.length;
38712
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
38713
+ return endpoint.slice(0, end);
38714
+ }
38715
+ var SLASH = "/".charCodeAt(0);
38716
+ function createRemoteClient(options) {
38717
+ const base = withoutTrailingSlashes(options.endpoint);
38718
+ const url2 = (route2) => `${base}${route2}`;
38719
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
38720
+ const sendOne = async (event) => {
38721
+ const validated = RecordAuditEventRequest.safeParse(event);
38722
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
38723
+ const response = await send({
38724
+ ...common,
38725
+ method: "POST",
38726
+ url: url2(ROUTES.auditEvents),
38727
+ body: JSON.stringify(validated.data)
38728
+ });
38729
+ okBody(response);
38730
+ };
38731
+ return {
38732
+ async ingestEvents(batch) {
38733
+ const response = await send({
38734
+ ...common,
38735
+ method: "POST",
38736
+ url: url2(ROUTES.events),
38737
+ body: JSON.stringify(batch)
38738
+ });
38739
+ return parsed(IngestAck, okBody(response), ROUTES.events);
38740
+ },
38741
+ async ingestInventory(context) {
38742
+ const response = await send({
38743
+ ...common,
38744
+ method: "POST",
38745
+ url: url2(ROUTES.inventory),
38746
+ body: JSON.stringify(context)
38747
+ });
38748
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
38749
+ },
38750
+ async recordAuditEvent(event) {
38751
+ await sendOne(event);
38752
+ },
38753
+ async recordAuditEvents(events, opts) {
38754
+ const validated = RecordAuditEventBatch.safeParse({ events });
38755
+ if (!validated.success) {
38756
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
38757
+ }
38758
+ const response = await send({
38759
+ ...common,
38760
+ method: "POST",
38761
+ url: url2(ROUTES.auditEventsBatch),
38762
+ body: JSON.stringify(validated.data)
38763
+ });
38764
+ if (response.status === 404) {
38765
+ if (opts?.fallbackToSingleEvents !== true) {
38766
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
38767
+ }
38768
+ for (const event of validated.data.events) await sendOne(event);
38769
+ return { accepted: validated.data.events.length };
38770
+ }
38771
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
38772
+ },
38773
+ async reportStorePosture(snapshot) {
38774
+ const response = await send({
38775
+ ...common,
38776
+ method: "POST",
38777
+ url: url2(ROUTES.storePosture),
38778
+ body: JSON.stringify(snapshot)
38779
+ });
38780
+ okBody(response);
38781
+ },
38782
+ async getPolicyBundle(etag) {
38783
+ const response = await send({
38784
+ ...common,
38785
+ method: "GET",
38786
+ url: url2(ROUTES.policyBundle),
38787
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
38788
+ });
38789
+ if (response.status === 304) {
38790
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
38791
+ }
38792
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
38793
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
38794
+ },
38795
+ async whoami() {
38796
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
38797
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
38798
+ },
38799
+ async recordProjectEgress(request) {
38800
+ const validated = EgressIngestRequest.safeParse(request);
38801
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
38802
+ const response = await send({
38803
+ ...common,
38804
+ method: "POST",
38805
+ url: url2(ROUTES.shares),
38806
+ body: JSON.stringify(validated.data)
38807
+ });
38808
+ okBody(response);
38809
+ },
38810
+ async pollCommand() {
38811
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
38812
+ if (response.status === 404) return null;
38813
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
38814
+ },
38815
+ async ackCommand(id, body) {
38816
+ const validated = DeviceCommandAckBody.safeParse(body);
38817
+ const route2 = ackRoute(id);
38818
+ if (!validated.success) throw new RemoteRequestInvalid(route2, validated.error);
38819
+ const response = await send({
38820
+ ...common,
38821
+ method: "POST",
38822
+ url: url2(route2),
38823
+ body: JSON.stringify(validated.data)
38824
+ });
38825
+ okBody(response);
38826
+ }
38827
+ };
38828
+ }
38829
+
37949
38830
  // ../../packages/plugin-runtime/src/attached/failure.ts
37950
38831
  function statusOf(err) {
37951
38832
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -37964,12 +38845,27 @@ function classifyFailure(err) {
37964
38845
  }
37965
38846
  }
37966
38847
 
38848
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
38849
+ var REQUEST_TIMEOUT_MS = 2e3;
38850
+ function withTimeout(promise2, ms) {
38851
+ let timer;
38852
+ const timeout = new Promise((_, reject) => {
38853
+ timer = setTimeout(() => {
38854
+ reject(new Error("attached gateway request timed out"));
38855
+ }, ms);
38856
+ });
38857
+ promise2.catch(() => void 0);
38858
+ return Promise.race([promise2, timeout]).finally(() => {
38859
+ clearTimeout(timer);
38860
+ });
38861
+ }
38862
+
37967
38863
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
37968
- import { readFileSync as readFileSync13 } from "fs";
37969
- import { join as join22 } from "path";
38864
+ import { readFileSync as readFileSync14 } from "fs";
38865
+ import { join as join23 } from "path";
37970
38866
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
37971
38867
  function forwardDropsPath(dataDir2) {
37972
- return join22(dataDir2, FORWARD_DROPS_FILENAME);
38868
+ return join23(dataDir2, FORWARD_DROPS_FILENAME);
37973
38869
  }
37974
38870
  function recordForwardDrops(dataDir2, count, nowMs) {
37975
38871
  if (count <= 0) return;
@@ -37987,7 +38883,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
37987
38883
  }
37988
38884
  function readForwardDrops(dataDir2) {
37989
38885
  try {
37990
- const parsed2 = JSON.parse(readFileSync13(forwardDropsPath(dataDir2), "utf8"));
38886
+ const parsed2 = JSON.parse(readFileSync14(forwardDropsPath(dataDir2), "utf8"));
37991
38887
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
37992
38888
  const record2 = parsed2;
37993
38889
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -38005,29 +38901,19 @@ function readForwardDrops(dataDir2) {
38005
38901
 
38006
38902
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
38007
38903
  import { randomUUID as randomUUID15 } from "crypto";
38008
- import { readFileSync as readFileSync14 } from "fs";
38904
+ import { readFileSync as readFileSync15 } from "fs";
38009
38905
  import { readFile, rename, writeFile } from "fs/promises";
38010
- import { join as join23 } from "path";
38011
-
38012
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
38013
- var REQUEST_TIMEOUT_MS = 2e3;
38014
- function withTimeout(promise2, ms) {
38015
- let timer;
38016
- const timeout = new Promise((_, reject) => {
38017
- timer = setTimeout(() => {
38018
- reject(new Error("attached gateway request timed out"));
38019
- }, ms);
38020
- });
38021
- promise2.catch(() => void 0);
38022
- return Promise.race([promise2, timeout]).finally(() => {
38023
- clearTimeout(timer);
38024
- });
38025
- }
38026
-
38027
- // ../../packages/plugin-runtime/src/attached/forward-policy.ts
38906
+ import { join as join24 } from "path";
38028
38907
  function isInvalidRequest(err) {
38029
38908
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
38030
38909
  }
38910
+ function isRouteAbsent(err) {
38911
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
38912
+ }
38913
+ function isServerRejection(err) {
38914
+ const status = statusOf(err);
38915
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
38916
+ }
38031
38917
  var FORWARD_BUDGET_MS = 1500;
38032
38918
  var DECISION_PATH_BUDGET_MS = 800;
38033
38919
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -38055,7 +38941,7 @@ function parseBreakerState(raw, nowMs) {
38055
38941
  }
38056
38942
  function createForwardPolicy(deps) {
38057
38943
  const now = deps.now ?? (() => Date.now());
38058
- const file2 = join23(deps.dir, STATE_FILENAME);
38944
+ const file2 = join24(deps.dir, STATE_FILENAME);
38059
38945
  let state = null;
38060
38946
  let loading = null;
38061
38947
  async function readState() {
@@ -38095,6 +38981,20 @@ function createForwardPolicy(deps) {
38095
38981
  } catch {
38096
38982
  current = { ...CLOSED };
38097
38983
  }
38984
+ const restoreOpenedAtMs = (openedAtMs) => persist({
38985
+ consecutiveFailures: current.consecutiveFailures,
38986
+ openedAtMs,
38987
+ lastFailure: current.lastFailure
38988
+ });
38989
+ const recordFailure = (cause) => {
38990
+ const failures = current.consecutiveFailures + 1;
38991
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
38992
+ return persist({
38993
+ consecutiveFailures: failures,
38994
+ openedAtMs: shouldOpen ? now() : null,
38995
+ lastFailure: cause
38996
+ });
38997
+ };
38098
38998
  const at = now();
38099
38999
  if (current.openedAtMs !== null) {
38100
39000
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -38113,15 +39013,20 @@ function createForwardPolicy(deps) {
38113
39013
  }
38114
39014
  return { ok: true, value };
38115
39015
  } catch (err) {
38116
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
39016
+ if (isInvalidRequest(err)) {
39017
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
39018
+ return { ok: false, reason: "invalid-request" };
39019
+ }
39020
+ if (isRouteAbsent(err)) {
39021
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
39022
+ return { ok: false, reason: "route-absent" };
39023
+ }
39024
+ if (isServerRejection(err)) {
39025
+ await recordFailure("unreachable");
39026
+ return { ok: false, reason: "rejected" };
39027
+ }
38117
39028
  const reason = classifyFailure(err);
38118
- const failures = current.consecutiveFailures + 1;
38119
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
38120
- await persist({
38121
- consecutiveFailures: failures,
38122
- openedAtMs: shouldOpen ? now() : null,
38123
- lastFailure: reason
38124
- });
39029
+ await recordFailure(reason);
38125
39030
  return { ok: false, reason };
38126
39031
  }
38127
39032
  }
@@ -38129,13 +39034,11 @@ function createForwardPolicy(deps) {
38129
39034
  }
38130
39035
 
38131
39036
  // ../../packages/plugin-runtime/src/attached/gateway.ts
38132
- var ACTION_STRENGTH = {
38133
- allow: 0,
38134
- log: 1,
38135
- warn: 2,
38136
- redact: 3,
38137
- block: 4
38138
- };
39037
+ function strongerOf(a, b) {
39038
+ if (a === null) return b;
39039
+ if (b === null) return a;
39040
+ return strongerAction(a, b);
39041
+ }
38139
39042
  function ruleCategoryMap(wireRules, localRules) {
38140
39043
  const map2 = /* @__PURE__ */ new Map();
38141
39044
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -38145,11 +39048,6 @@ function ruleCategoryMap(wireRules, localRules) {
38145
39048
  }
38146
39049
  return map2;
38147
39050
  }
38148
- function strongerOf(a, b) {
38149
- if (a === null) return b;
38150
- if (b === null) return a;
38151
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
38152
- }
38153
39051
  function policyKey(policy) {
38154
39052
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
38155
39053
  }
@@ -38168,7 +39066,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38168
39066
  const floor = floorFor(policy, categoryByRuleId);
38169
39067
  remoteCategoryAction.set(
38170
39068
  policy.target.category,
38171
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
39069
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
38172
39070
  );
38173
39071
  }
38174
39072
  for (const policy of localPolicies) {
@@ -38185,7 +39083,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38185
39083
  }
38186
39084
  merged.set(
38187
39085
  key,
38188
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
39086
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
38189
39087
  );
38190
39088
  }
38191
39089
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -38205,13 +39103,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38205
39103
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
38206
39104
  }
38207
39105
  const effectiveFloor = strongerOf(floor, localFloor);
38208
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
39106
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
38209
39107
  const existing = merged.get(key);
38210
39108
  if (existing === void 0) {
38211
39109
  merged.set(key, clamped);
38212
39110
  continue;
38213
39111
  }
38214
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
39112
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
38215
39113
  merged.set(key, clamped);
38216
39114
  }
38217
39115
  }
@@ -38244,6 +39142,8 @@ var AttachedDataGateway = class {
38244
39142
  );
38245
39143
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
38246
39144
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
39145
+ } else {
39146
+ this.deps.local.markCaptureOwed(record2.event);
38247
39147
  }
38248
39148
  }
38249
39149
  async ensureInventory(ctx) {
@@ -38280,9 +39180,10 @@ var AttachedDataGateway = class {
38280
39180
  // a retried tool_call, exactly this path — can never stomp a populated row.
38281
39181
  async recordAuditEvent(event) {
38282
39182
  await this.deps.local.recordAuditEvent(event);
38283
- await this.deps.forward.run(
39183
+ const forwarded = await this.deps.forward.run(
38284
39184
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
38285
39185
  );
39186
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
38286
39187
  }
38287
39188
  // Attached `llm_call` is written locally by the inner gateway, then routed to
38288
39189
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -38291,44 +39192,170 @@ var AttachedDataGateway = class {
38291
39192
  // which would write the event to the local store a second time.
38292
39193
  async recordLlmCall(input2) {
38293
39194
  await this.deps.local.recordLlmCall(input2);
38294
- await this.deps.forward.run(
38295
- () => this.deps.client.recordAuditEvent(
38296
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
38297
- )
39195
+ const event = llmAuditEvent(input2);
39196
+ const forwarded = await this.deps.forward.run(
39197
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
38298
39198
  );
39199
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
38299
39200
  }
38300
39201
  /**
38301
- * Forward one batch, item by item, under ONE aggregate deadline.
39202
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
39203
+ *
39204
+ * This used to send one HTTP request per event, which is what made the batch
39205
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
39206
+ * away everything after them. The same rows now cross 50 at a time over
39207
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
39208
+ * used — so the same budget admits ~750. The wire cap is the server's own
39209
+ * constant, sized against server cost, and the client REFUSES a longer array
39210
+ * client-side, so the chunking here is not a convention.
39211
+ *
39212
+ * Still serial, and still for the original reason: firing N requests at once
39213
+ * would trade a latency problem for a burst the plane's per-key rate limiting
39214
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
39215
+ * the fix; more concurrent ones is not.
39216
+ *
39217
+ * When the deadline passes the remainder is dropped rather than sent: the
39218
+ * local write has already succeeded, so every caller has a correct result to
39219
+ * return. What is dropped is COUNTED, everywhere it can happen — this path
39220
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
39221
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
39222
+ * the breaker closed, renders a healthy block, and discards the tail of every
39223
+ * batch indefinitely. The SAME tally also covers a single that fails inside
39224
+ * the per-item retry below — the breaker opening mid-retry is a failure the
39225
+ * breaker's own state DOES capture, but the events still in this chunk once
39226
+ * that happens are neither delivered nor otherwise counted anywhere, which is
39227
+ * the same invisibility with a different cause.
38302
39228
  *
38303
- * Per-item budgets bound each request and nothing bounded their sum see
38304
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
38305
- * rather than sent: the local write has already succeeded, so every caller
38306
- * has a correct result to return, and a drop is the outcome this path is
38307
- * built to accept (G8) where a blown hook timeout is not.
39229
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
39230
+ * single-event ack and at fifty times the blast radius here:
39231
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
39232
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
39233
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
39234
+ * delivered and never re-offer the twenty the plane did not take. So success
39235
+ * is checked against `chunk.length`; anything short of it falls into the same
39236
+ * per-item pass as a refused chunk, which is the only way to recover the
39237
+ * rows that did not land, since the ack carries no per-row verdict to
39238
+ * resend by.
38308
39239
  *
38309
- * Serial rather than concurrent on purpose. Firing N requests at once would
38310
- * trade a latency problem for a burst the plane's own per-key rate limiting
38311
- * would answer with the refusals the breaker then counts.
39240
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
39241
+ * no-op rather than a second cost an assumption this file cannot verify.
39242
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
39243
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
39244
+ * the batch size as the invariant `recordCapture` reads), so whether a
39245
+ * duplicate counts toward THIS route's `accepted` is not expressed
39246
+ * anywhere in this repo. If it follows its sibling's convention and does
39247
+ * NOT, a chunk containing even one already-delivered row — the ordinary
39248
+ * consequence of a lost stamp, which this file already treats as cheap —
39249
+ * answers short forever and enters the per-item pass on every pass it is
39250
+ * offered again. The cost of that is bounded rather than silent: the
39251
+ * pass converges (every row lands and stamps), so it is one wasted round
39252
+ * of singles rather than a stall, and it errs toward an extra resend
39253
+ * rather than toward the lost row the alternative risks.
38312
39254
  *
38313
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
38314
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
38315
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
38316
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
38317
- * plane produces no failures, keeps the breaker closed, renders a healthy
38318
- * block, and discards the tail of every batch indefinitely.
39255
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
39256
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
39257
+ * settles none which is why the whole chunk is stamped together on a FULL
39258
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
39259
+ * treatment, alongside a short accept, and all are re-sent one event at a
39260
+ * time:
39261
+ *
39262
+ * `invalid-request` a chunk the client refused to send at all. One malformed
39263
+ * event would otherwise cost the 49 good ones beside it —
39264
+ * a new way to lose data introduced by the very change
39265
+ * meant to stop losing it.
39266
+ * `route-absent` a deployment that predates the batch route. The
39267
+ * single-event route is the one it serves, and re-sending
39268
+ * here rather than inside the client is what gives each
39269
+ * request its own budget instead of 50 inside one.
39270
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
39271
+ * a 4xx body refusal from schema drift on the other side
39272
+ * of the wire. Settlement is batch-atomic on this reason
39273
+ * exactly as on the others, so leaving it out would cost
39274
+ * the whole chunk for one event the DEPLOYMENT considers
39275
+ * malformed, where the per-item form cost only that one.
39276
+ *
39277
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
39278
+ * chunk, and re-sending it item by item would just spend the budget failing 50
39279
+ * more times — for those, the blast radius stays exactly what it was before
39280
+ * batching.
38319
39281
  */
38320
39282
  async forwardBatch(inputs, toEvent) {
38321
39283
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
38322
- for (let i = 0; i < inputs.length; i += 1) {
38323
- const now = Date.now();
38324
- if (now >= deadline) {
38325
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
38326
- return;
39284
+ const delivered = [];
39285
+ try {
39286
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
39287
+ const now = Date.now();
39288
+ if (now >= deadline) {
39289
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
39290
+ return;
39291
+ }
39292
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
39293
+ const forwarded = await this.deps.forward.run(
39294
+ () => this.deps.client.recordAuditEvents(
39295
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
39296
+ )
39297
+ );
39298
+ if (forwarded.ok) {
39299
+ if (forwarded.value.accepted === chunk.length) {
39300
+ delivered.push(...chunk);
39301
+ continue;
39302
+ }
39303
+ } else if (
39304
+ // THREE reasons are worth a second pass, one at a time, and they are
39305
+ // the three settled BEFORE the control plane refused anything, or
39306
+ // (for `rejected`) refused the BODY rather than the connection.
39307
+ //
39308
+ // `invalid-request` — the CLIENT refused the body before any request
39309
+ // went out: a defect in one event, not an outage. Re-sending singly
39310
+ // isolates the bad one instead of charging its 49 neighbours for it.
39311
+ //
39312
+ // `route-absent` — the deployment predates the batch route and serves
39313
+ // only the single-event one. The retry IS the compatibility path, and
39314
+ // it has to live HERE rather than inside the client: each single gets
39315
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
39316
+ // fallback would spend 50 sequential round trips inside the ONE
39317
+ // budget wrapping this call — turning a working older deployment into
39318
+ // a timeout, three of those into an open breaker, and every row into
39319
+ // a silent drop while the status surface called an answering
39320
+ // deployment down.
39321
+ //
39322
+ // `rejected` — the deployment's own 4xx refusal of the body, the
39323
+ // server-side twin of `invalid-request`: isolating it the same way
39324
+ // costs one event instead of the whole chunk for a defect the
39325
+ // deployment considers local to one row.
39326
+ //
39327
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
39328
+ // the whole chunk; re-sending it item by item would just spend the
39329
+ // budget failing 50 more times.
39330
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
39331
+ ) {
39332
+ continue;
39333
+ }
39334
+ for (const [j, event] of chunk.entries()) {
39335
+ const at = Date.now();
39336
+ if (at >= deadline) {
39337
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
39338
+ return;
39339
+ }
39340
+ const single = await this.deps.forward.run(
39341
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
39342
+ );
39343
+ if (single.ok) {
39344
+ delivered.push(event);
39345
+ continue;
39346
+ }
39347
+ if (single.reason === "breaker-open") {
39348
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
39349
+ return;
39350
+ }
39351
+ recordForwardDrops(this.deps.dataDir, 1, at);
39352
+ }
39353
+ }
39354
+ } finally {
39355
+ try {
39356
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
39357
+ } catch {
38327
39358
  }
38328
- const input2 = inputs[i];
38329
- await this.deps.forward.run(
38330
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
38331
- );
38332
39359
  }
38333
39360
  }
38334
39361
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -38371,9 +39398,10 @@ var AttachedDataGateway = class {
38371
39398
  // local store.
38372
39399
  async recordConfigScan(record2) {
38373
39400
  await this.deps.local.recordConfigScan(record2);
38374
- await this.deps.forward.run(
39401
+ const forwarded = await this.deps.forward.run(
38375
39402
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
38376
39403
  );
39404
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
38377
39405
  }
38378
39406
  async recordBlockedDetection(entry) {
38379
39407
  return this.deps.local.recordBlockedDetection(entry);
@@ -38507,6 +39535,18 @@ var AttachedDataGateway = class {
38507
39535
  // exactly what it did, leaving the whole control inert on every device
38508
39536
  // while every test around it stayed green.
38509
39537
  prohibitedModels: cached2.prohibitedModels
39538
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
39539
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
39540
+ // it emits, so an 'authored' policy arriving from the control plane
39541
+ // keeps that marker even where the clamp rebuilds it with a stronger
39542
+ // action. The device reads it in exactly one direction — the rules such a
39543
+ // policy targets are not locally re-assignable — so it sits on the
39544
+ // `prohibitedModels` side of the line for the same reason that field
39545
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
39546
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
39547
+ // would be the silent failure rather than the safe one — the action would
39548
+ // still be enforced while the local override the organization authored
39549
+ // away quietly came back.
38510
39550
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
38511
39551
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
38512
39552
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -38546,10 +39586,10 @@ var AttachedDataGateway = class {
38546
39586
  //
38547
39587
  // Implementing these is what actually closes the skipped-local-maintenance
38548
39588
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
38549
- // any object carrying all five, so the composite qualifies and SessionStart
39589
+ // any object carrying them all, so the composite qualifies and SessionStart
38550
39590
  // runs maintenance on the device's real store.
38551
39591
  //
38552
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
39592
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
38553
39593
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
38554
39594
  // return value directly; declaring them `async` here would hand those call
38555
39595
  // sites a Promise and silently break both.
@@ -38572,9 +39612,15 @@ var AttachedDataGateway = class {
38572
39612
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
38573
39613
  // gives: `recordCapture` calls it after the forward has already settled, on a
38574
39614
  // path that has nothing left to await.
39615
+ markCaptureOwed(event) {
39616
+ this.deps.local.markCaptureOwed(event);
39617
+ }
38575
39618
  markCaptureDelivered(event, atMs) {
38576
39619
  this.deps.local.markCaptureDelivered(event, atMs);
38577
39620
  }
39621
+ markAuditEventsDelivered(events, atMs) {
39622
+ this.deps.local.markAuditEventsDelivered(events, atMs);
39623
+ }
38578
39624
  };
38579
39625
  function reKeyForForward(event, remote) {
38580
39626
  if (remote === null) {
@@ -38617,281 +39663,17 @@ function toolAuditEvent(input2) {
38617
39663
  }
38618
39664
 
38619
39665
  // ../../packages/plugin-runtime/src/attached/history-state.ts
38620
- import { readFileSync as readFileSync15 } from "fs";
38621
- import { join as join24 } from "path";
39666
+ import { readFileSync as readFileSync16 } from "fs";
39667
+ import { join as join25 } from "path";
38622
39668
 
38623
39669
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
38624
39670
  import { createHash as createHash6 } from "crypto";
38625
39671
  import { hostname as hostname5 } from "os";
38626
39672
 
38627
- // ../../packages/remote/src/http.ts
38628
- import { request as httpRequest } from "http";
38629
- import { request as httpsRequest } from "https";
38630
- var DEFAULT_TIMEOUT_MS = 1e4;
38631
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
38632
- var RemoteRequestError = class extends Error {
38633
- constructor(status) {
38634
- super(`control-plane request failed with status ${String(status)}`);
38635
- this.status = status;
38636
- this.name = "RemoteRequestError";
38637
- }
38638
- status;
38639
- };
38640
- var RemoteRequestInvalid = class extends Error {
38641
- constructor(route2, cause) {
38642
- super(`refusing to send a malformed body to ${route2}`);
38643
- this.cause = cause;
38644
- this.name = "RemoteRequestInvalid";
38645
- }
38646
- cause;
38647
- };
38648
- var RemoteResponseInvalid = class extends Error {
38649
- constructor(route2, detail) {
38650
- super(`control plane answered ${route2} with ${detail}`);
38651
- this.name = "RemoteResponseInvalid";
38652
- }
38653
- };
38654
- var RemoteTransportError = class extends Error {
38655
- /**
38656
- * The status the peer sent, when headers arrived and only the BODY was
38657
- * refused.
38658
- *
38659
- * Undefined for the ordinary case this class was written for — no answer at
38660
- * all. It exists because two paths reject after a status has already been
38661
- * delivered: an oversized body and an aborted response. Discarding it there
38662
- * reported a deployment answering 401 with a verbose body as a network
38663
- * outage, which sends the reader to look at their network instead of their
38664
- * credential.
38665
- */
38666
- constructor(reason, status) {
38667
- super(`control-plane request did not complete: ${reason}`);
38668
- this.status = status;
38669
- this.name = "RemoteTransportError";
38670
- }
38671
- status;
38672
- };
38673
- async function send(options) {
38674
- const url2 = new URL(options.url);
38675
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38676
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
38677
- const requestOptions = {
38678
- method: options.method,
38679
- headers: {
38680
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
38681
- // last they win, and two of the values below are ones no caller may
38682
- // replace: `x-api-key` is the credential, and `content-length` is the
38683
- // byte count that stops a multi-byte body being truncated by the
38684
- // receiver. `SendOptions.headers` is a free-form record on an exported
38685
- // function, so "no caller does that today" is not the guarantee to rely
38686
- // on. The one header any caller actually passes — `if-none-match` on the
38687
- // conditional GET — is untouched by this order.
38688
- ...options.headers,
38689
- // The credential. One header, matching what the deployment authenticates
38690
- // on; a second copy in an `Authorization` header would be one more place
38691
- // it can be logged by an intermediary for no gain.
38692
- //
38693
- // Spread conditionally rather than assigned as `undefined`: Node's header
38694
- // handling and `content-length` bookkeeping treat a present-but-undefined
38695
- // key differently from an absent one, and "the header is not there" is
38696
- // the property the attach flow needs.
38697
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
38698
- accept: "application/json",
38699
- ...options.body === void 0 ? {} : {
38700
- "content-type": "application/json",
38701
- // Byte length, not string length: a multi-byte body sent with a
38702
- // character count is truncated by the receiver.
38703
- "content-length": String(Buffer.byteLength(options.body))
38704
- }
38705
- }
38706
- };
38707
- return new Promise((resolve3, reject) => {
38708
- let settled = false;
38709
- const fail2 = (reason, status) => {
38710
- if (settled) return;
38711
- settled = true;
38712
- reject(new RemoteTransportError(reason, status));
38713
- };
38714
- const req = send_(url2, requestOptions, (res) => {
38715
- const chunks = [];
38716
- let size = 0;
38717
- res.on("data", (chunk) => {
38718
- size += chunk.length;
38719
- if (size > MAX_RESPONSE_BYTES) {
38720
- fail2(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
38721
- res.destroy();
38722
- req.destroy();
38723
- return;
38724
- }
38725
- chunks.push(chunk);
38726
- });
38727
- res.on("aborted", () => {
38728
- fail2("the response was aborted", res.statusCode);
38729
- });
38730
- res.on("end", () => {
38731
- if (settled) return;
38732
- settled = true;
38733
- resolve3({
38734
- status: res.statusCode ?? 0,
38735
- headers: res.headers,
38736
- body: Buffer.concat(chunks).toString("utf8")
38737
- });
38738
- });
38739
- });
38740
- const deadline = setTimeout(() => {
38741
- fail2(`no response within ${String(timeoutMs)}ms`);
38742
- req.destroy();
38743
- }, timeoutMs);
38744
- deadline.unref();
38745
- req.on("upgrade", (_res, socket) => {
38746
- fail2("the deployment answered with a protocol upgrade");
38747
- socket.destroy();
38748
- });
38749
- req.on("close", () => {
38750
- fail2("the connection closed before a response was read");
38751
- clearTimeout(deadline);
38752
- });
38753
- req.on("error", (err) => {
38754
- fail2(err.message);
38755
- });
38756
- if (options.body !== void 0) req.write(options.body);
38757
- req.end();
38758
- });
38759
- }
38760
-
38761
- // ../../packages/remote/src/client.ts
38762
- var ROUTES = {
38763
- events: "/v1/events",
38764
- auditEvents: "/v1/audit-events",
38765
- auditEventsBatch: "/v1/audit-events/batch",
38766
- inventory: "/v1/inventory",
38767
- storePosture: "/v1/store-posture",
38768
- policyBundle: "/v1/policy-bundle",
38769
- whoami: "/v1/plugin/whoami",
38770
- shares: "/v1/shares"
38771
- };
38772
- function headerValue(response, name) {
38773
- const raw = response.headers[name];
38774
- if (raw === void 0) return void 0;
38775
- return Array.isArray(raw) ? raw[0] : raw;
38776
- }
38777
- function okBody(response) {
38778
- if (response.status < 200 || response.status >= 300) {
38779
- throw new RemoteRequestError(response.status);
38780
- }
38781
- return response.body;
38782
- }
38783
- function parsed(schema, body, route2) {
38784
- let json2;
38785
- try {
38786
- json2 = JSON.parse(body);
38787
- } catch {
38788
- throw new RemoteResponseInvalid(route2, "a body that is not JSON");
38789
- }
38790
- const result = schema.safeParse(json2);
38791
- if (!result.success) {
38792
- throw new RemoteResponseInvalid(route2, "a body this client cannot read");
38793
- }
38794
- return result.data;
38795
- }
38796
- function withoutTrailingSlashes(endpoint) {
38797
- let end = endpoint.length;
38798
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
38799
- return endpoint.slice(0, end);
38800
- }
38801
- var SLASH = "/".charCodeAt(0);
38802
- function createRemoteClient(options) {
38803
- const base = withoutTrailingSlashes(options.endpoint);
38804
- const url2 = (route2) => `${base}${route2}`;
38805
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
38806
- const sendOne = async (event) => {
38807
- const validated = RecordAuditEventRequest.safeParse(event);
38808
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
38809
- const response = await send({
38810
- ...common,
38811
- method: "POST",
38812
- url: url2(ROUTES.auditEvents),
38813
- body: JSON.stringify(validated.data)
38814
- });
38815
- okBody(response);
38816
- };
38817
- return {
38818
- async ingestEvents(batch) {
38819
- const response = await send({
38820
- ...common,
38821
- method: "POST",
38822
- url: url2(ROUTES.events),
38823
- body: JSON.stringify(batch)
38824
- });
38825
- return parsed(IngestAck, okBody(response), ROUTES.events);
38826
- },
38827
- async ingestInventory(context) {
38828
- const response = await send({
38829
- ...common,
38830
- method: "POST",
38831
- url: url2(ROUTES.inventory),
38832
- body: JSON.stringify(context)
38833
- });
38834
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
38835
- },
38836
- async recordAuditEvent(event) {
38837
- await sendOne(event);
38838
- },
38839
- async recordAuditEvents(events) {
38840
- const validated = RecordAuditEventBatch.safeParse({ events });
38841
- if (!validated.success) {
38842
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
38843
- }
38844
- const response = await send({
38845
- ...common,
38846
- method: "POST",
38847
- url: url2(ROUTES.auditEventsBatch),
38848
- body: JSON.stringify(validated.data)
38849
- });
38850
- if (response.status === 404) {
38851
- for (const event of validated.data.events) await sendOne(event);
38852
- return { accepted: validated.data.events.length };
38853
- }
38854
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
38855
- },
38856
- async reportStorePosture(snapshot) {
38857
- const response = await send({
38858
- ...common,
38859
- method: "POST",
38860
- url: url2(ROUTES.storePosture),
38861
- body: JSON.stringify(snapshot)
38862
- });
38863
- okBody(response);
38864
- },
38865
- async getPolicyBundle(etag) {
38866
- const response = await send({
38867
- ...common,
38868
- method: "GET",
38869
- url: url2(ROUTES.policyBundle),
38870
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
38871
- });
38872
- if (response.status === 304) {
38873
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
38874
- }
38875
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
38876
- return { changed: true, bundle, etag: headerValue(response, "etag") };
38877
- },
38878
- async whoami() {
38879
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
38880
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
38881
- },
38882
- async recordProjectEgress(request) {
38883
- const validated = EgressIngestRequest.safeParse(request);
38884
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
38885
- const response = await send({
38886
- ...common,
38887
- method: "POST",
38888
- url: url2(ROUTES.shares),
38889
- body: JSON.stringify(validated.data)
38890
- });
38891
- okBody(response);
38892
- }
38893
- };
38894
- }
39673
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
39674
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
39675
+ var TRACE_ID = EventMetadata.shape.traceId;
39676
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
38895
39677
 
38896
39678
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
38897
39679
  import { spawn } from "child_process";
@@ -38899,7 +39681,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
38899
39681
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
38900
39682
 
38901
39683
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
38902
- import { readFileSync as readFileSync16 } from "fs";
39684
+ import { readFileSync as readFileSync17 } from "fs";
38903
39685
  function createPluginBlock(build, policyStore) {
38904
39686
  return async () => {
38905
39687
  const cached2 = await policyStore.read();
@@ -38918,7 +39700,7 @@ function createPluginBlock(build, policyStore) {
38918
39700
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38919
39701
  import { randomUUID as randomUUID16 } from "crypto";
38920
39702
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
38921
- import { join as join25 } from "path";
39703
+ import { join as join26 } from "path";
38922
39704
 
38923
39705
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
38924
39706
  import { rename as rename2 } from "fs/promises";
@@ -38942,7 +39724,7 @@ async function publishByRename(tmp, file2, move = rename2) {
38942
39724
 
38943
39725
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38944
39726
  function createPolicyStore(dir = dataDir()) {
38945
- const file2 = join25(dir, "policy-cache.json");
39727
+ const file2 = join26(dir, "policy-cache.json");
38946
39728
  async function read() {
38947
39729
  try {
38948
39730
  const raw = await readFile2(file2, "utf8");
@@ -38951,22 +39733,32 @@ function createPolicyStore(dir = dataDir()) {
38951
39733
  const record2 = parsed2;
38952
39734
  const bundle = PolicyBundle.parse(record2.bundle);
38953
39735
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
38954
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
39736
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
39737
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
39738
+ const etag = replayable ? stored : void 0;
38955
39739
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
38956
39740
  } catch {
38957
39741
  return null;
38958
39742
  }
38959
39743
  }
38960
- async function write(bundle, etag) {
38961
- await ensureDataDir(dir);
38962
- const stored = {
38963
- bundle,
38964
- fetchedAtMs: Date.now(),
38965
- ...etag === void 0 ? {} : { etag }
38966
- };
39744
+ function knowsMoreThanThisBuild(shapeId) {
39745
+ if (typeof shapeId !== "string" || shapeId === "") return false;
39746
+ const theirs = new Set(shapeId.split(","));
39747
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
39748
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
39749
+ }
39750
+ async function priorRecord() {
39751
+ try {
39752
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
39753
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
39754
+ } catch {
39755
+ return null;
39756
+ }
39757
+ }
39758
+ async function publishRecord(record2) {
38967
39759
  const tmp = `${file2}.${randomUUID16()}.tmp`;
38968
39760
  try {
38969
- await writeFile2(tmp, JSON.stringify(stored), {
39761
+ await writeFile2(tmp, JSON.stringify(record2), {
38970
39762
  encoding: "utf8",
38971
39763
  mode: DATA_FILE_MODE,
38972
39764
  flag: "wx"
@@ -38977,6 +39769,27 @@ function createPolicyStore(dir = dataDir()) {
38977
39769
  throw err;
38978
39770
  }
38979
39771
  }
39772
+ async function write(bundle, etag) {
39773
+ await ensureDataDir(dir);
39774
+ const prior = await priorRecord();
39775
+ const priorVersion = prior?.bundle?.version;
39776
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
39777
+ await publishRecord({
39778
+ ...prior,
39779
+ fetchedAtMs: Date.now()
39780
+ });
39781
+ return;
39782
+ }
39783
+ await publishRecord({
39784
+ bundle,
39785
+ fetchedAtMs: Date.now(),
39786
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
39787
+ // bundle it already holds, and the point of the stamp is to describe the
39788
+ // build that last narrowed those bytes, which is this one.
39789
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
39790
+ ...etag === void 0 ? {} : { etag }
39791
+ });
39792
+ }
38980
39793
  return { read, write, file: file2 };
38981
39794
  }
38982
39795
 
@@ -39142,11 +39955,11 @@ function readStorePosture(dbPath2) {
39142
39955
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
39143
39956
  import { randomUUID as randomUUID17 } from "crypto";
39144
39957
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
39145
- import { join as join26 } from "path";
39958
+ import { join as join27 } from "path";
39146
39959
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39147
39960
  function createPostureStore(dir = settingsDir(), legacyDir) {
39148
- const file2 = join26(dir, "posture-state.json");
39149
- const legacyFile = legacyDir === void 0 ? null : join26(legacyDir, "posture-state.json");
39961
+ const file2 = join27(dir, "posture-state.json");
39962
+ const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
39150
39963
  async function persist(state) {
39151
39964
  await ensureDataDir(dir);
39152
39965
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -39214,8 +40027,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
39214
40027
  }
39215
40028
 
39216
40029
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
39217
- import { readFileSync as readFileSync17 } from "fs";
39218
- import { join as join27 } from "path";
40030
+ import { readFileSync as readFileSync18 } from "fs";
40031
+ import { join as join28 } from "path";
39219
40032
 
39220
40033
  // ../../packages/plugin-runtime/src/attached/status.ts
39221
40034
  var REFUSAL_LINES = {
@@ -39533,9 +40346,21 @@ var StandaloneDataGateway = class {
39533
40346
  // for the whole of it, so a member that threw would make that answer a lie
39534
40347
  // the moment a composite delegated to it. A store-level no-op is the honest
39535
40348
  // shape — a standalone machine has nothing delivered to record.
40349
+ markCaptureOwed(event) {
40350
+ this.db.markCaptureOwed(event);
40351
+ }
39536
40352
  markCaptureDelivered(event, atMs) {
39537
40353
  this.db.markCaptureDelivered(event, atMs);
39538
40354
  }
40355
+ // Implemented, not stubbed, for the same reason its sibling above is: the
40356
+ // attached gateway is a DECORATOR over an instance of this class
40357
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
40358
+ // stamp the live forward makes lands here with a non-empty array. This is the
40359
+ // production write path for that feature, not a shape-satisfying no-op — a
40360
+ // machine that is merely standalone simply never calls it.
40361
+ markAuditEventsDelivered(events, atMs) {
40362
+ this.db.markAuditEventsDelivered(events, atMs);
40363
+ }
39539
40364
  staleBinaryNotice(currentVersion) {
39540
40365
  try {
39541
40366
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -39674,17 +40499,26 @@ import { randomUUID as randomUUID19 } from "crypto";
39674
40499
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
39675
40500
 
39676
40501
  // src/history/transcripts.ts
39677
- import { readdirSync as readdirSync6, readFileSync as readFileSync18 } from "fs";
40502
+ import { readdirSync as readdirSync6, readFileSync as readFileSync19 } from "fs";
39678
40503
  import { homedir as homedir3 } from "os";
39679
- import { join as join28 } from "path";
40504
+ import { join as join29 } from "path";
39680
40505
  function transcriptsDir(home) {
39681
- return join28(home ?? homedir3(), ".claude", "projects");
40506
+ return join29(home ?? homedir3(), ".claude", "projects");
39682
40507
  }
39683
40508
  var DAY_MS5 = 24 * 60 * 60 * 1e3;
39684
40509
 
39685
40510
  // src/remediation/redact.ts
39686
- import { readFileSync as readFileSync19, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
39687
- import { isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "path";
40511
+ import {
40512
+ lstatSync as lstatSync4,
40513
+ readdirSync as readdirSync7,
40514
+ readFileSync as readFileSync20,
40515
+ realpathSync as realpathSync4,
40516
+ renameSync as renameSync5,
40517
+ rmSync as rmSync8,
40518
+ statSync as statSync10,
40519
+ writeFileSync as writeFileSync10
40520
+ } from "fs";
40521
+ import { basename as basename7, dirname as dirname7, isAbsolute as isAbsolute2, join as join30, relative, resolve as resolve2 } from "path";
39688
40522
  var REDACTED_PLACEHOLDER = "[REDACTED:SECRET]";
39689
40523
  var REPLACE_PATTERN_SEQUENCE = /\$[$&`'<0-9]/;
39690
40524
  function replacementFor(rawValue, replacements) {
@@ -39715,6 +40549,41 @@ function resolveRedactableArtifact(filePath, scope) {
39715
40549
  if (realTarget === null) return null;
39716
40550
  return scope.artifactRoots.some((root) => isWithinRoot(realTarget, root)) ? realTarget : null;
39717
40551
  }
40552
+ var TMP_SUFFIX = ".aka-redact.tmp";
40553
+ function tempPathFor(realPath) {
40554
+ return `${realPath}.${String(process.pid)}${TMP_SUFFIX}`;
40555
+ }
40556
+ function isProcessAlive(pid) {
40557
+ try {
40558
+ process.kill(pid, 0);
40559
+ return true;
40560
+ } catch (error61) {
40561
+ return error61.code !== "ESRCH";
40562
+ }
40563
+ }
40564
+ function sweepStrandedTemps(realPath) {
40565
+ const dir = dirname7(realPath);
40566
+ const prefix = `${basename7(realPath)}.`;
40567
+ let names;
40568
+ try {
40569
+ names = readdirSync7(dir);
40570
+ } catch {
40571
+ return;
40572
+ }
40573
+ for (const name of names) {
40574
+ if (!name.startsWith(prefix) || !name.endsWith(TMP_SUFFIX)) continue;
40575
+ const pidPart = name.slice(prefix.length, name.length - TMP_SUFFIX.length);
40576
+ if (!/^\d+$/.test(pidPart)) continue;
40577
+ const pid = Number(pidPart);
40578
+ if (pid !== process.pid && isProcessAlive(pid)) continue;
40579
+ try {
40580
+ const stranded = join30(dir, name);
40581
+ if (!lstatSync4(stranded).isFile()) continue;
40582
+ rmSync8(stranded, { force: true });
40583
+ } catch {
40584
+ }
40585
+ }
40586
+ }
39718
40587
  function redactLeakedKeysDetailed(targets, scope = platformRedactionScope(), replacements) {
39719
40588
  const byFile = /* @__PURE__ */ new Map();
39720
40589
  for (const target of targets) {
@@ -39730,11 +40599,14 @@ function redactLeakedKeysDetailed(targets, scope = platformRedactionScope(), rep
39730
40599
  const struck = [];
39731
40600
  for (const [filePath, fileTargets] of byFile) {
39732
40601
  let content;
40602
+ let mode;
39733
40603
  try {
39734
- content = readFileSync19(filePath, "utf8");
40604
+ mode = statSync10(filePath).mode & 511;
40605
+ content = readFileSync20(filePath, "utf8");
39735
40606
  } catch {
39736
40607
  continue;
39737
40608
  }
40609
+ sweepStrandedTemps(filePath);
39738
40610
  const struckHere = [];
39739
40611
  let pointeredHere = 0;
39740
40612
  const applied = /* @__PURE__ */ new Map();
@@ -39759,9 +40631,9 @@ function redactLeakedKeysDetailed(targets, scope = platformRedactionScope(), rep
39759
40631
  if (replacement !== REDACTED_PLACEHOLDER) pointeredHere += 1;
39760
40632
  }
39761
40633
  if (struckHere.length === 0) continue;
39762
- const tmpPath = `${filePath}.aka-redact.tmp`;
40634
+ const tmpPath = tempPathFor(filePath);
39763
40635
  try {
39764
- writeFileSync10(tmpPath, content);
40636
+ writeFileSync10(tmpPath, content, { mode, flag: "wx" });
39765
40637
  renameSync5(tmpPath, filePath);
39766
40638
  } catch {
39767
40639
  try {
@@ -39817,7 +40689,8 @@ async function buildPointerReplacements(recovered, base) {
39817
40689
  const replacement = await glue.tokenizeValue(rawValue, {
39818
40690
  ruleId: entry.ruleId,
39819
40691
  category: entry.category,
39820
- maskedMatch: maskMatch(rawValue)
40692
+ maskedMatch: maskMatch(rawValue),
40693
+ userAuthorized: true
39821
40694
  });
39822
40695
  if (replacement.startsWith("[[aka:") && replacement !== rawValue) {
39823
40696
  replacements.set(rawValue, replacement);
@@ -39854,7 +40727,7 @@ async function redactSurfacedSecrets(findings, overrides = {}) {
39854
40727
  for (const [filePath, fileFindings] of byFile) {
39855
40728
  let content;
39856
40729
  try {
39857
- content = readFileSync20(filePath, "utf8");
40730
+ content = readFileSync21(filePath, "utf8");
39858
40731
  } catch {
39859
40732
  unrecovered.push(...fileFindings);
39860
40733
  continue;
@@ -40019,7 +40892,7 @@ if (process.argv[1] && fileURLToPath5(import.meta.url) === process.argv[1]) {
40019
40892
  try {
40020
40893
  const argv = process.argv.slice(2);
40021
40894
  const optionIndex = argv.indexOf("--option");
40022
- const frameText = readFileSync21(0, "utf8");
40895
+ const frameText = readFileSync22(0, "utf8");
40023
40896
  if (optionIndex === -1) {
40024
40897
  present(frameText);
40025
40898
  } else {