@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,7 +492,7 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // src/hooks/session-start.ts
495
- import { readFileSync as readFileSync20 } from "fs";
495
+ import { readFileSync as readFileSync21 } from "fs";
496
496
 
497
497
  // ../../packages/plugin-runtime/src/attached/egress-wire.ts
498
498
  import { createHash as createHash4 } from "crypto";
@@ -500,6 +500,7 @@ import { createHash as createHash4 } from "crypto";
500
500
  // ../../packages/persistence/src/attached-derived.ts
501
501
  import { rmSync } from "fs";
502
502
  import { join } from "path";
503
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
503
504
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
504
505
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
505
506
 
@@ -600,6 +601,30 @@ var SQLITE_MIGRATIONS = [
600
601
  {
601
602
  tag: "0022_audit_inspection_ms",
602
603
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
604
+ },
605
+ {
606
+ tag: "0023_secret_vault_user_authorized",
607
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
608
+ },
609
+ {
610
+ tag: "0024_finding_resolution_key_created_index",
611
+ 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`);"
612
+ },
613
+ {
614
+ tag: "0025_audit_capture_attribute_columns",
615
+ 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;"
616
+ },
617
+ {
618
+ tag: "0026_audit_llm_call_usage_columns",
619
+ 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;"
620
+ },
621
+ {
622
+ tag: "0027_audit_llm_usage_index",
623
+ 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;"
624
+ },
625
+ {
626
+ tag: "0028_activity_session_probe_indexes",
627
+ 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"
603
628
  }
604
629
  ];
605
630
 
@@ -22256,6 +22281,26 @@ var AttachTokenResponse = external_exports.union([
22256
22281
  AttachTokenExpired,
22257
22282
  external_exports.object({ status: printable(64) })
22258
22283
  ]);
22284
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22285
+ var DeviceCommand = external_exports.object({
22286
+ id: printable(128).min(1),
22287
+ kind: DeviceCommandKind,
22288
+ issuedAt: printable(64).min(1),
22289
+ expiresAt: printable(64).min(1)
22290
+ }).strict();
22291
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22292
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22293
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22294
+ external_exports.object({
22295
+ outcome: external_exports.literal("reported"),
22296
+ projectsScanned: external_exports.number().int().nonnegative()
22297
+ }).strict(),
22298
+ external_exports.object({
22299
+ outcome: external_exports.literal("failed"),
22300
+ reason: DeviceCommandFailureReason,
22301
+ projectsScanned: external_exports.number().int().nonnegative()
22302
+ }).strict()
22303
+ ]);
22259
22304
 
22260
22305
  // ../../packages/schema/src/zod/registry.ts
22261
22306
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22422,7 +22467,7 @@ var PackManifest = external_exports.object({
22422
22467
  }).meta({ id: "PackManifest" });
22423
22468
 
22424
22469
  // ../../packages/schema/src/zod/detection.ts
22425
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22470
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22426
22471
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22427
22472
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22428
22473
  var DetectionCounts = external_exports.object({
@@ -22559,14 +22604,17 @@ function optional2(key, parsed2, raw) {
22559
22604
  function isStringArray(value) {
22560
22605
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22561
22606
  }
22607
+ var ORIGIN_VALUES = { library: true, custom: true };
22608
+ function resolveOrigin(origin) {
22609
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22610
+ }
22562
22611
  function summaryToDetectionListItem(s) {
22563
22612
  return {
22564
22613
  id: `${s.namespace}/${s.packId}`,
22565
22614
  name: s.name,
22566
22615
  version: s.version,
22567
22616
  enabled: s.enabled,
22568
- origin: "library",
22569
- // v1: every installed pack is library origin
22617
+ origin: resolveOrigin(s.origin),
22570
22618
  namespace: s.namespace,
22571
22619
  packId: s.packId,
22572
22620
  ruleCount: s.ruleCount,
@@ -22618,7 +22666,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22618
22666
  name: row.name,
22619
22667
  version: row.version,
22620
22668
  enabled: row.enabled,
22621
- origin: "library",
22669
+ origin: resolveOrigin(row.origin),
22622
22670
  namespace: row.namespace,
22623
22671
  packId: row.packId,
22624
22672
  ruleCount: row.rules.length,
@@ -22638,16 +22686,20 @@ function splitDetectionId(id) {
22638
22686
  }
22639
22687
  function buildDetectionsList(summaries, query) {
22640
22688
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22689
+ const originOf = (s) => resolveOrigin(s.origin);
22641
22690
  const counts = {
22642
22691
  all: summaries.length,
22643
- library: summaries.length,
22644
- // all origin=library in v1
22645
- custom: 0,
22692
+ library: summaries.filter((s) => originOf(s) === "library").length,
22693
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22694
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22695
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22696
+ // place, and that state does not exist — editing a library pack forks it. See
22697
+ // OriginEnum.
22646
22698
  customized: 0,
22647
22699
  updates: withUpdate.length
22648
22700
  };
22649
22701
  const filter = query.filter;
22650
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22702
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22651
22703
  if (query.q) {
22652
22704
  const q = query.q.toLowerCase();
22653
22705
  filtered = filtered.filter(
@@ -22727,8 +22779,9 @@ var Event = external_exports.object({
22727
22779
  metadata: EventMetadata.optional()
22728
22780
  }).meta({ id: "Event" });
22729
22781
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22782
+ var INGEST_BATCH_MAX = 100;
22730
22783
  var IngestBatch = external_exports.object({
22731
- events: external_exports.array(IngestEvent).min(1).max(100),
22784
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22732
22785
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22733
22786
  // additionally rejects any event whose contentHash the store has already
22734
22787
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23284,383 +23337,11 @@ var PatchInstalledPackRequest = external_exports.object({
23284
23337
  message: "At least one field must be provided"
23285
23338
  }).meta({ id: "PatchInstalledPackRequest" });
23286
23339
 
23287
- // ../../packages/schema/src/zod/vault.ts
23288
- var POINTER_FORMAT_VERSION = 2;
23289
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23290
- var POINTER_TOKEN_PATTERN = new RegExp(
23291
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23292
- );
23293
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23294
- function pointerTokenScanner() {
23295
- return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23296
- }
23297
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23298
- var ParsedPointer = external_exports.object({
23299
- category: DetectionCategory,
23300
- keyVersion: external_exports.number().int().positive(),
23301
- pointerId: external_exports.string(),
23302
- tag: external_exports.string()
23303
- });
23304
- var VaultEntry = external_exports.object({
23305
- pointerId: external_exports.string(),
23306
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23307
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23308
- // independently of the vault encryption key below.
23309
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23310
- fingerprintKeyVersion: external_exports.number().int().positive(),
23311
- // The vault-key epoch this row's ciphertext was sealed under.
23312
- keyVersion: external_exports.number().int().positive(),
23313
- // Fixed at first mint and never updated: the same value detected later under a
23314
- // different rule's category keeps the category it was minted with, so one
23315
- // value always produces exactly one wire token.
23316
- category: DetectionCategory,
23317
- ruleId: external_exports.string(),
23318
- // Partial-reveal preview for badges and listings. Never the raw value.
23319
- maskedMatch: external_exports.string(),
23320
- provider: external_exports.string().optional(),
23321
- ciphertext: external_exports.string(),
23322
- nonce: external_exports.string(),
23323
- authTag: external_exports.string(),
23324
- // How many times this value has been detected on this machine — the reuse
23325
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23326
- occurrenceCount: external_exports.number().int().nonnegative(),
23327
- firstSeen: external_exports.string(),
23328
- lastSeen: external_exports.string()
23329
- });
23330
- var PointerDescriptor = external_exports.object({
23331
- category: DetectionCategory,
23332
- provider: external_exports.string().optional(),
23333
- maskedMatch: external_exports.string(),
23334
- occurrences: external_exports.number().int().nonnegative(),
23335
- firstSeen: external_exports.string(),
23336
- lastSeen: external_exports.string()
23337
- });
23338
- var PointerIdentity = external_exports.object({
23339
- ruleId: external_exports.string(),
23340
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23341
- fingerprintKeyVersion: external_exports.number().int().positive()
23342
- });
23343
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23344
- var VaultDerefReason = external_exports.enum([
23345
- "display",
23346
- "explicit-reveal",
23347
- "view-render",
23348
- "model-input",
23349
- "remediation",
23350
- "purge"
23351
- ]);
23352
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23353
- var VaultDeref = external_exports.object({
23354
- id: external_exports.guid(),
23355
- pointerId: external_exports.string(),
23356
- at: external_exports.string(),
23357
- target: DetokenizeTarget,
23358
- reason: VaultDerefReason,
23359
- outcome: VaultDerefOutcome,
23360
- // Present only on a model-target crossing that a reveal grant authorized.
23361
- grantId: external_exports.string().optional(),
23362
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23363
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23364
- pointerCount: external_exports.number().int().positive().default(1)
23365
- });
23366
- var VaultSightingKind = external_exports.enum([
23367
- "prompt",
23368
- "tool-input",
23369
- "tool-output",
23370
- "file",
23371
- "transcript"
23372
- ]);
23373
- var VaultSighting = external_exports.object({
23374
- location: external_exports.string(),
23375
- kind: VaultSightingKind,
23376
- firstSeen: external_exports.string(),
23377
- lastSeen: external_exports.string()
23378
- });
23379
- var VaultInventoryEntry = external_exports.object({
23380
- pointerId: external_exports.string(),
23381
- category: DetectionCategory,
23382
- provider: external_exports.string().optional(),
23383
- maskedMatch: external_exports.string(),
23384
- occurrences: external_exports.number().int().nonnegative(),
23385
- firstSeen: external_exports.string(),
23386
- lastSeen: external_exports.string(),
23387
- // The active reveal-to-model grant covering this value, when one exists —
23388
- // the inventory badges it, the row links to revocation.
23389
- revealGrantId: external_exports.string().nullable(),
23390
- sightings: external_exports.array(VaultSighting)
23391
- });
23392
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23393
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23394
- var MAX_VAULT_PAGE_LIMIT = 200;
23395
- var ListVaultInventoryQuery = external_exports.object({
23396
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23397
- // Opaque; names the last row of the page just served.
23398
- cursor: external_exports.string().optional()
23399
- });
23400
- var ListVaultInventoryResponse = external_exports.object({
23401
- // Vaulted values across the whole store, not just this page — cursor-
23402
- // independent, so paging never changes what the count claims.
23403
- totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23404
- items: external_exports.array(VaultInventoryEntry),
23405
- // `null` once the last page is reached.
23406
- nextCursor: external_exports.string().nullable()
23407
- });
23408
- var ListVaultReuseQuery = external_exports.object({
23409
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23410
- cursor: external_exports.string().optional()
23411
- });
23412
- var ListVaultReuseResponse = external_exports.object({
23413
- // Reused values across the whole store — the number the section's claim
23414
- // ("values detected in more than one place") is about.
23415
- totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23416
- items: external_exports.array(VaultInventoryEntry),
23417
- nextCursor: external_exports.string().nullable()
23418
- });
23419
- var ListVaultDerefsQuery = external_exports.object({
23420
- // Include the batched, high-volume reasons (display, view-render). Omitted
23421
- // hides them and counts them into `hiddenBatched` instead, so the model
23422
- // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23423
- // over a Server Action, which preserves the type, never as a URL param.
23424
- includeBatched: external_exports.boolean().optional(),
23425
- limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23426
- cursor: external_exports.string().optional()
23427
- });
23428
- var ListVaultDerefsResponse = external_exports.object({
23429
- items: external_exports.array(VaultDeref),
23430
- nextCursor: external_exports.string().nullable(),
23431
- // Display/view-render rows the query hid, over the WHOLE trail rather than
23432
- // this page — it is the count the "N hidden" line and its toggle speak for.
23433
- // Always 0 when `includeBatched` was set, since nothing was hidden.
23434
- hiddenBatched: external_exports.number().int().nonnegative()
23435
- });
23436
- var VaultKeyCustody = external_exports.string();
23437
- var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23438
- var VAULT_CONSENT_VERSION = 1;
23439
- var VaultConsent = external_exports.object({
23440
- acknowledgedAt: external_exports.iso.datetime(),
23441
- version: external_exports.number().int().positive()
23442
- });
23443
- function isVaultConsentValid(consent) {
23444
- return consent?.version === VAULT_CONSENT_VERSION;
23445
- }
23446
-
23447
- // ../../packages/schema/src/zod/local.ts
23448
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23449
- var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23450
- var HISTORY_SYNC_PAYLOAD_VERSION = 1;
23451
- var RunMode = external_exports.enum(["standalone", "attached"]);
23452
- var ControlPlaneConnection = external_exports.object({
23453
- endpoint: external_exports.string().min(1),
23454
- // Display name for the deployment, shown instead of the raw endpoint.
23455
- label: external_exports.string().min(1).optional(),
23456
- attachedAt: external_exports.iso.datetime()
23457
- }).meta({ id: "ControlPlaneConnection" });
23458
- var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23459
- var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23460
- var ModelJudgeConsent = external_exports.object({
23461
- acknowledgedAt: external_exports.iso.datetime(),
23462
- payloadVersion: external_exports.number().int().positive()
23463
- });
23464
- var HistorySyncConsent = external_exports.object({
23465
- acknowledgedAt: external_exports.iso.datetime(),
23466
- payloadVersion: external_exports.number().int().positive(),
23467
- endpoint: external_exports.string()
23468
- });
23469
- function isHistorySyncConsentValid(consent, endpoint) {
23470
- if (consent === void 0 || endpoint === void 0) return false;
23471
- return consent.payloadVersion === HISTORY_SYNC_PAYLOAD_VERSION && consent.endpoint === endpoint;
23472
- }
23473
- var WorkspaceSettings = external_exports.object({
23474
- specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23475
- runMode: RunMode.default("standalone"),
23476
- // Present only while attached; a detach clears it. Its presence is what makes
23477
- // `runMode: 'attached'` mean anything — see isAttached.
23478
- controlPlane: ControlPlaneConnection.optional(),
23479
- policy: SimpleDetectionPolicy.default("redact"),
23480
- // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23481
- historicalAccess: HistoricalAccess.default("session-only"),
23482
- // In-place egress extraction on the scan paths; disable to stop all Data
23483
- // Shares writes.
23484
- dataSharesInPlace: external_exports.boolean().default(true),
23485
- // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23486
- // vault, instead of destroying them. Absent by default: this is a custody
23487
- // change from one-way redaction, so it is never an assumed grant on upgrade.
23488
- // Revoking stops future vaulting; it does not erase what is already stored —
23489
- // purging the vault is the eraser.
23490
- vaultConsent: VaultConsent.optional(),
23491
- // Where the vault master key lives.
23492
- vaultKeyCustody: VaultKeyCustody.default("file"),
23493
- // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23494
- vaultInlineReveal: VaultInlineReveal.default("masked"),
23495
- // Absent until /aka:setup completes; its presence is what "onboarded" means.
23496
- onboardedAt: external_exports.iso.datetime().optional(),
23497
- // Records that the user consented to sending findings to the model API for
23498
- // the /aka:setup judge, along with the payload-shape version they agreed to.
23499
- // Absent until granted; a stale payloadVersion means the consent no longer
23500
- // covers the current payload and must be re-granted.
23501
- modelJudgeConsent: ModelJudgeConsent.optional(),
23502
- // Records that the user consented to sending the activity already recorded on
23503
- // this machine to the deployment it is attached to, along with the payload
23504
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23505
- // a different endpoint or an older payload no longer counts.
23506
- historySyncConsent: HistorySyncConsent.optional()
23507
- });
23508
- function defaultWorkspaceSettings() {
23509
- return WorkspaceSettings.parse({});
23510
- }
23511
- function isAttached(settings) {
23512
- return settings.runMode === "attached" && settings.controlPlane !== void 0;
23513
- }
23514
- function toInventoryRow(input2, id, now) {
23515
- return {
23516
- id,
23517
- objectType: input2.objectType,
23518
- location: input2.location ?? null,
23519
- title: input2.title ?? null,
23520
- hostId: input2.hostId ?? null,
23521
- attributes: JSON.stringify(input2.attributes),
23522
- firstSeen: now,
23523
- lastSeen: now
23524
- };
23525
- }
23526
- function toSourceProjectRow(input2, id, now) {
23527
- return {
23528
- id,
23529
- url: input2.url,
23530
- name: input2.name ?? null,
23531
- attributes: JSON.stringify(input2.attributes),
23532
- firstSeen: now,
23533
- lastSeen: now
23534
- };
23535
- }
23536
- function toAuditEventRow(input2) {
23537
- return {
23538
- id: input2.id,
23539
- parentId: input2.parentId ?? null,
23540
- rootSessionId: input2.rootSessionId ?? null,
23541
- eventType: input2.eventType,
23542
- hostId: input2.hostId ?? null,
23543
- harnessId: input2.harnessId ?? null,
23544
- sourceProjectId: input2.sourceProjectId ?? null,
23545
- startedAt: isoToEpochMillis(input2.startedAt),
23546
- endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23547
- severity: input2.severity ?? null,
23548
- priority: input2.priority ?? null,
23549
- content: input2.content ?? null,
23550
- contentHash: input2.contentHash ?? null,
23551
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23552
- };
23553
- }
23554
- function toClassifiedDataRow(input2, id) {
23555
- return {
23556
- id,
23557
- class: input2.class,
23558
- label: input2.label ?? null,
23559
- attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23560
- };
23561
- }
23562
- function toInspectionDefinitionRow(input2, id) {
23563
- return {
23564
- id,
23565
- ruleId: input2.ruleId,
23566
- name: input2.name,
23567
- category: input2.category,
23568
- severity: input2.severity,
23569
- definition: input2.definition,
23570
- version: input2.version
23571
- };
23572
- }
23573
- function toInspectionFindingRow(input2) {
23574
- return {
23575
- id: input2.id,
23576
- auditEventId: input2.auditEventId,
23577
- inspectionDefinitionId: input2.inspectionDefinitionId,
23578
- classifiedDataId: input2.classifiedDataId ?? null,
23579
- spanStart: input2.span.start,
23580
- spanEnd: input2.span.end,
23581
- maskedMatch: input2.maskedMatch,
23582
- actionTaken: input2.actionTaken,
23583
- confidence: input2.confidence,
23584
- findingKey: input2.findingKey ?? null,
23585
- firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23586
- };
23587
- }
23588
- function toCaptureAttributes(event) {
23589
- const metadata = event.metadata;
23590
- return {
23591
- source_tool: event.sourceTool,
23592
- ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23593
- ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23594
- ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23595
- ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23596
- ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23597
- ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23598
- ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23599
- ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23600
- ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23601
- // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23602
- // has ever populated either), but every legacy metadata key still rides
23603
- // the bag rather than being silently dropped — CaptureAttributes'
23604
- // `.catchall(z.unknown())` carries the long tail.
23605
- ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23606
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23607
- };
23608
- }
23609
- function captureDefinitionVersion(finding2) {
23610
- return `capture/${finding2.category}/${finding2.severity}`;
23611
- }
23612
- function toCaptureDefinitionInput(finding2) {
23613
- return {
23614
- ruleId: finding2.ruleId,
23615
- version: captureDefinitionVersion(finding2),
23616
- name: finding2.ruleId,
23617
- category: finding2.category,
23618
- severity: finding2.severity,
23619
- definition: JSON.stringify({ ruleId: finding2.ruleId })
23620
- };
23621
- }
23622
-
23623
- // ../../packages/schema/src/zod/managed.ts
23624
- var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23625
- var MANAGED_SETTINGS_SPEC_VERSION = 1;
23626
- var ManagedSettingKey = external_exports.enum([
23627
- "runMode",
23628
- "historicalAccess",
23629
- "vaultConsent",
23630
- "vaultKeyCustody",
23631
- "vaultInlineReveal",
23632
- "modelJudgeConsent",
23633
- "dataSharesInPlace"
23634
- ]).meta({ id: "ManagedSettingKey" });
23635
- var ManagedSettingsValues = external_exports.object({
23636
- runMode: external_exports.enum(["standalone", "attached"]).optional(),
23637
- controlPlane: external_exports.object({
23638
- endpoint: external_exports.string().min(1),
23639
- label: external_exports.string().min(1).optional()
23640
- }).optional(),
23641
- historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23642
- vaultConsent: external_exports.boolean().optional(),
23643
- vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23644
- vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23645
- modelJudgeConsent: external_exports.boolean().optional(),
23646
- dataSharesInPlace: external_exports.boolean().optional()
23647
- }).meta({ id: "ManagedSettingsValues" });
23648
- var ManagedSettings = external_exports.object({
23649
- specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23650
- // Shown on every locked control, so the user can tell an administrative
23651
- // decision from a bug. Absent renders as a generic "your organization".
23652
- organization: external_exports.string().min(1).optional(),
23653
- // What the administrator pinned.
23654
- values: ManagedSettingsValues.default({}),
23655
- // Which of those the user may not change. A key here with no matching value
23656
- // freezes whatever the user last chose; a value with no lock is a DEFAULT
23657
- // the user may still override. The two are separable on purpose.
23658
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23659
- }).meta({ id: "ManagedSettings" });
23660
-
23661
23340
  // ../../packages/schema/src/zod/policy.ts
23662
23341
  var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23663
23342
  var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23343
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23344
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23664
23345
  var Policy = external_exports.object({
23665
23346
  id: external_exports.guid(),
23666
23347
  scope: PolicyScope,
@@ -23670,7 +23351,27 @@ var Policy = external_exports.object({
23670
23351
  customKeywords: external_exports.array(external_exports.string()).optional(),
23671
23352
  // Display name — optional so older policy rows without name still parse.
23672
23353
  // Added for the findings API (policy.name column migration).
23673
- name: external_exports.string().optional()
23354
+ name: external_exports.string().optional(),
23355
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23356
+ // which row this is. A producer that collapses several rows onto one target
23357
+ // must carry the marker onto whichever row survives, or the collapse decides
23358
+ // the answer; a survivor may therefore be a built-in expansion still marked
23359
+ // 'authored' because an authored sibling targeted the same thing.
23360
+ // Optional so an older producer — and an older on-disk cache — still parses;
23361
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23362
+ //
23363
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23364
+ // built-in archetype catalog entry a policy is, which every catalog surface
23365
+ // reads and which a caller may state. This one is a statement the PRODUCER
23366
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23367
+ // — the CRUD routes neither accept nor set it.
23368
+ //
23369
+ // A device consumes this in exactly one direction: an 'authored' policy
23370
+ // arriving from a control plane marks the rules it targets as not
23371
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23372
+ // which is what makes it safe to honour from an unsigned cache — the same
23373
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23374
+ provenance: PolicyProvenance.optional()
23674
23375
  }).meta({ id: "Policy" });
23675
23376
  var PolicyBundle = external_exports.object({
23676
23377
  version: external_exports.string(),
@@ -23722,6 +23423,12 @@ var PolicyBundle = external_exports.object({
23722
23423
  customKeywords: external_exports.array(external_exports.string()),
23723
23424
  fetchedAt: external_exports.iso.datetime()
23724
23425
  }).meta({ id: "PolicyBundle" });
23426
+ var POLICY_BUNDLE_SHAPE_ID = [
23427
+ ...Object.keys(PolicyBundle.shape),
23428
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23429
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23430
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23431
+ ].sort().join(",");
23725
23432
  var OBSERVE_ONLY_CATEGORIES = ["config"];
23726
23433
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23727
23434
  var CATEGORY_PEAK_SEVERITY = {
@@ -23742,9 +23449,11 @@ function severityFloorPolicy(category) {
23742
23449
  const peak = CATEGORY_PEAK_SEVERITY[category];
23743
23450
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23744
23451
  }
23745
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23746
23452
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23747
23453
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23454
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23455
+ id: "RedactFallback"
23456
+ });
23748
23457
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23749
23458
  var BUILTIN_POLICY_SPECS = {
23750
23459
  monitor: {
@@ -23781,6 +23490,42 @@ var BUILTIN_POLICY_SPECS = {
23781
23490
  function builtinPolicyToAction(id) {
23782
23491
  return BUILTIN_POLICY_SPECS[id].action;
23783
23492
  }
23493
+ var PALETTE_WEAKEST_FIRST = [
23494
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23495
+ ];
23496
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23497
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23498
+ );
23499
+ var ACTION_STRENGTH_ORDER = [
23500
+ ...BELOW_PALETTE,
23501
+ ...PALETTE_WEAKEST_FIRST
23502
+ ];
23503
+ function actionRank(action) {
23504
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23505
+ }
23506
+ function isActionAtLeast(action, floor) {
23507
+ return actionRank(action) >= actionRank(floor);
23508
+ }
23509
+ function strongerAction(a, b) {
23510
+ return actionRank(a) >= actionRank(b) ? a : b;
23511
+ }
23512
+ function weakestBuiltinAtLeast(floor) {
23513
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23514
+ }
23515
+ var PackPolicyFloor = external_exports.object({
23516
+ /**
23517
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23518
+ * rather than a raw ActionTaken because that is the vocabulary the user
23519
+ * picks from — a floor a UI cannot name is one it cannot explain.
23520
+ */
23521
+ floor: BuiltinPolicyId,
23522
+ /**
23523
+ * True when the organization AUTHORED a policy governing this pack rather
23524
+ * than stating a minimum: it gave the answer, so the pack is not
23525
+ * re-assignable locally in either direction.
23526
+ */
23527
+ locked: external_exports.boolean()
23528
+ }).describe("PackPolicyFloor");
23784
23529
  var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23785
23530
  (id) => !BUILTIN_POLICY_SPECS[id].reversible
23786
23531
  );
@@ -23838,6 +23583,405 @@ var PolicyStatsResponse = external_exports.object({
23838
23583
  detectionsGoverned: external_exports.number().int().nonnegative()
23839
23584
  }).meta({ id: "PolicyStatsResponse" });
23840
23585
 
23586
+ // ../../packages/schema/src/zod/vault.ts
23587
+ var POINTER_FORMAT_VERSION = 2;
23588
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23589
+ var POINTER_TOKEN_PATTERN = new RegExp(
23590
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23591
+ );
23592
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23593
+ function pointerTokenScanner() {
23594
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
23595
+ }
23596
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23597
+ var ParsedPointer = external_exports.object({
23598
+ category: DetectionCategory,
23599
+ keyVersion: external_exports.number().int().positive(),
23600
+ pointerId: external_exports.string(),
23601
+ tag: external_exports.string()
23602
+ });
23603
+ var VaultEntry = external_exports.object({
23604
+ pointerId: external_exports.string(),
23605
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23606
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23607
+ // independently of the vault encryption key below.
23608
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23609
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23610
+ // The vault-key epoch this row's ciphertext was sealed under.
23611
+ keyVersion: external_exports.number().int().positive(),
23612
+ // Fixed at first mint and never updated: the same value detected later under a
23613
+ // different rule's category keeps the category it was minted with, so one
23614
+ // value always produces exactly one wire token.
23615
+ category: DetectionCategory,
23616
+ ruleId: external_exports.string(),
23617
+ // Partial-reveal preview for badges and listings. Never the raw value.
23618
+ maskedMatch: external_exports.string(),
23619
+ provider: external_exports.string().optional(),
23620
+ ciphertext: external_exports.string(),
23621
+ nonce: external_exports.string(),
23622
+ authTag: external_exports.string(),
23623
+ // How many times this value has been detected on this machine — the reuse
23624
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23625
+ occurrenceCount: external_exports.number().int().nonnegative(),
23626
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23627
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23628
+ // one row however many paths vault it, so this is what tells a policy sweep
23629
+ // that the row carries somebody's own instruction and not just an assignment
23630
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23631
+ // vaulting of the same value must never clear it — what the user said about
23632
+ // the value does not expire.
23633
+ userAuthorized: external_exports.boolean(),
23634
+ firstSeen: external_exports.string(),
23635
+ lastSeen: external_exports.string()
23636
+ });
23637
+ var PointerDescriptor = external_exports.object({
23638
+ category: DetectionCategory,
23639
+ provider: external_exports.string().optional(),
23640
+ maskedMatch: external_exports.string(),
23641
+ occurrences: external_exports.number().int().nonnegative(),
23642
+ firstSeen: external_exports.string(),
23643
+ lastSeen: external_exports.string()
23644
+ });
23645
+ var PointerIdentity = external_exports.object({
23646
+ ruleId: external_exports.string(),
23647
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23648
+ fingerprintKeyVersion: external_exports.number().int().positive()
23649
+ });
23650
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23651
+ var VaultDerefReason = external_exports.enum([
23652
+ "display",
23653
+ "explicit-reveal",
23654
+ "view-render",
23655
+ "model-input",
23656
+ "remediation",
23657
+ "purge"
23658
+ ]);
23659
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23660
+ var VaultDeref = external_exports.object({
23661
+ id: external_exports.guid(),
23662
+ pointerId: external_exports.string(),
23663
+ at: external_exports.string(),
23664
+ target: DetokenizeTarget,
23665
+ reason: VaultDerefReason,
23666
+ outcome: VaultDerefOutcome,
23667
+ // Present only on a model-target crossing that a reveal grant authorized.
23668
+ grantId: external_exports.string().optional(),
23669
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23670
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23671
+ pointerCount: external_exports.number().int().positive().default(1)
23672
+ });
23673
+ var VaultSightingKind = external_exports.enum([
23674
+ "prompt",
23675
+ "tool-input",
23676
+ "tool-output",
23677
+ "file",
23678
+ "transcript"
23679
+ ]);
23680
+ var VaultSighting = external_exports.object({
23681
+ location: external_exports.string(),
23682
+ kind: VaultSightingKind,
23683
+ firstSeen: external_exports.string(),
23684
+ lastSeen: external_exports.string()
23685
+ });
23686
+ var VaultInventoryEntry = external_exports.object({
23687
+ pointerId: external_exports.string(),
23688
+ category: DetectionCategory,
23689
+ provider: external_exports.string().optional(),
23690
+ maskedMatch: external_exports.string(),
23691
+ occurrences: external_exports.number().int().nonnegative(),
23692
+ firstSeen: external_exports.string(),
23693
+ lastSeen: external_exports.string(),
23694
+ // The active reveal-to-model grant covering this value, when one exists —
23695
+ // the inventory badges it, the row links to revocation.
23696
+ revealGrantId: external_exports.string().nullable(),
23697
+ sightings: external_exports.array(VaultSighting)
23698
+ });
23699
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23700
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23701
+ var MAX_VAULT_PAGE_LIMIT = 200;
23702
+ var ListVaultInventoryQuery = external_exports.object({
23703
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23704
+ // Opaque; names the last row of the page just served.
23705
+ cursor: external_exports.string().optional()
23706
+ });
23707
+ var ListVaultInventoryResponse = external_exports.object({
23708
+ // Vaulted values across the whole store, not just this page — cursor-
23709
+ // independent, so paging never changes what the count claims.
23710
+ totals: external_exports.object({ values: external_exports.number().int().nonnegative() }),
23711
+ items: external_exports.array(VaultInventoryEntry),
23712
+ // `null` once the last page is reached.
23713
+ nextCursor: external_exports.string().nullable()
23714
+ });
23715
+ var ListVaultReuseQuery = external_exports.object({
23716
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23717
+ cursor: external_exports.string().optional()
23718
+ });
23719
+ var ListVaultReuseResponse = external_exports.object({
23720
+ // Reused values across the whole store — the number the section's claim
23721
+ // ("values detected in more than one place") is about.
23722
+ totals: external_exports.object({ reused: external_exports.number().int().nonnegative() }),
23723
+ items: external_exports.array(VaultInventoryEntry),
23724
+ nextCursor: external_exports.string().nullable()
23725
+ });
23726
+ var ListVaultDerefsQuery = external_exports.object({
23727
+ // Include the batched, high-volume reasons (display, view-render). Omitted
23728
+ // hides them and counts them into `hiddenBatched` instead, so the model
23729
+ // crossings stay visible. A real boolean, not `z.stringbool()`: this arrives
23730
+ // over a Server Action, which preserves the type, never as a URL param.
23731
+ includeBatched: external_exports.boolean().optional(),
23732
+ limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23733
+ cursor: external_exports.string().optional()
23734
+ });
23735
+ var ListVaultDerefsResponse = external_exports.object({
23736
+ items: external_exports.array(VaultDeref),
23737
+ nextCursor: external_exports.string().nullable(),
23738
+ // Display/view-render rows the query hid, over the WHOLE trail rather than
23739
+ // this page — it is the count the "N hidden" line and its toggle speak for.
23740
+ // Always 0 when `includeBatched` was set, since nothing was hidden.
23741
+ hiddenBatched: external_exports.number().int().nonnegative()
23742
+ });
23743
+ var VaultKeyCustody = external_exports.string();
23744
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
23745
+ var VAULT_CONSENT_VERSION = 1;
23746
+ var VaultConsent = external_exports.object({
23747
+ acknowledgedAt: external_exports.iso.datetime(),
23748
+ version: external_exports.number().int().positive()
23749
+ });
23750
+ function isVaultConsentValid(consent) {
23751
+ return consent?.version === VAULT_CONSENT_VERSION;
23752
+ }
23753
+
23754
+ // ../../packages/schema/src/zod/local.ts
23755
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23756
+ var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23757
+ var HISTORY_SYNC_PAYLOAD_VERSION = 2;
23758
+ var RunMode = external_exports.enum(["standalone", "attached"]);
23759
+ var ControlPlaneConnection = external_exports.object({
23760
+ endpoint: external_exports.string().min(1),
23761
+ // Display name for the deployment, shown instead of the raw endpoint.
23762
+ label: external_exports.string().min(1).optional(),
23763
+ attachedAt: external_exports.iso.datetime()
23764
+ }).meta({ id: "ControlPlaneConnection" });
23765
+ var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
23766
+ var HistoricalAccess = external_exports.enum(["full", "session-only"]);
23767
+ var ModelJudgeConsent = external_exports.object({
23768
+ acknowledgedAt: external_exports.iso.datetime(),
23769
+ payloadVersion: external_exports.number().int().positive()
23770
+ });
23771
+ var HistorySyncConsent = external_exports.object({
23772
+ acknowledgedAt: external_exports.iso.datetime(),
23773
+ payloadVersion: external_exports.number().int().positive(),
23774
+ endpoint: external_exports.string()
23775
+ });
23776
+ function isHistorySyncConsentValid(consent, endpoint) {
23777
+ if (consent === void 0 || endpoint === void 0) return false;
23778
+ return consent.payloadVersion === HISTORY_SYNC_PAYLOAD_VERSION && consent.endpoint === endpoint;
23779
+ }
23780
+ var WorkspaceSettings = external_exports.object({
23781
+ specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23782
+ runMode: RunMode.default("standalone"),
23783
+ // Present only while attached; a detach clears it. Its presence is what makes
23784
+ // `runMode: 'attached'` mean anything — see isAttached.
23785
+ controlPlane: ControlPlaneConnection.optional(),
23786
+ policy: SimpleDetectionPolicy.default("redact"),
23787
+ // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
23788
+ historicalAccess: HistoricalAccess.default("session-only"),
23789
+ // In-place egress extraction on the scan paths; disable to stop all Data
23790
+ // Shares writes.
23791
+ dataSharesInPlace: external_exports.boolean().default(true),
23792
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
23793
+ // vault, instead of destroying them. Absent by default: this is a custody
23794
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
23795
+ // Revoking stops future vaulting; it does not erase what is already stored —
23796
+ // purging the vault is the eraser.
23797
+ vaultConsent: VaultConsent.optional(),
23798
+ // Where the vault master key lives.
23799
+ vaultKeyCustody: VaultKeyCustody.default("file"),
23800
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23801
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
23802
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23803
+ // place. Not a handling policy: the policy has already resolved to redact,
23804
+ // and this only says what happens when the host offers no channel to carry it
23805
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23806
+ // Claude Code decline to mask a field that EXECUTES because masking would
23807
+ // change what runs. Per FIELD rather than per host, so a host that can
23808
+ // rewrite some inputs keeps true redaction on those.
23809
+ //
23810
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23811
+ // an attached machine's merge is `strongerAction` over the one action ladder
23812
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23813
+ // word and stays out of the stored value.
23814
+ redactFallback: RedactFallback.default("warn"),
23815
+ // Absent until /aka:setup completes; its presence is what "onboarded" means.
23816
+ onboardedAt: external_exports.iso.datetime().optional(),
23817
+ // Records that the user consented to sending findings to the model API for
23818
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
23819
+ // Absent until granted; a stale payloadVersion means the consent no longer
23820
+ // covers the current payload and must be re-granted.
23821
+ modelJudgeConsent: ModelJudgeConsent.optional(),
23822
+ // Records that the user consented to the DEFERRED send — the outbox — along
23823
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23824
+ // that covers both the pre-attach backlog and undelivered captures (which
23825
+ // carry prompt/reply text in `content`); the key name predates the widening.
23826
+ // Absent until granted, and a grant for a different endpoint or an older
23827
+ // payload no longer counts.
23828
+ historySyncConsent: HistorySyncConsent.optional()
23829
+ });
23830
+ function defaultWorkspaceSettings() {
23831
+ return WorkspaceSettings.parse({});
23832
+ }
23833
+ function isAttached(settings) {
23834
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23835
+ }
23836
+ function toInventoryRow(input2, id, now) {
23837
+ return {
23838
+ id,
23839
+ objectType: input2.objectType,
23840
+ location: input2.location ?? null,
23841
+ title: input2.title ?? null,
23842
+ hostId: input2.hostId ?? null,
23843
+ attributes: JSON.stringify(input2.attributes),
23844
+ firstSeen: now,
23845
+ lastSeen: now
23846
+ };
23847
+ }
23848
+ function toSourceProjectRow(input2, id, now) {
23849
+ return {
23850
+ id,
23851
+ url: input2.url,
23852
+ name: input2.name ?? null,
23853
+ attributes: JSON.stringify(input2.attributes),
23854
+ firstSeen: now,
23855
+ lastSeen: now
23856
+ };
23857
+ }
23858
+ function toAuditEventRow(input2) {
23859
+ return {
23860
+ id: input2.id,
23861
+ parentId: input2.parentId ?? null,
23862
+ rootSessionId: input2.rootSessionId ?? null,
23863
+ eventType: input2.eventType,
23864
+ hostId: input2.hostId ?? null,
23865
+ harnessId: input2.harnessId ?? null,
23866
+ sourceProjectId: input2.sourceProjectId ?? null,
23867
+ startedAt: isoToEpochMillis(input2.startedAt),
23868
+ endedAt: input2.endedAt ? isoToEpochMillis(input2.endedAt) : null,
23869
+ severity: input2.severity ?? null,
23870
+ priority: input2.priority ?? null,
23871
+ content: input2.content ?? null,
23872
+ contentHash: input2.contentHash ?? null,
23873
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23874
+ };
23875
+ }
23876
+ function toClassifiedDataRow(input2, id) {
23877
+ return {
23878
+ id,
23879
+ class: input2.class,
23880
+ label: input2.label ?? null,
23881
+ attributes: input2.attributes ? JSON.stringify(input2.attributes) : null
23882
+ };
23883
+ }
23884
+ function toInspectionDefinitionRow(input2, id) {
23885
+ return {
23886
+ id,
23887
+ ruleId: input2.ruleId,
23888
+ name: input2.name,
23889
+ category: input2.category,
23890
+ severity: input2.severity,
23891
+ definition: input2.definition,
23892
+ version: input2.version
23893
+ };
23894
+ }
23895
+ function toInspectionFindingRow(input2) {
23896
+ return {
23897
+ id: input2.id,
23898
+ auditEventId: input2.auditEventId,
23899
+ inspectionDefinitionId: input2.inspectionDefinitionId,
23900
+ classifiedDataId: input2.classifiedDataId ?? null,
23901
+ spanStart: input2.span.start,
23902
+ spanEnd: input2.span.end,
23903
+ maskedMatch: input2.maskedMatch,
23904
+ actionTaken: input2.actionTaken,
23905
+ confidence: input2.confidence,
23906
+ findingKey: input2.findingKey ?? null,
23907
+ firstDetectedAt: input2.firstDetectedAt ? isoToEpochMillis(input2.firstDetectedAt) : null
23908
+ };
23909
+ }
23910
+ function toCaptureAttributes(event) {
23911
+ const metadata = event.metadata;
23912
+ return {
23913
+ source_tool: event.sourceTool,
23914
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
23915
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
23916
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
23917
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
23918
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
23919
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
23920
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23921
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23922
+ ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
23923
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23924
+ // has ever populated either), but every legacy metadata key still rides
23925
+ // the bag rather than being silently dropped — CaptureAttributes'
23926
+ // `.catchall(z.unknown())` carries the long tail.
23927
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23928
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
23929
+ };
23930
+ }
23931
+ function captureDefinitionVersion(finding2) {
23932
+ return `capture/${finding2.category}/${finding2.severity}`;
23933
+ }
23934
+ function toCaptureDefinitionInput(finding2) {
23935
+ return {
23936
+ ruleId: finding2.ruleId,
23937
+ version: captureDefinitionVersion(finding2),
23938
+ name: finding2.ruleId,
23939
+ category: finding2.category,
23940
+ severity: finding2.severity,
23941
+ definition: JSON.stringify({ ruleId: finding2.ruleId })
23942
+ };
23943
+ }
23944
+
23945
+ // ../../packages/schema/src/zod/managed.ts
23946
+ var MANAGED_SETTINGS_FILENAME = "managed-settings.json";
23947
+ var MANAGED_SETTINGS_SPEC_VERSION = 1;
23948
+ var ManagedSettingKey = external_exports.enum([
23949
+ "runMode",
23950
+ "historicalAccess",
23951
+ "vaultConsent",
23952
+ "vaultKeyCustody",
23953
+ "vaultInlineReveal",
23954
+ "modelJudgeConsent",
23955
+ "dataSharesInPlace",
23956
+ "redactFallback"
23957
+ ]).meta({ id: "ManagedSettingKey" });
23958
+ var ManagedSettingsValues = external_exports.object({
23959
+ runMode: external_exports.enum(["standalone", "attached"]).optional(),
23960
+ controlPlane: external_exports.object({
23961
+ endpoint: external_exports.string().min(1),
23962
+ label: external_exports.string().min(1).optional()
23963
+ }).optional(),
23964
+ historicalAccess: external_exports.enum(["full", "session-only"]).optional(),
23965
+ vaultConsent: external_exports.boolean().optional(),
23966
+ vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23967
+ vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23968
+ modelJudgeConsent: external_exports.boolean().optional(),
23969
+ dataSharesInPlace: external_exports.boolean().optional(),
23970
+ redactFallback: RedactFallback.optional()
23971
+ }).meta({ id: "ManagedSettingsValues" });
23972
+ var ManagedSettings = external_exports.object({
23973
+ specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
23974
+ // Shown on every locked control, so the user can tell an administrative
23975
+ // decision from a bug. Absent renders as a generic "your organization".
23976
+ organization: external_exports.string().min(1).optional(),
23977
+ // What the administrator pinned.
23978
+ values: ManagedSettingsValues.default({}),
23979
+ // Which of those the user may not change. A key here with no matching value
23980
+ // freezes whatever the user last chose; a value with no lock is a DEFAULT
23981
+ // the user may still override. The two are separable on purpose.
23982
+ lockedFields: external_exports.array(ManagedSettingKey).default([])
23983
+ }).meta({ id: "ManagedSettings" });
23984
+
23841
23985
  // ../../packages/schema/src/zod/project-files.ts
23842
23986
  var ProjectFileInput = external_exports.object({
23843
23987
  path: external_exports.string().min(1),
@@ -24083,10 +24227,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
24083
24227
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
24084
24228
 
24085
24229
  // ../../packages/schema/src/zod/settings-action.ts
24230
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24231
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
24086
24232
  var SaveSettingsInput = external_exports.object({
24087
24233
  historicalAccess: external_exports.string(),
24088
- modelJudgeConsent: external_exports.boolean(),
24089
- historySyncConsent: external_exports.boolean(),
24234
+ modelJudgeConsent: ModelJudgeConsentChoice,
24235
+ historySyncConsent: HistorySyncConsentChoice,
24090
24236
  vaultConsent: external_exports.string(),
24091
24237
  vaultInlineReveal: external_exports.string()
24092
24238
  });
@@ -24236,9 +24382,9 @@ function deriveReviewReasons(trust, transports) {
24236
24382
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24237
24383
  return reasons;
24238
24384
  }
24239
- function buildReviewInfo(trust, transports) {
24385
+ function buildReviewInfo(trust, transports, decided) {
24240
24386
  const reasons = deriveReviewReasons(trust, transports);
24241
- return { needsReview: reasons.length > 0, reasons };
24387
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24242
24388
  }
24243
24389
  function distinctTransports(transports) {
24244
24390
  return Array.from(new Set(transports));
@@ -24440,8 +24586,8 @@ function readControlPlaneCredential(settingsDir2, connection) {
24440
24586
  }
24441
24587
 
24442
24588
  // ../../packages/persistence/src/database.ts
24443
- import { randomUUID as randomUUID10 } from "crypto";
24444
- import { join as join4, sep } from "path";
24589
+ import { randomUUID as randomUUID11 } from "crypto";
24590
+ import { dirname as dirname2, join as join7, sep } from "path";
24445
24591
  import { DatabaseSync } from "node:sqlite";
24446
24592
 
24447
24593
  // ../../packages/persistence/src/ids.ts
@@ -24696,6 +24842,10 @@ function allRows(stmt, params) {
24696
24842
  if (Array.isArray(params)) return stmt.all(...params);
24697
24843
  return stmt.all(params);
24698
24844
  }
24845
+ function* iterateRows(stmt, params) {
24846
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24847
+ for (const row of rows) yield row;
24848
+ }
24699
24849
  function getRow(stmt, params) {
24700
24850
  if (params === void 0) return stmt.get();
24701
24851
  if (Array.isArray(params)) return stmt.get(...params);
@@ -25164,10 +25314,17 @@ function ensureSyncedAtColumn(db, table) {
25164
25314
  if (!columns.includes("sync_claimed_at")) {
25165
25315
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
25166
25316
  }
25317
+ if (!columns.includes("outbox_owed")) {
25318
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25319
+ }
25167
25320
  db.exec(
25168
25321
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25169
25322
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
25170
25323
  );
25324
+ db.exec(
25325
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25326
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25327
+ );
25171
25328
  db.exec(
25172
25329
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
25173
25330
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25272,7 +25429,6 @@ function decodeKeysetCursor(cursor) {
25272
25429
  // ../../packages/persistence/src/repositories/activity.ts
25273
25430
  var DAY_MS = 864e5;
25274
25431
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25275
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25276
25432
  function defaultTimeZone() {
25277
25433
  try {
25278
25434
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25327,6 +25483,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25327
25483
  error: "error",
25328
25484
  active: "active"
25329
25485
  };
25486
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25330
25487
  function safeParseStringArray(raw) {
25331
25488
  if (!raw) return [];
25332
25489
  const parsed2 = safeJson(raw, null);
@@ -25400,6 +25557,37 @@ var TIMELINE_COLUMNS = `
25400
25557
  json_extract(attributes, '$.targetId') AS target_id,
25401
25558
  json_extract(attributes, '$.internal') AS internal,
25402
25559
  json_extract(attributes, '$.flagged') AS flagged`;
25560
+ var LLM_USAGE_SELECT = `
25561
+ SELECT root_session_id AS sessionId,
25562
+ provider,
25563
+ model,
25564
+ service_tier AS serviceTier,
25565
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25566
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25567
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25568
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25569
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25570
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25571
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25572
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25573
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25574
+ function usageLeaves(rows) {
25575
+ return rows.map((row) => {
25576
+ const attributes = {
25577
+ input_tokens: row.inputTokens,
25578
+ output_tokens: row.outputTokens,
25579
+ cache_creation_input_tokens: row.cacheCreationTokens,
25580
+ cache_read_input_tokens: row.cacheReadTokens,
25581
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25582
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25583
+ web_search_requests: row.webSearchRequests
25584
+ };
25585
+ if (row.provider !== null) attributes.provider = row.provider;
25586
+ if (row.model !== null) attributes.model = row.model;
25587
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25588
+ return { sessionId: row.sessionId, attributes };
25589
+ });
25590
+ }
25403
25591
  var SESSION_ROOT = `event_type = 'session'`;
25404
25592
  var HAS_ACTIVITY = `EXISTS (
25405
25593
  SELECT 1 FROM audit_events c
@@ -25425,16 +25613,17 @@ var SqliteActivityRepository = class {
25425
25613
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25426
25614
  const liveNow = countScalar(
25427
25615
  this.db,
25428
- `SELECT count(*) AS n FROM audit_events s
25616
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25429
25617
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25430
- AND max(
25431
- s.started_at,
25432
- coalesce(
25433
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25434
- s.started_at
25435
- )
25436
- ) >= ?`,
25437
- [liveThreshold]
25618
+ AND s.id IN (
25619
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25620
+ UNION
25621
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25622
+ WHERE started_at >= ?
25623
+ UNION
25624
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25625
+ WHERE ended_at >= ?)`,
25626
+ [liveThreshold, liveThreshold, liveThreshold]
25438
25627
  );
25439
25628
  const toolCallsToday = countScalar(
25440
25629
  this.db,
@@ -25564,7 +25753,7 @@ var SqliteActivityRepository = class {
25564
25753
  this.db.prepare(
25565
25754
  `SELECT ${TIMELINE_COLUMNS}
25566
25755
  FROM audit_events
25567
- WHERE id = ? OR root_session_id = ?
25756
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25568
25757
  ORDER BY started_at ASC, id ASC`
25569
25758
  ),
25570
25759
  [sessionId, sessionId]
@@ -25577,14 +25766,14 @@ var SqliteActivityRepository = class {
25577
25766
  coalesce(sum(output_tokens), 0) AS output,
25578
25767
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25579
25768
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25580
- FROM audit_events
25769
+ FROM audit_events INDEXED BY idx_audit_session_type
25581
25770
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25582
25771
  ),
25583
25772
  [sessionId]
25584
25773
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25585
25774
  const primaryModel = getRow(
25586
25775
  this.db.prepare(
25587
- `SELECT model, provider FROM audit_events
25776
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25588
25777
  WHERE root_session_id = ? AND event_type = 'llm_call'
25589
25778
  ORDER BY started_at ASC, id ASC
25590
25779
  LIMIT 1`
@@ -25595,7 +25784,7 @@ var SqliteActivityRepository = class {
25595
25784
  this.db.prepare(
25596
25785
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25597
25786
  count(*) AS n
25598
- FROM audit_events
25787
+ FROM audit_events INDEXED BY idx_audit_session
25599
25788
  WHERE root_session_id = ? AND event_type = 'tool_call'
25600
25789
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25601
25790
  ),
@@ -25603,7 +25792,7 @@ var SqliteActivityRepository = class {
25603
25792
  );
25604
25793
  const modelRows = allRows(
25605
25794
  this.db.prepare(
25606
- `SELECT DISTINCT model FROM audit_events
25795
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25607
25796
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25608
25797
  ORDER BY model`
25609
25798
  ),
@@ -25612,7 +25801,7 @@ var SqliteActivityRepository = class {
25612
25801
  const derivedModels = modelRows.map((r) => r.model);
25613
25802
  const commits = countScalar(
25614
25803
  this.db,
25615
- `SELECT count(*) AS n FROM audit_events
25804
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25616
25805
  WHERE root_session_id = ? AND event_type = 'commit'`,
25617
25806
  [sessionId]
25618
25807
  );
@@ -25648,25 +25837,57 @@ var SqliteActivityRepository = class {
25648
25837
  return Promise.resolve(session);
25649
25838
  }
25650
25839
  /**
25651
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25652
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25653
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25654
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25655
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25656
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25840
+ * Cross-session token report — every `llm_call` in the store (or in a
25841
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25842
+ * session, with USD cost DERIVED at read time via the shared
25843
+ * `defaultCostModel` (never stored). The caller collapses these onto
25844
+ * per-model rows with `aggregateTokenUsage`.
25845
+ *
25846
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25847
+ * the members the rollup sums — and priced once per group, which is exact
25848
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25849
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25850
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25851
+ * the index stores the values once, at write, and answers the same window in
25852
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25853
+ * planner prefers the general event-type index and fetches every row to
25854
+ * recompute the columns it could have read. The index is one every open
25855
+ * store carries, since opening runs the migrations, so the hard requirement
25856
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25857
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25858
+ * per call, no bag parsed.
25657
25859
  */
25658
25860
  tokenReports(fromMs) {
25659
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25660
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25861
+ const rows = allRows(
25862
+ this.db.prepare(
25863
+ `${LLM_USAGE_SELECT}
25864
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25865
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25866
+ ${LLM_USAGE_GROUP}`
25867
+ ),
25868
+ fromMs === void 0 ? void 0 : [fromMs]
25869
+ );
25870
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25661
25871
  }
25662
25872
  /**
25663
- * One session's token report — its `llm_call` leaves grouped per (provider,
25664
- * model) with derived cost, or `null` when the session made no `llm_call`s
25665
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25666
- * breakdown + estimated cost.
25873
+ * One session's token report — its `llm_call`s grouped per (provider,
25874
+ * model, tier) with derived cost, or `null` when the session made no
25875
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25876
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25877
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25878
+ * it replaces walked every `llm_call` in the store to find one session's.
25667
25879
  */
25668
25880
  tokenReportForSession(sessionId) {
25669
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25881
+ const rows = allRows(
25882
+ this.db.prepare(
25883
+ `${LLM_USAGE_SELECT}
25884
+ FROM audit_events
25885
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25886
+ ${LLM_USAGE_GROUP}`
25887
+ ),
25888
+ [sessionId]
25889
+ );
25890
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25670
25891
  return Promise.resolve(reports[0] ?? null);
25671
25892
  }
25672
25893
  /**
@@ -25690,42 +25911,6 @@ var SqliteActivityRepository = class {
25690
25911
  for (const row of rows) seen.add(toHarness(row.harness));
25691
25912
  return Promise.resolve([...seen]);
25692
25913
  }
25693
- /**
25694
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25695
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25696
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25697
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25698
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25699
- */
25700
- readLlmCallLeaves(opts = {}) {
25701
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25702
- const params = [];
25703
- if (opts.sessionId !== void 0) {
25704
- conditions.push("root_session_id = ?");
25705
- params.push(opts.sessionId);
25706
- }
25707
- if (opts.fromMs !== void 0) {
25708
- conditions.push("started_at >= ?");
25709
- params.push(opts.fromMs);
25710
- }
25711
- const rows = allRows(
25712
- this.db.prepare(
25713
- `SELECT root_session_id AS sessionId, attributes
25714
- FROM audit_events
25715
- WHERE ${conditions.join(" AND ")}`
25716
- ),
25717
- params
25718
- );
25719
- return mapRowsTolerant(
25720
- rows.filter(
25721
- (row) => row.sessionId !== null
25722
- ),
25723
- (row) => ({
25724
- sessionId: row.sessionId,
25725
- attributes: JSON.parse(row.attributes)
25726
- })
25727
- );
25728
- }
25729
25914
  /**
25730
25915
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25731
25916
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25740,20 +25925,23 @@ var SqliteActivityRepository = class {
25740
25925
  const inClause = placeholders(sessionIds.length);
25741
25926
  const lastActivityRows = allRows(
25742
25927
  this.db.prepare(
25743
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25744
- WHERE root_session_id IN (${inClause})
25745
- GROUP BY root_session_id`
25928
+ `SELECT ids.value AS id,
25929
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25930
+ (SELECT max(ended_at) FROM audit_events e
25931
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25932
+ FROM json_each(?) AS ids`
25746
25933
  ),
25747
- sessionIds
25934
+ [JSON.stringify(sessionIds)]
25748
25935
  );
25749
25936
  for (const row of lastActivityRows) {
25750
- if (row.id === null) continue;
25751
25937
  const entry = result.get(row.id);
25752
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25938
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25939
+ if (entry && last > 0) entry.lastActivityMs = last;
25753
25940
  }
25754
25941
  const turnsRows = allRows(
25755
25942
  this.db.prepare(
25756
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25943
+ `SELECT root_session_id AS id, count(*) AS n
25944
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25757
25945
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25758
25946
  GROUP BY root_session_id`
25759
25947
  ),
@@ -25768,7 +25956,7 @@ var SqliteActivityRepository = class {
25768
25956
  this.db.prepare(
25769
25957
  `SELECT root_session_id AS id,
25770
25958
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25771
- FROM audit_events
25959
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25772
25960
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25773
25961
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25774
25962
  GROUP BY root_session_id`
@@ -25798,7 +25986,7 @@ var SqliteActivityRepository = class {
25798
25986
  this.db.prepare(
25799
25987
  `SELECT root_session_id AS id,
25800
25988
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25801
- FROM audit_events
25989
+ FROM audit_events INDEXED BY idx_audit_session_share
25802
25990
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25803
25991
  GROUP BY root_session_id`
25804
25992
  ),
@@ -26827,7 +27015,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26827
27015
 
26828
27016
  // ../../packages/persistence/src/repositories/findings.ts
26829
27017
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26830
- var SCAN_BATCH_ROWS = 1e3;
26831
27018
  var DEFAULT_LOCATIONS_LIMIT = 100;
26832
27019
  var LOCATION_RULE_IDS_CAP = 20;
26833
27020
  function compareLocationOrder(a, b) {
@@ -26856,6 +27043,25 @@ function deriveInstanceStatus(row) {
26856
27043
  latestResolutionStatus: row.latest_status
26857
27044
  });
26858
27045
  }
27046
+ function toFlatFindingRow(r) {
27047
+ return {
27048
+ id: r.id,
27049
+ ruleId: r.rule_id,
27050
+ category: r.category,
27051
+ severity: r.severity,
27052
+ maskedMatch: r.masked_match,
27053
+ actionTaken: r.action_taken,
27054
+ confidence: r.confidence,
27055
+ occurredAt: epochMillisToIso(r.occurred_at),
27056
+ sourceTool: r.source_tool,
27057
+ repo: r.repo ?? "",
27058
+ file: r.file ?? "",
27059
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
27060
+ eventId: r.event_id,
27061
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
27062
+ status: deriveInstanceStatus(r)
27063
+ };
27064
+ }
26859
27065
  function encodeGroupCursor(group) {
26860
27066
  const payload = {
26861
27067
  sev: group.severity,
@@ -26931,7 +27137,7 @@ var SqliteFindingsRepository = class {
26931
27137
  this.db.prepare(
26932
27138
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26933
27139
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26934
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27140
+ e.source_tool AS source_tool,
26935
27141
  e.event_type AS kind
26936
27142
  FROM audit_events e
26937
27143
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -27039,56 +27245,11 @@ var SqliteFindingsRepository = class {
27039
27245
  predicate,
27040
27246
  params: sessionParams
27041
27247
  });
27042
- const rows = allRows(
27043
- this.db.prepare(
27044
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
27045
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
27046
- kind, finding_key, latest_status
27047
- FROM (
27048
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27049
- d.severity AS severity, f.masked_match AS masked_match,
27050
- f.action_taken AS action_taken, f.confidence AS confidence,
27051
- e.started_at AS occurred_at,
27052
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27053
- json_extract(e.attributes, '$.repo') AS repo,
27054
- json_extract(e.attributes, '$.file_path') AS file,
27055
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27056
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27057
- e.event_type AS kind, f.finding_key AS finding_key,
27058
- latest.status AS latest_status,
27059
- ROW_NUMBER() OVER (
27060
- PARTITION BY d.rule_id
27061
- ORDER BY e.started_at DESC, f.id DESC
27062
- ) AS rn
27063
- FROM inspection_findings f
27064
- JOIN audit_events e ON e.id = f.audit_event_id
27065
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27066
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27067
- ON latest.finding_key = f.finding_key
27068
- ${predicate}
27069
- )
27070
- WHERE rn <= :cap
27071
- ORDER BY occurred_at DESC, id DESC`
27072
- ),
27073
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
27074
- );
27075
- const groupable = rows.map((r) => ({
27076
- id: r.id,
27077
- ruleId: r.rule_id,
27078
- category: r.category,
27079
- severity: r.severity,
27080
- maskedMatch: r.masked_match,
27081
- actionTaken: r.action_taken,
27082
- confidence: r.confidence,
27083
- occurredAt: epochMillisToIso(r.occurred_at),
27084
- sourceTool: r.source_tool,
27085
- repo: r.repo ?? "",
27086
- file: r.file ?? "",
27087
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27088
- eventId: r.event_id,
27089
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27090
- status: deriveInstanceStatus(r)
27091
- }));
27248
+ const rows = this.previewRows(aggregates, {
27249
+ sessionId: query.sessionId,
27250
+ from: query.from
27251
+ });
27252
+ const groupable = rows.map(toFlatFindingRow);
27092
27253
  const allGroups = buildFindingGroups(groupable, { aggregates });
27093
27254
  const filterOpts = {
27094
27255
  severity: query.severity,
@@ -27174,8 +27335,10 @@ var SqliteFindingsRepository = class {
27174
27335
  *
27175
27336
  * The scan runs from the top of the scope on every request, not from the
27176
27337
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
27177
- * move as the caller pages. Rows are pulled in batches so memory stays flat
27178
- * while the counting runs, and only the page itself is retained.
27338
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27339
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27340
+ * counting runs — a generator streaming the index order, not a sequence of
27341
+ * fetched batches; only the page itself is retained.
27179
27342
  */
27180
27343
  listFindingInstances(query) {
27181
27344
  const opts = {
@@ -27191,6 +27354,10 @@ var SqliteFindingsRepository = class {
27191
27354
  };
27192
27355
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27193
27356
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27357
+ const isPastCursor = cursor === null ? () => true : (row) => {
27358
+ const rowMs = isoToEpochMillis(row.occurredAt);
27359
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27360
+ };
27194
27361
  const accumulator = createInstanceFacetAccumulator(opts);
27195
27362
  const items = [];
27196
27363
  let total = 0;
@@ -27203,6 +27370,7 @@ var SqliteFindingsRepository = class {
27203
27370
  accumulator.add(row);
27204
27371
  if (!matchesInstanceFilters(row, opts)) continue;
27205
27372
  total += 1;
27373
+ if (!isPastCursor(row)) continue;
27206
27374
  if (items.length < limit) {
27207
27375
  items.push(toInstanceDetail(row));
27208
27376
  last = row;
@@ -27211,15 +27379,6 @@ var SqliteFindingsRepository = class {
27211
27379
  }
27212
27380
  }
27213
27381
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27214
- if (cursor !== null) {
27215
- const resumed = this.pageAfter(cursor, opts, limit, query);
27216
- return Promise.resolve({
27217
- totals: { findings: total },
27218
- facets: accumulator.facets(),
27219
- items: resumed.items,
27220
- nextCursor: resumed.nextCursor
27221
- });
27222
- }
27223
27382
  return Promise.resolve({
27224
27383
  totals: { findings: total },
27225
27384
  facets: accumulator.facets(),
@@ -27227,35 +27386,6 @@ var SqliteFindingsRepository = class {
27227
27386
  nextCursor
27228
27387
  });
27229
27388
  }
27230
- /**
27231
- * The page of matching rows strictly after `cursor`. Separate from the
27232
- * counting pass because that one starts at the top of the scope by design;
27233
- * this one narrows the scan with the same keyset predicate the activity list
27234
- * uses, so a later page costs less than the first rather than more.
27235
- */
27236
- pageAfter(cursor, opts, limit, query) {
27237
- const items = [];
27238
- let last;
27239
- let hasMore = false;
27240
- for (const row of this.scanFindingRows({
27241
- sessionId: query.sessionId,
27242
- from: query.from,
27243
- after: cursor
27244
- })) {
27245
- if (!matchesInstanceFilters(row, opts)) continue;
27246
- if (items.length < limit) {
27247
- items.push(toInstanceDetail(row));
27248
- last = row;
27249
- } else {
27250
- hasMore = true;
27251
- break;
27252
- }
27253
- }
27254
- return {
27255
- items,
27256
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27257
- };
27258
- }
27259
27389
  /**
27260
27390
  * The same findings folded by location: repository, then file within it.
27261
27391
  *
@@ -27338,25 +27468,111 @@ var SqliteFindingsRepository = class {
27338
27468
  });
27339
27469
  }
27340
27470
  /**
27341
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27471
+ * Each group's newest instances, for the table's expanded rows.
27472
+ *
27473
+ * ONE index-ordered scan with early termination, and the shape is the point.
27474
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27475
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27476
+ * through a temp B-tree to keep a bounded preview of each group, and then
27477
+ * sorts the survivors again for the page order. Both sorts grow with the
27478
+ * store while the answer does not.
27479
+ *
27480
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27481
+ * (or the session or window index the scope names — see `findingScanSql`),
27482
+ * which is already the order the page wants, and keeps rows per rule until
27483
+ * each rule has as many as it can show. The aggregate the caller already holds
27484
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27485
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27486
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27487
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27488
+ * store with many firing rules widens it. The bound that DOES hold
27489
+ * unconditionally is the sorted form's floor: this scan visits at most as
27490
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27491
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27492
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27493
+ * wanted instances sitting at the tail of the scope — is one pass over
27494
+ * everything in scope with a block sort of the id tie-break only, never a
27495
+ * sort of the scope, which is still that floor.
27496
+ *
27497
+ * A row whose rule the aggregate did not see is skipped: the two statements
27498
+ * run without a shared snapshot, so a capture landing between them can add a
27499
+ * rule here that has no counts there, and the counts are what the group is
27500
+ * built from.
27501
+ */
27502
+ previewRows(aggregates, scope) {
27503
+ const wanted = /* @__PURE__ */ new Map();
27504
+ let remaining = 0;
27505
+ for (const [ruleId, agg] of aggregates) {
27506
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27507
+ wanted.set(ruleId, n);
27508
+ remaining += n;
27509
+ }
27510
+ const rows = [];
27511
+ if (remaining === 0) return rows;
27512
+ const { sql, params } = this.findingScanSql(scope);
27513
+ const taken = /* @__PURE__ */ new Map();
27514
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27515
+ const want = wanted.get(r.rule_id);
27516
+ if (want === void 0) continue;
27517
+ const have = taken.get(r.rule_id) ?? 0;
27518
+ if (have >= want) continue;
27519
+ taken.set(r.rule_id, have + 1);
27520
+ rows.push(r);
27521
+ remaining -= 1;
27522
+ if (remaining === 0) break;
27523
+ }
27524
+ return rows;
27525
+ }
27526
+ /**
27527
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27342
27528
  *
27343
27529
  * A generator so a caller streams the scope without it ever being an array:
27344
27530
  * the flat list counts and facets the whole filtered scope, which on a large
27345
- * store is far more rows than any page. Each batch advances the same keyset
27346
- * predicate the page read uses, so the scan is a sequence of bounded reads
27347
- * rather than one unbounded result set.
27348
- *
27349
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27350
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27351
- * makes it a point lookup per row, and the derived table would re-materialize
27352
- * a window over the whole resolution table once per batch.
27531
+ * store is far more rows than any page. The rows come off ONE statement,
27532
+ * iterated rather than materialized, in the index order `findingScanSql`
27533
+ * arranges so the scan is a single pass with a block sort of the id
27534
+ * tie-break only, never a sort of the scope, where a sequence of
27535
+ * keyset-bounded batches re-sorted everything below the cursor on every
27536
+ * batch and cost the square of the scope.
27353
27537
  *
27354
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27355
- * would be missing from its own facet, which is computed by excluding that
27356
- * dimension see listFindingInstances.
27538
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27539
+ * dimension narrowed here would be missing from its own facet, which is
27540
+ * computed by excluding that dimension (see listFindingInstances). There is
27541
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27542
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27543
+ * narrower statement, since the counting pass already visits every row a
27544
+ * page-2+ request would otherwise re-seek for.
27357
27545
  */
27358
27546
  *scanFindingRows(scope) {
27359
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27547
+ const { sql, params } = this.findingScanSql(scope);
27548
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27549
+ yield toFlatFindingRow(r);
27550
+ }
27551
+ }
27552
+ /**
27553
+ * The one statement both instance-level scans run: every finding in scope,
27554
+ * joined to its event and definition, newest first.
27555
+ *
27556
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27557
+ * the same two `recentFindings` documents at length, for the same reason:
27558
+ *
27559
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27560
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27561
+ * yields `started_at` order per event type, not across the four, so
27562
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27563
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27564
+ * `idx_audit_session` for a session scope, which is also `started_at`
27565
+ * ordered within the session — and the order falls out of the index.
27566
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27567
+ * JOINs the planner drives from the findings and sorts everything.
27568
+ *
27569
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27570
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27571
+ * index probe per keyed row, and a derived table over the whole resolution
27572
+ * table would be materialized before the first row streamed.
27573
+ */
27574
+ findingScanSql(scope) {
27575
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27360
27576
  const params = [];
27361
27577
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27362
27578
  conditions.push("e.root_session_id = ?");
@@ -27370,58 +27586,24 @@ var SqliteFindingsRepository = class {
27370
27586
  d.severity AS severity, f.masked_match AS masked_match,
27371
27587
  f.action_taken AS action_taken, f.confidence AS confidence,
27372
27588
  e.started_at AS occurred_at,
27373
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27374
- json_extract(e.attributes, '$.repo') AS repo,
27375
- json_extract(e.attributes, '$.file_path') AS file,
27376
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27589
+ e.source_tool AS source_tool,
27590
+ e.repo AS repo,
27591
+ e.file_path AS file,
27592
+ e.tool_name AS tool_name,
27377
27593
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27378
27594
  e.event_type AS kind, f.finding_key AS finding_key,
27379
27595
  ${latestResolutionStatusSql("f")} AS latest_status
27380
- FROM inspection_findings f
27381
- JOIN audit_events e ON e.id = f.audit_event_id
27382
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27596
+ FROM audit_events e
27597
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27598
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27383
27599
  WHERE ${conditions.join(" AND ")}
27384
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27385
- ORDER BY e.started_at DESC, f.id DESC
27386
- LIMIT ?`;
27387
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27388
- for (; ; ) {
27389
- const rows = allRows(this.db.prepare(sql), [
27390
- ...params,
27391
- after.startedAtMs,
27392
- after.startedAtMs,
27393
- after.id,
27394
- SCAN_BATCH_ROWS
27395
- ]);
27396
- for (const r of rows) {
27397
- yield {
27398
- id: r.id,
27399
- ruleId: r.rule_id,
27400
- category: r.category,
27401
- severity: r.severity,
27402
- maskedMatch: r.masked_match,
27403
- actionTaken: r.action_taken,
27404
- confidence: r.confidence,
27405
- occurredAt: epochMillisToIso(r.occurred_at),
27406
- sourceTool: r.source_tool,
27407
- repo: r.repo ?? "",
27408
- file: r.file ?? "",
27409
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27410
- eventId: r.event_id,
27411
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27412
- status: deriveInstanceStatus(r)
27413
- };
27414
- }
27415
- if (rows.length < SCAN_BATCH_ROWS) return;
27416
- const lastRow = rows[rows.length - 1];
27417
- if (lastRow === void 0) return;
27418
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27419
- }
27600
+ ORDER BY e.started_at DESC, f.id DESC`;
27601
+ return { sql, params };
27420
27602
  }
27421
27603
  groupAggregates(withSearchText, scope) {
27422
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27423
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27424
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27604
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27605
+ group_concat(DISTINCT e.file_path) AS files,
27606
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27425
27607
  const rows = this.db.prepare(
27426
27608
  `SELECT rule_id,
27427
27609
  sum(tuple_count) AS instance_count,
@@ -27439,7 +27621,7 @@ var SqliteFindingsRepository = class {
27439
27621
  coalesce(latest.status, '') AS status_tuple,
27440
27622
  count(*) AS tuple_count,
27441
27623
  max(e.started_at) AS latest_at,
27442
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27624
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27443
27625
  group_concat(DISTINCT f.action_taken) AS actions_taken
27444
27626
  ${innerSearchColumns}
27445
27627
  FROM inspection_findings f
@@ -27570,6 +27752,8 @@ function isoDay(ms) {
27570
27752
  // ../../packages/persistence/src/repositories/history-sync.ts
27571
27753
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27572
27754
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27755
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27756
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27573
27757
  var SKIPPED = -1;
27574
27758
  var ROW_COLUMNS = `id,
27575
27759
  parent_id AS parentId,
@@ -27609,6 +27793,20 @@ var SqliteHistorySyncRepository = class {
27609
27793
  ORDER BY (event_type = 'session') DESC, started_at
27610
27794
  LIMIT :limit`
27611
27795
  );
27796
+ this.captureRowsStmt = db.prepare(
27797
+ `SELECT ${ROW_COLUMNS}
27798
+ FROM audit_events
27799
+ WHERE synced_at IS NULL
27800
+ AND sync_claimed_at IS NULL
27801
+ AND outbox_owed = 1
27802
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27803
+ AND started_at < :before
27804
+ ORDER BY started_at
27805
+ LIMIT :limit`
27806
+ );
27807
+ this.markOwedStmt = db.prepare(
27808
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27809
+ );
27612
27810
  this.stampStmt = db.prepare(
27613
27811
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27614
27812
  );
@@ -27640,6 +27838,12 @@ var SqliteHistorySyncRepository = class {
27640
27838
  FROM audit_events
27641
27839
  WHERE event_type IN (${TYPE_LIST})`
27642
27840
  );
27841
+ this.captureSkipCountStmt = db.prepare(
27842
+ `SELECT COUNT(*) AS skipped
27843
+ FROM audit_events
27844
+ WHERE synced_at = ${String(SKIPPED)}
27845
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27846
+ );
27643
27847
  this.fingerprintStmt = db.prepare(
27644
27848
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27645
27849
  FROM history_sync WHERE id = 1`
@@ -27649,6 +27853,10 @@ var SqliteHistorySyncRepository = class {
27649
27853
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27650
27854
  WHERE id = 1`
27651
27855
  );
27856
+ this.disownCapturesStmt = db.prepare(
27857
+ `UPDATE audit_events SET outbox_owed = NULL
27858
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27859
+ );
27652
27860
  this.rearmStmt = db.prepare(
27653
27861
  `UPDATE audit_events SET synced_at = NULL
27654
27862
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27721,6 +27929,10 @@ var SqliteHistorySyncRepository = class {
27721
27929
  closeWindowStmt;
27722
27930
  releaseBoundaryStmt;
27723
27931
  freezeBoundaryStmt;
27932
+ captureRowsStmt;
27933
+ markOwedStmt;
27934
+ captureSkipCountStmt;
27935
+ disownCapturesStmt;
27724
27936
  partitionStmt;
27725
27937
  claimRowStmt;
27726
27938
  releaseRowStmt;
@@ -27754,6 +27966,34 @@ var SqliteHistorySyncRepository = class {
27754
27966
  pendingRows(sessionId, limit, before) {
27755
27967
  return allRows(this.rowsStmt, { sessionId, limit, before });
27756
27968
  }
27969
+ /**
27970
+ * Captures this machine still owes the deployment, oldest first.
27971
+ *
27972
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27973
+ * by a time window — see captureRowsStmt for why a window could not express
27974
+ * this. `before` is the grace window that leaves a just-recorded capture to
27975
+ * the live path.
27976
+ */
27977
+ pendingCaptureRows(limit, before) {
27978
+ return allRows(this.captureRowsStmt, { limit, before });
27979
+ }
27980
+ /**
27981
+ * Record that a capture is OWED to the deployment.
27982
+ *
27983
+ * Written by the attached forward path when a live send did not confirm
27984
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27985
+ * a fact rather than an inference: the machine was attached, the send did not
27986
+ * land, so the row is owed — which no time window can state, because the same
27987
+ * window that holds the rows a past attachment left owed also holds every
27988
+ * capture recorded while the machine was DETACHED, and those were never
27989
+ * offered to anyone.
27990
+ *
27991
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27992
+ * out of the drain's read.
27993
+ */
27994
+ markCaptureOwed(id) {
27995
+ this.markOwedStmt.run({ id });
27996
+ }
27757
27997
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27758
27998
  markSynced(ids, atMs) {
27759
27999
  this.stampAll(ids, atMs);
@@ -27837,10 +28077,12 @@ var SqliteHistorySyncRepository = class {
27837
28077
  this.countsStmt,
27838
28078
  { before }
27839
28079
  );
28080
+ const captures = getRow(this.captureSkipCountStmt);
27840
28081
  return {
27841
28082
  pending: row?.pending ?? 0,
27842
28083
  sent: row?.sent ?? 0,
27843
- skipped: row?.skipped ?? 0
28084
+ skipped: row?.skipped ?? 0,
28085
+ capturesSkipped: captures?.skipped ?? 0
27844
28086
  };
27845
28087
  }
27846
28088
  /**
@@ -27881,7 +28123,11 @@ var SqliteHistorySyncRepository = class {
27881
28123
  withTransaction(
27882
28124
  this.db,
27883
28125
  () => {
28126
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27884
28127
  this.rearmStmt.run();
28128
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28129
+ this.disownCapturesStmt.run();
28130
+ }
27885
28131
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27886
28132
  },
27887
28133
  "IMMEDIATE"
@@ -28078,7 +28324,259 @@ var SqliteInspectionFindingsRepository = class {
28078
28324
  };
28079
28325
 
28080
28326
  // ../../packages/persistence/src/repositories/installed-packs.ts
28081
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28327
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28328
+
28329
+ // ../../packages/persistence/src/policy-floor.ts
28330
+ import { readFileSync as readFileSync5 } from "fs";
28331
+ import { join as join6 } from "path";
28332
+
28333
+ // ../../packages/persistence/src/local-layout.ts
28334
+ import { renameSync as renameSync3 } from "fs";
28335
+ import { mkdir } from "fs/promises";
28336
+ import { homedir } from "os";
28337
+ import { join as join4 } from "path";
28338
+ function defaultDataDir() {
28339
+ return join4(homedir(), ".aka");
28340
+ }
28341
+ function settingsDir(base = defaultDataDir()) {
28342
+ return join4(base, "settings");
28343
+ }
28344
+ function dataDir(base = defaultDataDir()) {
28345
+ return join4(base, "data");
28346
+ }
28347
+ function dbPath(base = defaultDataDir()) {
28348
+ return join4(dataDir(base), "aka.db");
28349
+ }
28350
+ function keysDir(base = defaultDataDir()) {
28351
+ return join4(base, "keys");
28352
+ }
28353
+ async function ensureDataDir(dir = defaultDataDir()) {
28354
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
28355
+ tightenDir(dir);
28356
+ }
28357
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28358
+ ensureDataDirSync(dir);
28359
+ }
28360
+ function migrateLegacyLayout(base = defaultDataDir()) {
28361
+ const moves = [
28362
+ { name: "config.json", dest: settingsDir(base) },
28363
+ { name: "policy-cache.json", dest: dataDir(base) }
28364
+ ];
28365
+ for (const { name, dest } of moves) {
28366
+ try {
28367
+ ensureDataDirSync(dest);
28368
+ const moved = join4(dest, name);
28369
+ renameSync3(join4(base, name), moved);
28370
+ tightenFile(moved);
28371
+ } catch {
28372
+ }
28373
+ }
28374
+ }
28375
+
28376
+ // ../../packages/persistence/src/settings.ts
28377
+ import { readFileSync as readFileSync4 } from "fs";
28378
+ import { join as join5 } from "path";
28379
+
28380
+ // ../../packages/persistence/src/file-lock.ts
28381
+ import { randomUUID as randomUUID3 } from "crypto";
28382
+ import {
28383
+ closeSync,
28384
+ existsSync as existsSync2,
28385
+ openSync,
28386
+ readFileSync as readFileSync2,
28387
+ rmSync as rmSync5,
28388
+ statSync as statSync3,
28389
+ writeFileSync as writeFileSync2
28390
+ } from "fs";
28391
+ import { hostname as hostname3 } from "os";
28392
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28393
+
28394
+ // ../../packages/persistence/src/managed-settings.ts
28395
+ import { readFileSync as readFileSync3 } from "fs";
28396
+ import { posix, win32 } from "path";
28397
+ function managedSettingsPaths(platform2 = process.platform) {
28398
+ if (platform2 === "darwin") {
28399
+ return [
28400
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28401
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28402
+ ];
28403
+ }
28404
+ if (platform2 === "win32") {
28405
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28406
+ }
28407
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28408
+ }
28409
+ function readManagedSettings(paths = managedSettingsPaths()) {
28410
+ for (const path of paths) {
28411
+ let text;
28412
+ try {
28413
+ text = readFileSync3(path, "utf8");
28414
+ } catch {
28415
+ continue;
28416
+ }
28417
+ const record2 = parseJsonObject(text);
28418
+ if (!record2) continue;
28419
+ const parsed2 = ManagedSettings.safeParse(record2);
28420
+ if (parsed2.success) return parsed2.data;
28421
+ }
28422
+ return null;
28423
+ }
28424
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28425
+ if (!managed) return settings;
28426
+ const { values } = managed;
28427
+ const merged = { ...settings };
28428
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28429
+ if (values.controlPlane !== void 0) {
28430
+ merged.controlPlane = {
28431
+ ...values.controlPlane,
28432
+ // The administrator pinned WHICH deployment, not WHEN this machine
28433
+ // joined it. Keep the user's own attach time when the endpoint is
28434
+ // unchanged, so a managed machine does not appear to re-attach on every
28435
+ // read; stamp a fresh one when the administrator moved it.
28436
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28437
+ };
28438
+ }
28439
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28440
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28441
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28442
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28443
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28444
+ if (values.vaultConsent !== void 0) {
28445
+ merged.vaultConsent = values.vaultConsent ? (
28446
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28447
+ // at the current version otherwise.
28448
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28449
+ ) : void 0;
28450
+ }
28451
+ if (values.modelJudgeConsent !== void 0) {
28452
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28453
+ acknowledgedAt: now().toISOString(),
28454
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28455
+ } : void 0;
28456
+ }
28457
+ return merged;
28458
+ }
28459
+
28460
+ // ../../packages/persistence/src/settings.ts
28461
+ var SETTINGS_FILENAME = "settings.json";
28462
+ function readWorkspaceSettings(base = defaultDataDir()) {
28463
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28464
+ }
28465
+ function readUserSettings(base) {
28466
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28467
+ if (!record2) return defaultWorkspaceSettings();
28468
+ try {
28469
+ return WorkspaceSettings.parse(record2);
28470
+ } catch {
28471
+ return defaultWorkspaceSettings();
28472
+ }
28473
+ }
28474
+ function readJson(file2) {
28475
+ let text;
28476
+ try {
28477
+ text = readFileSync4(file2, "utf8");
28478
+ } catch {
28479
+ return null;
28480
+ }
28481
+ return parseJsonObject(text) ?? null;
28482
+ }
28483
+
28484
+ // ../../packages/persistence/src/policy-floor.ts
28485
+ function refusalMessage(pack, attempted, floor, refusal) {
28486
+ switch (refusal) {
28487
+ case "lock":
28488
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28489
+ case "disable":
28490
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28491
+ case "floor":
28492
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28493
+ }
28494
+ }
28495
+ var PolicyFloorError = class extends Error {
28496
+ /** `namespace/packId` of the detection whose write was refused. */
28497
+ pack;
28498
+ /**
28499
+ * The archetype the caller asked for, or null when the write named none —
28500
+ * clearing the assignment, or switching the detection off.
28501
+ */
28502
+ attempted;
28503
+ /** The weakest archetype the control plane permits for this pack. */
28504
+ floor;
28505
+ refusal;
28506
+ constructor(pack, attempted, floor, refusal) {
28507
+ super(refusalMessage(pack, attempted, floor, refusal));
28508
+ this.name = "PolicyFloorError";
28509
+ this.pack = pack;
28510
+ this.attempted = attempted;
28511
+ this.floor = floor;
28512
+ this.refusal = refusal;
28513
+ }
28514
+ };
28515
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28516
+ try {
28517
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28518
+ const parsed2 = JSON.parse(raw);
28519
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28520
+ return PolicyBundle.parse(parsed2.bundle);
28521
+ } catch {
28522
+ return null;
28523
+ }
28524
+ }
28525
+ function indexEnabled(policies) {
28526
+ const byRuleId = /* @__PURE__ */ new Map();
28527
+ const byCategory = /* @__PURE__ */ new Map();
28528
+ for (const policy of policies) {
28529
+ if (!policy.enabled) continue;
28530
+ if ("ruleId" in policy.target) {
28531
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28532
+ } else if (!byCategory.has(policy.target.category)) {
28533
+ byCategory.set(policy.target.category, policy.action);
28534
+ }
28535
+ }
28536
+ return { byRuleId, byCategory };
28537
+ }
28538
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28539
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28540
+ const categories = new Set(rules.map((rule) => rule.category));
28541
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28542
+ return policies.some((policy) => {
28543
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28544
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28545
+ });
28546
+ }
28547
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28548
+ const floors = openControlPlaneFloors(base);
28549
+ return floors === null ? null : floors.floorFor(rules);
28550
+ }
28551
+ function openControlPlaneFloors(base = defaultDataDir()) {
28552
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28553
+ const bundle = readCachedPolicyBundle(base);
28554
+ if (bundle === null) return null;
28555
+ const indexes = indexEnabled(bundle.policies);
28556
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28557
+ }
28558
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28559
+ let action = null;
28560
+ for (const rule of rules) {
28561
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28562
+ if (resolved === void 0) continue;
28563
+ action = action === null ? resolved : strongerAction(action, resolved);
28564
+ }
28565
+ if (action === null) return null;
28566
+ return {
28567
+ floor: weakestBuiltinAtLeast(action),
28568
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28569
+ };
28570
+ }
28571
+ function policyAssignmentRefusal(policyId, floor) {
28572
+ if (floor.locked) return "lock";
28573
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28574
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28575
+ }
28576
+ function packEnablementRefusal(enabled, floor) {
28577
+ if (floor === null || enabled) return null;
28578
+ return "disable";
28579
+ }
28082
28580
 
28083
28581
  // ../../packages/persistence/src/semver.ts
28084
28582
  function parse3(version2) {
@@ -28172,8 +28670,19 @@ function ruleIdsOf(rulesJson) {
28172
28670
  return ids;
28173
28671
  }
28174
28672
  var SqliteInstalledPacksRepository = class {
28175
- constructor(db) {
28673
+ /**
28674
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28675
+ * floor needs both halves of it (settings/ says whether this machine is
28676
+ * attached, data/ holds the cached bundle). It is optional because a caller
28677
+ * holding only a DatabaseSync — every test construction site, and any embedder
28678
+ * that opens the store itself — has no layout to point at, and such a caller
28679
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28680
+ * from `openLocalDatabase`, which is the single construction site that owns a
28681
+ * real `~/.aka`.
28682
+ */
28683
+ constructor(db, baseDir) {
28176
28684
  this.db = db;
28685
+ this.baseDir = baseDir;
28177
28686
  this.insertMissingStmt = db.prepare(
28178
28687
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
28179
28688
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28195,11 +28704,17 @@ var SqliteInstalledPacksRepository = class {
28195
28704
  this.signatureStmt = db.prepare(
28196
28705
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28197
28706
  );
28707
+ this.packRulesStmt = db.prepare(
28708
+ `SELECT rules_json AS rulesJson FROM installed_packs
28709
+ WHERE namespace = ? AND pack_id = ?`
28710
+ );
28198
28711
  }
28199
28712
  db;
28713
+ baseDir;
28200
28714
  insertMissingStmt;
28201
28715
  upsertAvailableStmt;
28202
28716
  signatureStmt;
28717
+ packRulesStmt;
28203
28718
  /**
28204
28719
  * Record the running binary's detection inventory. Refreshes the
28205
28720
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28241,7 +28756,7 @@ var SqliteInstalledPacksRepository = class {
28241
28756
  let behind = false;
28242
28757
  for (const row of rows) {
28243
28758
  const params = {
28244
- id: randomUUID3(),
28759
+ id: randomUUID4(),
28245
28760
  namespace: row.namespace,
28246
28761
  packId: row.packId,
28247
28762
  version: row.version,
@@ -28253,7 +28768,7 @@ var SqliteInstalledPacksRepository = class {
28253
28768
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28254
28769
  this.upsertAvailableStmt.run({
28255
28770
  ...params,
28256
- id: randomUUID3(),
28771
+ id: randomUUID4(),
28257
28772
  recordedBy: meta4?.recordedBy ?? null
28258
28773
  });
28259
28774
  } else {
@@ -28499,9 +29014,65 @@ var SqliteInstalledPacksRepository = class {
28499
29014
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28500
29015
  // caller rather than swallowing them. Each returns whether a row matched, so the
28501
29016
  // caller can tell an edit from a no-such-detection.
29017
+ /**
29018
+ * The rules one installed pack owns, reduced to what a floor computation
29019
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
29020
+ * unreadable contributes no rules to a scan either, so it is not a detection
29021
+ * the control plane can be governing, and an empty list correctly imposes no
29022
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
29023
+ * the user can re-enable, and its assignment stays governed meanwhile.
29024
+ */
29025
+ packFloorRules(namespace, packId) {
29026
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
29027
+ if (!row) return [];
29028
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
29029
+ }
29030
+ /**
29031
+ * What the connected control plane imposes on one installed pack, or null on a
29032
+ * machine that is its own authority (standalone, no cached bundle, or a
29033
+ * repository constructed without a layout base).
29034
+ *
29035
+ * Exposed as a READ so a surface can render the constraint — grey out the
29036
+ * choices below the floor, mark a locked detection as locked — rather than
29037
+ * offer the user a picker whose selections it will then be told it may not
29038
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
29039
+ */
29040
+ policyFloor(namespace, packId) {
29041
+ if (this.baseDir === void 0) return null;
29042
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
29043
+ }
29044
+ /**
29045
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
29046
+ * entry only for a pack the control plane actually governs.
29047
+ *
29048
+ * A surface listing every detection asks per pack, and asking through
29049
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
29050
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
29051
+ * answer, repeated for each row, on every render. This reads all of that once.
29052
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
29053
+ * exactly as the single-pack read returns null for them.
29054
+ */
29055
+ policyFloors(packs2) {
29056
+ const floors = /* @__PURE__ */ new Map();
29057
+ if (this.baseDir === void 0) return floors;
29058
+ const source = openControlPlaneFloors(this.baseDir);
29059
+ if (source === null) return floors;
29060
+ for (const pack of packs2) {
29061
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
29062
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
29063
+ }
29064
+ return floors;
29065
+ }
28502
29066
  /**
28503
29067
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28504
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
29068
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
29069
+ *
29070
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
29071
+ * write below, and a detection the organization has authored a policy for is
29072
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
29073
+ * a throw rather than a silently substituted value. This is the one device-local
29074
+ * write path for the assignment, so the check belongs here rather than on any
29075
+ * surface that offers the choice.
28505
29076
  */
28506
29077
  setPolicy(namespace, packId, policyId) {
28507
29078
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28509,14 +29080,38 @@ var SqliteInstalledPacksRepository = class {
28509
29080
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28510
29081
  );
28511
29082
  }
29083
+ const requested = policyId;
29084
+ const floor = this.policyFloor(namespace, packId);
29085
+ if (floor !== null) {
29086
+ const refusal = policyAssignmentRefusal(requested, floor);
29087
+ if (refusal !== null) {
29088
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
29089
+ }
29090
+ }
28512
29091
  const res = this.db.prepare(
28513
29092
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28514
29093
  WHERE namespace = :namespace AND pack_id = :packId`
28515
29094
  ).run({ policyId, now: Date.now(), namespace, packId });
28516
29095
  return Number(res.changes) > 0;
28517
29096
  }
28518
- /** Enable or disable one installed pack. */
29097
+ /**
29098
+ * Enable or disable one installed pack.
29099
+ *
29100
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29101
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29102
+ * merely another point below the floor, and why re-enabling stays open. Like
29103
+ * the assignment above, the check belongs at this write path rather than on a
29104
+ * surface: this is the one device-local writer of the column, and a refusal
29105
+ * that lived in a page would leave the CLI free.
29106
+ */
28519
29107
  setEnabled(namespace, packId, enabled) {
29108
+ const floor = this.policyFloor(namespace, packId);
29109
+ if (floor !== null) {
29110
+ const refusal = packEnablementRefusal(enabled, floor);
29111
+ if (refusal !== null) {
29112
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29113
+ }
29114
+ }
28520
29115
  const res = this.db.prepare(
28521
29116
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28522
29117
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28602,7 +29197,7 @@ var SqliteInventoryRepository = class {
28602
29197
  };
28603
29198
 
28604
29199
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28605
- import { randomUUID as randomUUID4 } from "crypto";
29200
+ import { randomUUID as randomUUID5 } from "crypto";
28606
29201
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28607
29202
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28608
29203
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -29091,7 +29686,7 @@ var SqliteInventoryAssetsRepository = class {
29091
29686
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
29092
29687
  VALUES (:id, :projectId, :path, :access, :now, :now)
29093
29688
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
29094
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29689
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
29095
29690
  }
29096
29691
  return true;
29097
29692
  }
@@ -29112,7 +29707,7 @@ var SqliteInventoryAssetsRepository = class {
29112
29707
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
29113
29708
  VALUES (:id, :assetId, :trust, :now, :now)
29114
29709
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
29115
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29710
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
29116
29711
  }
29117
29712
  this.configRowsCache = void 0;
29118
29713
  return "ok";
@@ -29409,7 +30004,7 @@ var SqliteInventoryAssetsRepository = class {
29409
30004
  };
29410
30005
 
29411
30006
  // ../../packages/persistence/src/repositories/policies.ts
29412
- import { randomUUID as randomUUID5 } from "crypto";
30007
+ import { randomUUID as randomUUID6 } from "crypto";
29413
30008
  var SqlitePoliciesRepository = class {
29414
30009
  constructor(db) {
29415
30010
  this.db = db;
@@ -29444,7 +30039,7 @@ var SqlitePoliciesRepository = class {
29444
30039
  failOpenTransaction(this.db, () => {
29445
30040
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29446
30041
  stmt.run({
29447
- id: randomUUID5(),
30042
+ id: randomUUID6(),
29448
30043
  target: JSON.stringify({ category }),
29449
30044
  action,
29450
30045
  now: Date.now()
@@ -29464,7 +30059,7 @@ var SqlitePoliciesRepository = class {
29464
30059
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29465
30060
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29466
30061
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29467
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
30062
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29468
30063
  }
29469
30064
  // Caps every global per-category policy currently set to block/redact down
29470
30065
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29532,7 +30127,7 @@ var SqlitePolicyCatalogRepository = class {
29532
30127
  };
29533
30128
 
29534
30129
  // ../../packages/persistence/src/repositories/project-files.ts
29535
- import { randomUUID as randomUUID6 } from "crypto";
30130
+ import { randomUUID as randomUUID7 } from "crypto";
29536
30131
  var SqliteProjectFilesRepository = class {
29537
30132
  constructor(db) {
29538
30133
  this.db = db;
@@ -29564,7 +30159,7 @@ var SqliteProjectFilesRepository = class {
29564
30159
  const stamp = Math.max(now, maxStamp + 1);
29565
30160
  for (const file2 of scan2.files) {
29566
30161
  this.upsertStmt.run({
29567
- id: randomUUID6(),
30162
+ id: randomUUID7(),
29568
30163
  projectId,
29569
30164
  path: file2.path,
29570
30165
  name: file2.name,
@@ -29578,9 +30173,9 @@ var SqliteProjectFilesRepository = class {
29578
30173
  };
29579
30174
 
29580
30175
  // ../../packages/persistence/src/repositories/resolutions.ts
29581
- import { randomUUID as randomUUID7 } from "crypto";
30176
+ import { randomUUID as randomUUID8 } from "crypto";
29582
30177
  var SqliteResolutionsRepository = class {
29583
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30178
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29584
30179
  this.db = db;
29585
30180
  this.now = now;
29586
30181
  this.newId = newId;
@@ -29793,7 +30388,7 @@ var SqliteScanLedgerRepository = class {
29793
30388
  };
29794
30389
 
29795
30390
  // ../../packages/persistence/src/repositories/secret-vault.ts
29796
- import { randomUUID as randomUUID8 } from "crypto";
30391
+ import { randomUUID as randomUUID9 } from "crypto";
29797
30392
  function pageLimit(requested, fallback) {
29798
30393
  if (requested === void 0) return fallback;
29799
30394
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29839,12 +30434,14 @@ var SELECT_COLUMNS = `
29839
30434
  ciphertext,
29840
30435
  nonce,
29841
30436
  auth_tag AS authTag,
30437
+ user_authorized AS userAuthorized,
29842
30438
  occurrence_count AS occurrenceCount,
29843
30439
  first_seen AS firstSeen,
29844
30440
  last_seen AS lastSeen`;
29845
30441
  function toRow(raw) {
29846
- const { provider, ...rest } = raw;
29847
- return provider === null ? rest : { ...rest, provider };
30442
+ const { provider, userAuthorized, ...rest } = raw;
30443
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30444
+ return provider === null ? row : { ...row, provider };
29848
30445
  }
29849
30446
  var SqliteSecretVaultRepository = class {
29850
30447
  constructor(db) {
@@ -29854,17 +30451,18 @@ var SqliteSecretVaultRepository = class {
29854
30451
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29855
30452
  format_version, category, rule_id, masked_match, provider,
29856
30453
  ciphertext, nonce, auth_tag,
29857
- occurrence_count, first_seen, last_seen
30454
+ user_authorized, occurrence_count, first_seen, last_seen
29858
30455
  ) VALUES (
29859
30456
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29860
30457
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29861
30458
  :ciphertext, :nonce, :authTag,
29862
- 1, :now, :now
30459
+ :userAuthorized, 1, :now, :now
29863
30460
  )`
29864
30461
  );
29865
30462
  this.bumpStmt = db.prepare(
29866
30463
  `UPDATE secret_vault
29867
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30464
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30465
+ user_authorized = max(user_authorized, :userAuthorized)
29868
30466
  WHERE value_fingerprint = :valueFingerprint`
29869
30467
  );
29870
30468
  this.byPointerStmt = db.prepare(
@@ -29884,6 +30482,7 @@ var SqliteSecretVaultRepository = class {
29884
30482
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29885
30483
  WHERE pointer_id = :pointerId`
29886
30484
  );
30485
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29887
30486
  this.derefStmt = db.prepare(
29888
30487
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29889
30488
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29897,6 +30496,7 @@ var SqliteSecretVaultRepository = class {
29897
30496
  listStmt;
29898
30497
  replaceCiphertextStmt;
29899
30498
  refreshFingerprintStmt;
30499
+ deleteByPointerStmt;
29900
30500
  derefStmt;
29901
30501
  /**
29902
30502
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29905,6 +30505,11 @@ var SqliteSecretVaultRepository = class {
29905
30505
  * pointer, category and ciphertext, so the same secret always resolves to one
29906
30506
  * wire token. `minted` is true only when this call created the row.
29907
30507
  *
30508
+ * `userAuthorized` is the one field a repeat call may still change, and only
30509
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30510
+ * the row is shared with every automatic path that vaults the same value. See
30511
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30512
+ *
29908
30513
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29909
30514
  * writers cannot both decide they are minting.
29910
30515
  */
@@ -29931,13 +30536,18 @@ var SqliteSecretVaultRepository = class {
29931
30536
  ciphertext: input2.ciphertext,
29932
30537
  nonce: input2.nonce,
29933
30538
  authTag: input2.authTag,
30539
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29934
30540
  now
29935
30541
  })
29936
30542
  );
29937
30543
  minted = true;
29938
30544
  return;
29939
30545
  }
29940
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30546
+ this.bumpStmt.run({
30547
+ valueFingerprint: input2.valueFingerprint,
30548
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30549
+ now
30550
+ });
29941
30551
  },
29942
30552
  "IMMEDIATE"
29943
30553
  );
@@ -29997,6 +30607,42 @@ var SqliteSecretVaultRepository = class {
29997
30607
  );
29998
30608
  return destroyed;
29999
30609
  }
30610
+ /**
30611
+ * Destroy the named entries and report WHICH ones went — the scoped
30612
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30613
+ * values back where they came from. Ids the store does not hold are absent
30614
+ * from the answer rather than an error, so a set assembled from a stale read
30615
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30616
+ * it.
30617
+ *
30618
+ * The ids come back rather than a count because the caller's next act is to
30619
+ * write a purge row per destroyed entry, and a record of destruction has to
30620
+ * be a record of what was really destroyed: a selection is a claim about a
30621
+ * read that has since gone stale, and auditing from it invents a purge for an
30622
+ * entry still sitting in the vault.
30623
+ *
30624
+ * One transaction over the whole set rather than a statement per id: the
30625
+ * caller hands this the result of a restore pass it has completed, and a
30626
+ * fault partway through must leave the vault as it was found rather than
30627
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30628
+ * stands for, so half a delete is not a state anything can recover from.
30629
+ */
30630
+ deleteByPointerIds(pointerIds) {
30631
+ if (pointerIds.length === 0) return [];
30632
+ const deleted = [];
30633
+ withTransaction(
30634
+ this.db,
30635
+ () => {
30636
+ for (const pointerId of pointerIds) {
30637
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30638
+ deleted.push(pointerId);
30639
+ }
30640
+ }
30641
+ },
30642
+ "IMMEDIATE"
30643
+ );
30644
+ return deleted;
30645
+ }
30000
30646
  /**
30001
30647
  * Record (or re-stamp) one place a pointer has been written. One row per
30002
30648
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -30009,7 +30655,7 @@ var SqliteSecretVaultRepository = class {
30009
30655
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
30010
30656
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
30011
30657
  ).run({
30012
- id: randomUUID8(),
30658
+ id: randomUUID9(),
30013
30659
  pointerId: entry.pointerId,
30014
30660
  location: entry.location,
30015
30661
  kind: entry.kind,
@@ -30522,15 +31168,15 @@ var SqliteSecurityRepository = class {
30522
31168
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30523
31169
  const rows = allRows(
30524
31170
  this.db.prepare(
30525
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31171
+ `SELECT e.repo AS repo, count(*) AS c
30526
31172
  FROM inspection_findings f
30527
31173
  JOIN audit_events e ON e.id = f.audit_event_id
30528
31174
  WHERE e.started_at >= :from AND e.started_at < :to
30529
31175
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30530
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30531
- AND json_extract(e.attributes, '$.repo') != ''
30532
- GROUP BY repo
30533
- ORDER BY c DESC, repo
31176
+ AND e.repo IS NOT NULL
31177
+ AND e.repo != ''
31178
+ GROUP BY e.repo
31179
+ ORDER BY c DESC, e.repo
30534
31180
  LIMIT :limit`
30535
31181
  ),
30536
31182
  { from, to: now, limit }
@@ -30592,7 +31238,7 @@ var SqliteSecurityRepository = class {
30592
31238
  `SELECT f.finding_key AS finding_key,
30593
31239
  d.rule_id AS rule_id,
30594
31240
  d.severity AS severity,
30595
- json_extract(e.attributes, '$.file_path') AS path,
31241
+ e.file_path AS path,
30596
31242
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30597
31243
  latest.resolved_at AS latest_resolved_at
30598
31244
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30645,7 +31291,7 @@ var SqliteSecurityRepository = class {
30645
31291
  };
30646
31292
 
30647
31293
  // ../../packages/persistence/src/repositories/shares.ts
30648
- import { randomUUID as randomUUID9 } from "crypto";
31294
+ import { randomUUID as randomUUID10 } from "crypto";
30649
31295
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30650
31296
  var IN_CHUNK = 500;
30651
31297
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30733,7 +31379,7 @@ function buildSummary(dest, endpoints) {
30733
31379
  callSiteCount,
30734
31380
  transports: distinctTransports(transports),
30735
31381
  dataClasses: distinctDataClasses(dataClasses),
30736
- review: buildReviewInfo(dest.trust, transports),
31382
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30737
31383
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30738
31384
  endpoints: endpoints.map(toEndpointSummary)
30739
31385
  };
@@ -30760,7 +31406,7 @@ function buildDetail(dest, endpoints, callSites) {
30760
31406
  lastSeen: new Date(lastSeenMs).toISOString(),
30761
31407
  transports: distinctTransports(transports),
30762
31408
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30763
- review: buildReviewInfo(dest.trust, transports),
31409
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30764
31410
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30765
31411
  note: dest.note,
30766
31412
  endpoints: endpoints.map((ep) => ({
@@ -30789,7 +31435,11 @@ var SqliteSharesRepository = class {
30789
31435
  FROM share_destination d
30790
31436
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30791
31437
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30792
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31438
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31439
+ AND NOT EXISTS (
31440
+ SELECT 1 FROM egress_decision_override o
31441
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31442
+ )`
30793
31443
  );
30794
31444
  const kindCounts = countBy(
30795
31445
  this.db,
@@ -30901,7 +31551,7 @@ var SqliteSharesRepository = class {
30901
31551
  (id, destination_id, host, decision, created_at, updated_at)
30902
31552
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30903
31553
  ).run({
30904
- id: randomUUID9(),
31554
+ id: randomUUID10(),
30905
31555
  destinationId,
30906
31556
  host: dest.host,
30907
31557
  decision,
@@ -31050,7 +31700,7 @@ var SqliteSharesRepository = class {
31050
31700
  let destinationId = destIds.get(hit.host);
31051
31701
  if (destinationId === void 0) {
31052
31702
  destStmt.run({
31053
- id: randomUUID9(),
31703
+ id: randomUUID10(),
31054
31704
  kind: hit.kind,
31055
31705
  name: hit.name,
31056
31706
  host: hit.host,
@@ -31066,7 +31716,7 @@ var SqliteSharesRepository = class {
31066
31716
  let endpointId = endpointIds.get(endpointKey);
31067
31717
  if (endpointId === void 0) {
31068
31718
  endpointStmt.run({
31069
- id: randomUUID9(),
31719
+ id: randomUUID10(),
31070
31720
  destinationId,
31071
31721
  method: hit.method,
31072
31722
  transport: hit.transport,
@@ -31079,7 +31729,7 @@ var SqliteSharesRepository = class {
31079
31729
  endpointIds.set(endpointKey, endpointId);
31080
31730
  }
31081
31731
  siteStmt.run({
31082
- id: randomUUID9(),
31732
+ id: randomUUID10(),
31083
31733
  endpointId,
31084
31734
  project: input2.project,
31085
31735
  projectKey: input2.projectKey,
@@ -31444,6 +32094,7 @@ function purgeSampleData(db) {
31444
32094
  }
31445
32095
 
31446
32096
  // ../../packages/persistence/src/database.ts
32097
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31447
32098
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31448
32099
  "aka.persistence.unsafeTestOnlyRawHandle"
31449
32100
  );
@@ -31491,7 +32142,7 @@ function backupLegacyStore(db, file2) {
31491
32142
  discardStore(file2, backup);
31492
32143
  return backup;
31493
32144
  }
31494
- function openAndInitialize(file2) {
32145
+ function openAndInitialize(file2, base) {
31495
32146
  let db = openWithPragmas(file2);
31496
32147
  try {
31497
32148
  if (isForeignSqliteLineage(db)) {
@@ -31504,7 +32155,7 @@ function openAndInitialize(file2) {
31504
32155
  applyMigrations(db, file2);
31505
32156
  tightenPerms(file2);
31506
32157
  const policies = new SqlitePoliciesRepository(db);
31507
- const installedPacks = new SqliteInstalledPacksRepository(db);
32158
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31508
32159
  const repositories = {
31509
32160
  events: new SqliteEventsRepository(db),
31510
32161
  findings: new SqliteFindingsRepository(db),
@@ -31540,7 +32191,7 @@ function openAndInitialize(file2) {
31540
32191
  }
31541
32192
  function openLocalDatabase(dir) {
31542
32193
  ensureDataDirSync(dir);
31543
- const file2 = join4(dir, DB_FILENAME);
32194
+ const file2 = join7(dir, DB_FILENAME);
31544
32195
  reapStalePartials(file2);
31545
32196
  const {
31546
32197
  db,
@@ -31568,7 +32219,13 @@ function openLocalDatabase(dir) {
31568
32219
  inspectionDefinitions,
31569
32220
  inspectionFindings,
31570
32221
  configInventory
31571
- } = openAndInitialize(file2);
32222
+ } = openAndInitialize(
32223
+ file2,
32224
+ // `dir` is always `<base>/data` — every caller resolves it through
32225
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32226
+ // settings/ and data/, and the pack-policy floor needs both halves.
32227
+ dirname2(dir)
32228
+ );
31572
32229
  function captureRowId(event) {
31573
32230
  return captureId(
31574
32231
  event.metadata?.sessionId ?? null,
@@ -31581,6 +32238,21 @@ function openLocalDatabase(dir) {
31581
32238
  historySync.markSynced([captureRowId(event)], atMs);
31582
32239
  });
31583
32240
  }
32241
+ function markCaptureOwed(event) {
32242
+ failOpenTransaction(db, () => {
32243
+ historySync.markCaptureOwed(captureRowId(event));
32244
+ });
32245
+ }
32246
+ function markAuditEventsDelivered(events2, atMs) {
32247
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32248
+ if (stampable.length === 0) return;
32249
+ failOpenTransaction(db, () => {
32250
+ historySync.markSynced(
32251
+ stampable.map((event) => event.id),
32252
+ atMs
32253
+ );
32254
+ });
32255
+ }
31584
32256
  function recordCapture(event, detected) {
31585
32257
  failOpenTransaction(db, () => {
31586
32258
  const sessionId = event.metadata?.sessionId;
@@ -31667,7 +32339,7 @@ function openLocalDatabase(dir) {
31667
32339
  const definitionId = definitionIds.get(`${finding2.ruleId}@${finding2.version}`);
31668
32340
  if (!definitionId) continue;
31669
32341
  inspectionFindings.insertFinding({
31670
- id: randomUUID10(),
32342
+ id: randomUUID11(),
31671
32343
  auditEventId: record2.scanEvent.id,
31672
32344
  inspectionDefinitionId: definitionId,
31673
32345
  span: finding2.span,
@@ -31763,6 +32435,8 @@ function openLocalDatabase(dir) {
31763
32435
  inspectionFindings,
31764
32436
  recordCapture,
31765
32437
  markCaptureDelivered,
32438
+ markCaptureOwed,
32439
+ markAuditEventsDelivered,
31766
32440
  ensureInventory,
31767
32441
  recordConfigScan,
31768
32442
  recordProjectFiles,
@@ -31781,32 +32455,18 @@ function openLocalDatabase(dir) {
31781
32455
  };
31782
32456
  }
31783
32457
 
31784
- // ../../packages/persistence/src/file-lock.ts
31785
- import { randomUUID as randomUUID11 } from "crypto";
31786
- import {
31787
- closeSync,
31788
- existsSync as existsSync2,
31789
- openSync,
31790
- readFileSync as readFileSync2,
31791
- rmSync as rmSync5,
31792
- statSync as statSync3,
31793
- writeFileSync as writeFileSync2
31794
- } from "fs";
31795
- import { hostname as hostname3 } from "os";
31796
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31797
-
31798
32458
  // ../../packages/persistence/src/finding-key.ts
31799
32459
  import { createHash as createHash3 } from "crypto";
31800
32460
 
31801
32461
  // ../../packages/persistence/src/fingerprint.ts
31802
32462
  import { createHmac, randomBytes } from "crypto";
31803
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31804
- import { join as join5 } from "path";
32463
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32464
+ import { join as join8 } from "path";
31805
32465
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31806
32466
  var EXCEPTION_KEY_FILENAME = "exception.key";
31807
32467
  var KEY_MATERIAL_BYTES = 32;
31808
32468
  function keyFilePath(dataDir2) {
31809
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32469
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31810
32470
  }
31811
32471
  function parseKeyFile(raw) {
31812
32472
  const parsed2 = JSON.parse(raw);
@@ -31829,7 +32489,7 @@ function parseKeyFile(raw) {
31829
32489
  function readFingerprintKey(dataDir2) {
31830
32490
  let raw;
31831
32491
  try {
31832
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32492
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31833
32493
  } catch (err) {
31834
32494
  if (err.code === "ENOENT") return null;
31835
32495
  throw err instanceof Error ? err : new Error(String(err));
@@ -31839,146 +32499,12 @@ function readFingerprintKey(dataDir2) {
31839
32499
 
31840
32500
  // ../../packages/persistence/src/history-preview.ts
31841
32501
  import { existsSync as existsSync4 } from "fs";
31842
- import { join as join6 } from "path";
32502
+ import { join as join9 } from "path";
31843
32503
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31844
32504
 
31845
- // ../../packages/persistence/src/local-layout.ts
31846
- import { renameSync as renameSync3 } from "fs";
31847
- import { mkdir } from "fs/promises";
31848
- import { homedir } from "os";
31849
- import { join as join7 } from "path";
31850
- function defaultDataDir() {
31851
- return join7(homedir(), ".aka");
31852
- }
31853
- function settingsDir(base = defaultDataDir()) {
31854
- return join7(base, "settings");
31855
- }
31856
- function dataDir(base = defaultDataDir()) {
31857
- return join7(base, "data");
31858
- }
31859
- function dbPath(base = defaultDataDir()) {
31860
- return join7(dataDir(base), "aka.db");
31861
- }
31862
- function keysDir(base = defaultDataDir()) {
31863
- return join7(base, "keys");
31864
- }
31865
- async function ensureDataDir(dir = defaultDataDir()) {
31866
- await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
31867
- tightenDir(dir);
31868
- }
31869
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31870
- ensureDataDirSync(dir);
31871
- }
31872
- function migrateLegacyLayout(base = defaultDataDir()) {
31873
- const moves = [
31874
- { name: "config.json", dest: settingsDir(base) },
31875
- { name: "policy-cache.json", dest: dataDir(base) }
31876
- ];
31877
- for (const { name, dest } of moves) {
31878
- try {
31879
- ensureDataDirSync(dest);
31880
- const moved = join7(dest, name);
31881
- renameSync3(join7(base, name), moved);
31882
- tightenFile(moved);
31883
- } catch {
31884
- }
31885
- }
31886
- }
31887
-
31888
- // ../../packages/persistence/src/managed-settings.ts
31889
- import { readFileSync as readFileSync4 } from "fs";
31890
- import { posix, win32 } from "path";
31891
- function managedSettingsPaths(platform2 = process.platform) {
31892
- if (platform2 === "darwin") {
31893
- return [
31894
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31895
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31896
- ];
31897
- }
31898
- if (platform2 === "win32") {
31899
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31900
- }
31901
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31902
- }
31903
- function readManagedSettings(paths = managedSettingsPaths()) {
31904
- for (const path of paths) {
31905
- let text;
31906
- try {
31907
- text = readFileSync4(path, "utf8");
31908
- } catch {
31909
- continue;
31910
- }
31911
- const record2 = parseJsonObject(text);
31912
- if (!record2) continue;
31913
- const parsed2 = ManagedSettings.safeParse(record2);
31914
- if (parsed2.success) return parsed2.data;
31915
- }
31916
- return null;
31917
- }
31918
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31919
- if (!managed) return settings;
31920
- const { values } = managed;
31921
- const merged = { ...settings };
31922
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31923
- if (values.controlPlane !== void 0) {
31924
- merged.controlPlane = {
31925
- ...values.controlPlane,
31926
- // The administrator pinned WHICH deployment, not WHEN this machine
31927
- // joined it. Keep the user's own attach time when the endpoint is
31928
- // unchanged, so a managed machine does not appear to re-attach on every
31929
- // read; stamp a fresh one when the administrator moved it.
31930
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31931
- };
31932
- }
31933
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31934
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31935
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31936
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31937
- if (values.vaultConsent !== void 0) {
31938
- merged.vaultConsent = values.vaultConsent ? (
31939
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31940
- // at the current version otherwise.
31941
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31942
- ) : void 0;
31943
- }
31944
- if (values.modelJudgeConsent !== void 0) {
31945
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31946
- acknowledgedAt: now().toISOString(),
31947
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31948
- } : void 0;
31949
- }
31950
- return merged;
31951
- }
31952
-
31953
- // ../../packages/persistence/src/settings.ts
31954
- import { readFileSync as readFileSync5 } from "fs";
31955
- import { join as join8 } from "path";
31956
- var SETTINGS_FILENAME = "settings.json";
31957
- function readWorkspaceSettings(base = defaultDataDir()) {
31958
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31959
- }
31960
- function readUserSettings(base) {
31961
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31962
- if (!record2) return defaultWorkspaceSettings();
31963
- try {
31964
- return WorkspaceSettings.parse(record2);
31965
- } catch {
31966
- return defaultWorkspaceSettings();
31967
- }
31968
- }
31969
- function readJson(file2) {
31970
- let text;
31971
- try {
31972
- text = readFileSync5(file2, "utf8");
31973
- } catch {
31974
- return null;
31975
- }
31976
- return parseJsonObject(text) ?? null;
31977
- }
31978
-
31979
32505
  // ../../packages/persistence/src/store-symlinks.ts
31980
32506
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31981
- import { dirname as dirname2, join as join9, resolve } from "path";
32507
+ import { dirname as dirname3, join as join10, resolve } from "path";
31982
32508
  var STORE_DB = "the store database (including the prompt corpus)";
31983
32509
  var STORE_SETTINGS = "your settings file";
31984
32510
  function storeContents(home) {
@@ -31987,7 +32513,7 @@ function storeContents(home) {
31987
32513
  [settingsDir(home), STORE_SETTINGS],
31988
32514
  [dataDir(home), STORE_DB],
31989
32515
  [keysDir(home), "the vault key"],
31990
- [join9(settingsDir(home), "settings.json"), STORE_SETTINGS],
32516
+ [join10(settingsDir(home), "settings.json"), STORE_SETTINGS],
31991
32517
  [dbPath(home), STORE_DB]
31992
32518
  ]);
31993
32519
  }
@@ -32015,7 +32541,7 @@ function linkTarget(path) {
32015
32541
  try {
32016
32542
  return realpathSync(path);
32017
32543
  } catch {
32018
- return resolve(dirname2(path), readlinkSync(path));
32544
+ return resolve(dirname3(path), readlinkSync(path));
32019
32545
  }
32020
32546
  }
32021
32547
  function targetMode(path, platform2) {
@@ -32039,19 +32565,19 @@ import {
32039
32565
  // ../../packages/persistence/src/vault/key-provider.ts
32040
32566
  import { execFileSync } from "child_process";
32041
32567
  import { randomBytes as randomBytes2 } from "crypto";
32042
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32043
- import { join as join10 } from "path";
32568
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32569
+ import { join as join11 } from "path";
32044
32570
 
32045
32571
  // ../../packages/persistence/src/vault/vault.ts
32046
32572
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32047
32573
 
32048
32574
  // ../../packages/persistence/src/warn-era-cap.ts
32049
32575
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32050
- import { join as join11 } from "path";
32576
+ import { join as join12 } from "path";
32051
32577
  var MARKER = "warn-era-capped";
32052
32578
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32053
32579
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32054
- const marker = join11(dataDir2, MARKER);
32580
+ const marker = join12(dataDir2, MARKER);
32055
32581
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32056
32582
  const capped = db.policies.capCategoryActions();
32057
32583
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32095,6 +32621,307 @@ function toEgressIngestRequest(input2) {
32095
32621
  };
32096
32622
  }
32097
32623
 
32624
+ // ../../packages/remote/src/http.ts
32625
+ import { request as httpRequest } from "http";
32626
+ import { request as httpsRequest } from "https";
32627
+ var DEFAULT_TIMEOUT_MS = 1e4;
32628
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
32629
+ var RemoteRequestError = class extends Error {
32630
+ constructor(status) {
32631
+ super(`control-plane request failed with status ${String(status)}`);
32632
+ this.status = status;
32633
+ this.name = "RemoteRequestError";
32634
+ }
32635
+ status;
32636
+ };
32637
+ var RemoteRouteAbsent = class extends Error {
32638
+ constructor(route) {
32639
+ super(`control plane does not serve ${route}`);
32640
+ this.route = route;
32641
+ this.name = "RemoteRouteAbsent";
32642
+ }
32643
+ route;
32644
+ };
32645
+ var RemoteRequestInvalid = class extends Error {
32646
+ constructor(route, cause) {
32647
+ super(`refusing to send a malformed body to ${route}`);
32648
+ this.cause = cause;
32649
+ this.name = "RemoteRequestInvalid";
32650
+ }
32651
+ cause;
32652
+ };
32653
+ var RemoteResponseInvalid = class extends Error {
32654
+ constructor(route, detail) {
32655
+ super(`control plane answered ${route} with ${detail}`);
32656
+ this.name = "RemoteResponseInvalid";
32657
+ }
32658
+ };
32659
+ var RemoteTransportError = class extends Error {
32660
+ /**
32661
+ * The status the peer sent, when headers arrived and only the BODY was
32662
+ * refused.
32663
+ *
32664
+ * Undefined for the ordinary case this class was written for — no answer at
32665
+ * all. It exists because two paths reject after a status has already been
32666
+ * delivered: an oversized body and an aborted response. Discarding it there
32667
+ * reported a deployment answering 401 with a verbose body as a network
32668
+ * outage, which sends the reader to look at their network instead of their
32669
+ * credential.
32670
+ */
32671
+ constructor(reason, status) {
32672
+ super(`control-plane request did not complete: ${reason}`);
32673
+ this.status = status;
32674
+ this.name = "RemoteTransportError";
32675
+ }
32676
+ status;
32677
+ };
32678
+ async function send(options) {
32679
+ const url2 = new URL(options.url);
32680
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32681
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
32682
+ const requestOptions = {
32683
+ method: options.method,
32684
+ headers: {
32685
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
32686
+ // last they win, and two of the values below are ones no caller may
32687
+ // replace: `x-api-key` is the credential, and `content-length` is the
32688
+ // byte count that stops a multi-byte body being truncated by the
32689
+ // receiver. `SendOptions.headers` is a free-form record on an exported
32690
+ // function, so "no caller does that today" is not the guarantee to rely
32691
+ // on. The one header any caller actually passes — `if-none-match` on the
32692
+ // conditional GET — is untouched by this order.
32693
+ ...options.headers,
32694
+ // The credential. One header, matching what the deployment authenticates
32695
+ // on; a second copy in an `Authorization` header would be one more place
32696
+ // it can be logged by an intermediary for no gain.
32697
+ //
32698
+ // Spread conditionally rather than assigned as `undefined`: Node's header
32699
+ // handling and `content-length` bookkeeping treat a present-but-undefined
32700
+ // key differently from an absent one, and "the header is not there" is
32701
+ // the property the attach flow needs.
32702
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
32703
+ accept: "application/json",
32704
+ ...options.body === void 0 ? {} : {
32705
+ "content-type": "application/json",
32706
+ // Byte length, not string length: a multi-byte body sent with a
32707
+ // character count is truncated by the receiver.
32708
+ "content-length": String(Buffer.byteLength(options.body))
32709
+ }
32710
+ }
32711
+ };
32712
+ return new Promise((resolve2, reject) => {
32713
+ let settled = false;
32714
+ const fail = (reason, status) => {
32715
+ if (settled) return;
32716
+ settled = true;
32717
+ reject(new RemoteTransportError(reason, status));
32718
+ };
32719
+ const req = send_(url2, requestOptions, (res) => {
32720
+ const chunks = [];
32721
+ let size = 0;
32722
+ res.on("data", (chunk) => {
32723
+ size += chunk.length;
32724
+ if (size > MAX_RESPONSE_BYTES) {
32725
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
32726
+ res.destroy();
32727
+ req.destroy();
32728
+ return;
32729
+ }
32730
+ chunks.push(chunk);
32731
+ });
32732
+ res.on("aborted", () => {
32733
+ fail("the response was aborted", res.statusCode);
32734
+ });
32735
+ res.on("end", () => {
32736
+ if (settled) return;
32737
+ settled = true;
32738
+ resolve2({
32739
+ status: res.statusCode ?? 0,
32740
+ headers: res.headers,
32741
+ body: Buffer.concat(chunks).toString("utf8")
32742
+ });
32743
+ });
32744
+ });
32745
+ const deadline = setTimeout(() => {
32746
+ fail(`no response within ${String(timeoutMs)}ms`);
32747
+ req.destroy();
32748
+ }, timeoutMs);
32749
+ deadline.unref();
32750
+ req.on("upgrade", (_res, socket) => {
32751
+ fail("the deployment answered with a protocol upgrade");
32752
+ socket.destroy();
32753
+ });
32754
+ req.on("close", () => {
32755
+ fail("the connection closed before a response was read");
32756
+ clearTimeout(deadline);
32757
+ });
32758
+ req.on("error", (err) => {
32759
+ fail(err.message);
32760
+ });
32761
+ if (options.body !== void 0) req.write(options.body);
32762
+ req.end();
32763
+ });
32764
+ }
32765
+
32766
+ // ../../packages/remote/src/client.ts
32767
+ var ROUTES = {
32768
+ events: "/v1/events",
32769
+ auditEvents: "/v1/audit-events",
32770
+ auditEventsBatch: "/v1/audit-events/batch",
32771
+ inventory: "/v1/inventory",
32772
+ storePosture: "/v1/store-posture",
32773
+ policyBundle: "/v1/policy-bundle",
32774
+ whoami: "/v1/plugin/whoami",
32775
+ shares: "/v1/shares",
32776
+ commands: "/v1/plugin/commands"
32777
+ };
32778
+ function ackRoute(id) {
32779
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
32780
+ }
32781
+ function headerValue(response, name) {
32782
+ const raw = response.headers[name];
32783
+ if (raw === void 0) return void 0;
32784
+ return Array.isArray(raw) ? raw[0] : raw;
32785
+ }
32786
+ function okBody(response) {
32787
+ if (response.status < 200 || response.status >= 300) {
32788
+ throw new RemoteRequestError(response.status);
32789
+ }
32790
+ return response.body;
32791
+ }
32792
+ function parsed(schema, body, route) {
32793
+ let json2;
32794
+ try {
32795
+ json2 = JSON.parse(body);
32796
+ } catch {
32797
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
32798
+ }
32799
+ const result = schema.safeParse(json2);
32800
+ if (!result.success) {
32801
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
32802
+ }
32803
+ return result.data;
32804
+ }
32805
+ function withoutTrailingSlashes(endpoint) {
32806
+ let end = endpoint.length;
32807
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
32808
+ return endpoint.slice(0, end);
32809
+ }
32810
+ var SLASH = "/".charCodeAt(0);
32811
+ function createRemoteClient(options) {
32812
+ const base = withoutTrailingSlashes(options.endpoint);
32813
+ const url2 = (route) => `${base}${route}`;
32814
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
32815
+ const sendOne = async (event) => {
32816
+ const validated = RecordAuditEventRequest.safeParse(event);
32817
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
32818
+ const response = await send({
32819
+ ...common,
32820
+ method: "POST",
32821
+ url: url2(ROUTES.auditEvents),
32822
+ body: JSON.stringify(validated.data)
32823
+ });
32824
+ okBody(response);
32825
+ };
32826
+ return {
32827
+ async ingestEvents(batch) {
32828
+ const response = await send({
32829
+ ...common,
32830
+ method: "POST",
32831
+ url: url2(ROUTES.events),
32832
+ body: JSON.stringify(batch)
32833
+ });
32834
+ return parsed(IngestAck, okBody(response), ROUTES.events);
32835
+ },
32836
+ async ingestInventory(context) {
32837
+ const response = await send({
32838
+ ...common,
32839
+ method: "POST",
32840
+ url: url2(ROUTES.inventory),
32841
+ body: JSON.stringify(context)
32842
+ });
32843
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
32844
+ },
32845
+ async recordAuditEvent(event) {
32846
+ await sendOne(event);
32847
+ },
32848
+ async recordAuditEvents(events, opts) {
32849
+ const validated = RecordAuditEventBatch.safeParse({ events });
32850
+ if (!validated.success) {
32851
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
32852
+ }
32853
+ const response = await send({
32854
+ ...common,
32855
+ method: "POST",
32856
+ url: url2(ROUTES.auditEventsBatch),
32857
+ body: JSON.stringify(validated.data)
32858
+ });
32859
+ if (response.status === 404) {
32860
+ if (opts?.fallbackToSingleEvents !== true) {
32861
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
32862
+ }
32863
+ for (const event of validated.data.events) await sendOne(event);
32864
+ return { accepted: validated.data.events.length };
32865
+ }
32866
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
32867
+ },
32868
+ async reportStorePosture(snapshot) {
32869
+ const response = await send({
32870
+ ...common,
32871
+ method: "POST",
32872
+ url: url2(ROUTES.storePosture),
32873
+ body: JSON.stringify(snapshot)
32874
+ });
32875
+ okBody(response);
32876
+ },
32877
+ async getPolicyBundle(etag) {
32878
+ const response = await send({
32879
+ ...common,
32880
+ method: "GET",
32881
+ url: url2(ROUTES.policyBundle),
32882
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
32883
+ });
32884
+ if (response.status === 304) {
32885
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
32886
+ }
32887
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
32888
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
32889
+ },
32890
+ async whoami() {
32891
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
32892
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
32893
+ },
32894
+ async recordProjectEgress(request) {
32895
+ const validated = EgressIngestRequest.safeParse(request);
32896
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
32897
+ const response = await send({
32898
+ ...common,
32899
+ method: "POST",
32900
+ url: url2(ROUTES.shares),
32901
+ body: JSON.stringify(validated.data)
32902
+ });
32903
+ okBody(response);
32904
+ },
32905
+ async pollCommand() {
32906
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
32907
+ if (response.status === 404) return null;
32908
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
32909
+ },
32910
+ async ackCommand(id, body) {
32911
+ const validated = DeviceCommandAckBody.safeParse(body);
32912
+ const route = ackRoute(id);
32913
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
32914
+ const response = await send({
32915
+ ...common,
32916
+ method: "POST",
32917
+ url: url2(route),
32918
+ body: JSON.stringify(validated.data)
32919
+ });
32920
+ okBody(response);
32921
+ }
32922
+ };
32923
+ }
32924
+
32098
32925
  // ../../packages/plugin-runtime/src/attached/failure.ts
32099
32926
  function statusOf(err) {
32100
32927
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -32113,12 +32940,27 @@ function classifyFailure(err) {
32113
32940
  }
32114
32941
  }
32115
32942
 
32943
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
32944
+ var REQUEST_TIMEOUT_MS = 2e3;
32945
+ function withTimeout(promise2, ms) {
32946
+ let timer;
32947
+ const timeout = new Promise((_, reject) => {
32948
+ timer = setTimeout(() => {
32949
+ reject(new Error("attached gateway request timed out"));
32950
+ }, ms);
32951
+ });
32952
+ promise2.catch(() => void 0);
32953
+ return Promise.race([promise2, timeout]).finally(() => {
32954
+ clearTimeout(timer);
32955
+ });
32956
+ }
32957
+
32116
32958
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
32117
- import { readFileSync as readFileSync7 } from "fs";
32118
- import { join as join12 } from "path";
32959
+ import { readFileSync as readFileSync8 } from "fs";
32960
+ import { join as join13 } from "path";
32119
32961
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
32120
32962
  function forwardDropsPath(dataDir2) {
32121
- return join12(dataDir2, FORWARD_DROPS_FILENAME);
32963
+ return join13(dataDir2, FORWARD_DROPS_FILENAME);
32122
32964
  }
32123
32965
  function recordForwardDrops(dataDir2, count, nowMs) {
32124
32966
  if (count <= 0) return;
@@ -32136,7 +32978,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
32136
32978
  }
32137
32979
  function readForwardDrops(dataDir2) {
32138
32980
  try {
32139
- const parsed2 = JSON.parse(readFileSync7(forwardDropsPath(dataDir2), "utf8"));
32981
+ const parsed2 = JSON.parse(readFileSync8(forwardDropsPath(dataDir2), "utf8"));
32140
32982
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
32141
32983
  const record2 = parsed2;
32142
32984
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -32154,13 +32996,13 @@ function readForwardDrops(dataDir2) {
32154
32996
 
32155
32997
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
32156
32998
  import { randomUUID as randomUUID15 } from "crypto";
32157
- import { readFileSync as readFileSync13 } from "fs";
32999
+ import { readFileSync as readFileSync14 } from "fs";
32158
33000
  import { readFile, rename, writeFile } from "fs/promises";
32159
- import { join as join21 } from "path";
33001
+ import { join as join22 } from "path";
32160
33002
 
32161
33003
  // ../../packages/plugin-sdk/src/config.ts
32162
33004
  import { existsSync as existsSync7 } from "fs";
32163
- import { join as join13 } from "path";
33005
+ import { join as join14 } from "path";
32164
33006
 
32165
33007
  // ../../packages/plugin-sdk/src/provider-env.ts
32166
33008
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32214,7 +33056,7 @@ function resolveProvider() {
32214
33056
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32215
33057
  try {
32216
33058
  ensureLayoutDirSync(base);
32217
- const settingsFile = join13(settingsDir(base), "settings.json");
33059
+ const settingsFile = join14(settingsDir(base), "settings.json");
32218
33060
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
32219
33061
  } catch {
32220
33062
  }
@@ -32238,9 +33080,9 @@ function resolveProviderSafe(resolveProviderFn) {
32238
33080
  }
32239
33081
 
32240
33082
  // ../../packages/plugin-sdk/src/config-inventory.ts
32241
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33083
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32242
33084
  import { homedir as homedir2 } from "os";
32243
- import { basename as basename4, join as join15 } from "path";
33085
+ import { basename as basename4, join as join16 } from "path";
32244
33086
 
32245
33087
  // ../../packages/detections/src/egress/registry.ts
32246
33088
  var EXTRACTOR_VERSION = "1";
@@ -35369,8 +36211,8 @@ function maskText(text) {
35369
36211
  }
35370
36212
 
35371
36213
  // ../../packages/plugin-sdk/src/repo.ts
35372
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
35373
- import { basename as basename3, dirname as dirname3, isAbsolute, join as join14, sep as sep2 } from "path";
36214
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
36215
+ import { basename as basename3, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
35374
36216
  function resolveRepoIdentity(cwd) {
35375
36217
  try {
35376
36218
  const root = findGitRoot(cwd);
@@ -35420,7 +36262,7 @@ function resolveGitBranch(cwd) {
35420
36262
  try {
35421
36263
  const root = findGitRoot(cwd);
35422
36264
  if (!root) return void 0;
35423
- const dotGit = join14(root, ".git");
36265
+ const dotGit = join15(root, ".git");
35424
36266
  let gitdir;
35425
36267
  try {
35426
36268
  gitdir = statSync6(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
@@ -35428,7 +36270,7 @@ function resolveGitBranch(cwd) {
35428
36270
  return void 0;
35429
36271
  }
35430
36272
  if (gitdir === void 0) return void 0;
35431
- const head = safeRead(join14(gitdir, "HEAD"));
36273
+ const head = safeRead(join15(gitdir, "HEAD"));
35432
36274
  if (!head) return void 0;
35433
36275
  return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
35434
36276
  } catch {
@@ -35438,41 +36280,41 @@ function resolveGitBranch(cwd) {
35438
36280
  function resolveWorktreeGitdir(root, dotGitFile) {
35439
36281
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
35440
36282
  if (!target) return void 0;
35441
- return isAbsolute(target) ? target : join14(root, target);
36283
+ return isAbsolute(target) ? target : join15(root, target);
35442
36284
  }
35443
36285
  function findGitRoot(start) {
35444
36286
  let dir = start;
35445
36287
  for (; ; ) {
35446
- if (existsSync8(join14(dir, ".git"))) return dir;
35447
- const parent = dirname3(dir);
36288
+ if (existsSync8(join15(dir, ".git"))) return dir;
36289
+ const parent = dirname4(dir);
35448
36290
  if (parent === dir) return void 0;
35449
36291
  dir = parent;
35450
36292
  }
35451
36293
  }
35452
36294
  function resolveGitContext(root) {
35453
- const dotGit = join14(root, ".git");
36295
+ const dotGit = join15(root, ".git");
35454
36296
  try {
35455
36297
  if (statSync6(dotGit).isDirectory()) {
35456
- return { configPath: join14(dotGit, "config"), headRoot: root };
36298
+ return { configPath: join15(dotGit, "config"), headRoot: root };
35457
36299
  }
35458
36300
  } catch {
35459
36301
  return void 0;
35460
36302
  }
35461
36303
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
35462
36304
  if (!target) return void 0;
35463
- const gitdir = isAbsolute(target) ? target : join14(root, target);
35464
- if (existsSync8(join14(gitdir, "config"))) {
35465
- return { configPath: join14(gitdir, "config"), headRoot: root };
36305
+ const gitdir = isAbsolute(target) ? target : join15(root, target);
36306
+ if (existsSync8(join15(gitdir, "config"))) {
36307
+ return { configPath: join15(gitdir, "config"), headRoot: root };
35466
36308
  }
35467
- const commonRaw = safeRead(join14(gitdir, "commondir"))?.trim();
36309
+ const commonRaw = safeRead(join15(gitdir, "commondir"))?.trim();
35468
36310
  if (!commonRaw) return void 0;
35469
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join14(gitdir, commonRaw);
35470
- const headRoot = basename3(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
35471
- return { configPath: join14(commonGitDir, "config"), headRoot };
36311
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join15(gitdir, commonRaw);
36312
+ const headRoot = basename3(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
36313
+ return { configPath: join15(commonGitDir, "config"), headRoot };
35472
36314
  }
35473
36315
  function safeRead(path) {
35474
36316
  try {
35475
- return readFileSync8(path, "utf8");
36317
+ return readFileSync9(path, "utf8");
35476
36318
  } catch {
35477
36319
  return void 0;
35478
36320
  }
@@ -35534,31 +36376,31 @@ function resolveConfigInventory(input2) {
35534
36376
  };
35535
36377
  try {
35536
36378
  const home = input2.homeDir ?? homedir2();
35537
- const claudeDir = join15(home, ".claude");
36379
+ const claudeDir = join16(home, ".claude");
35538
36380
  const repo = resolveRepoIdentity(input2.cwd);
35539
36381
  const repoIdentity = repo?.url ?? input2.cwd;
35540
36382
  const projectSource = `project:${repoIdentity}`;
35541
- collectSettingsHooks(scan2, join15(claudeDir, "settings.json"), "user");
35542
- collectSettingsHooks(scan2, join15(input2.cwd, ".claude", "settings.json"), "project");
35543
- collectSettingsHooks(scan2, join15(input2.cwd, ".claude", "settings.local.json"), "local");
36383
+ collectSettingsHooks(scan2, join16(claudeDir, "settings.json"), "user");
36384
+ collectSettingsHooks(scan2, join16(input2.cwd, ".claude", "settings.json"), "project");
36385
+ collectSettingsHooks(scan2, join16(input2.cwd, ".claude", "settings.local.json"), "local");
35544
36386
  const projectOrigin = { scope: "project", project: repoIdentity };
35545
- collectMcpFile(scan2, join15(input2.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
35546
- collectUserClaudeJson(scan2, join15(home, ".claude.json"), input2.cwd, repoIdentity);
35547
- collectMcpFile(scan2, join15(claudeDir, "settings.json"), { scope: "user" });
35548
- collectMcpFile(scan2, join15(input2.cwd, ".claude", "settings.json"), projectOrigin);
35549
- collectMcpFile(scan2, join15(input2.cwd, ".claude", "settings.local.json"), {
36387
+ collectMcpFile(scan2, join16(input2.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
36388
+ collectUserClaudeJson(scan2, join16(home, ".claude.json"), input2.cwd, repoIdentity);
36389
+ collectMcpFile(scan2, join16(claudeDir, "settings.json"), { scope: "user" });
36390
+ collectMcpFile(scan2, join16(input2.cwd, ".claude", "settings.json"), projectOrigin);
36391
+ collectMcpFile(scan2, join16(input2.cwd, ".claude", "settings.local.json"), {
35550
36392
  scope: "local",
35551
36393
  project: repoIdentity
35552
36394
  });
35553
36395
  collectConfigFiles(scan2, claudeDir, input2.cwd);
35554
- collectSkillsDir(scan2, join15(claudeDir, "skills"), { source: "local", scope: "user" });
35555
- collectSkillsDir(scan2, join15(input2.cwd, ".claude", "skills"), {
36396
+ collectSkillsDir(scan2, join16(claudeDir, "skills"), { source: "local", scope: "user" });
36397
+ collectSkillsDir(scan2, join16(input2.cwd, ".claude", "skills"), {
35556
36398
  source: projectSource,
35557
36399
  scope: "project"
35558
36400
  });
35559
36401
  collectInstalledPlugins(scan2, claudeDir);
35560
36402
  collectMarketplaceSkills(scan2, claudeDir);
35561
- collectSkillsDir(scan2, join15(input2.cwd, "skills"), { source: projectSource, scope: "project" });
36403
+ collectSkillsDir(scan2, join16(input2.cwd, "skills"), { source: projectSource, scope: "project" });
35562
36404
  scan2.skills = dedupeSkills(scan2.skills);
35563
36405
  scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
35564
36406
  } catch (err) {
@@ -35687,7 +36529,7 @@ function projectEntryFor(projects, cwd) {
35687
36529
  return void 0;
35688
36530
  }
35689
36531
  function collectPluginManifestMcp(scan2, installPath, origin) {
35690
- const manifestPath = join15(installPath, ".claude-plugin", "plugin.json");
36532
+ const manifestPath = join16(installPath, ".claude-plugin", "plugin.json");
35691
36533
  const raw = readOptional(manifestPath);
35692
36534
  if (raw === void 0) return;
35693
36535
  try {
@@ -35695,7 +36537,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
35695
36537
  if (typeof parsed2 !== "object" || parsed2 === null) return;
35696
36538
  const declared = parsed2.mcpServers;
35697
36539
  if (typeof declared === "string" && declared.length > 0) {
35698
- collectMcpFile(scan2, join15(installPath, declared), origin, { recordErrors: true });
36540
+ collectMcpFile(scan2, join16(installPath, declared), origin, { recordErrors: true });
35699
36541
  } else {
35700
36542
  collectMcpObject(scan2, declared, manifestPath, origin);
35701
36543
  }
@@ -35712,14 +36554,14 @@ var SETTINGS_KEY_LABELS = [
35712
36554
  ["statusLine", "status line"]
35713
36555
  ];
35714
36556
  function collectConfigFiles(scan2, claudeDir, cwd) {
35715
- settingsConfigFile(scan2, join15(claudeDir, "settings.json"), "user", "User settings");
35716
- settingsConfigFile(scan2, join15(cwd, ".claude", "settings.json"), "project", "Project settings");
35717
- settingsConfigFile(scan2, join15(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
35718
- memoryConfigFile(scan2, join15(claudeDir, "CLAUDE.md"), "user", "User memory");
35719
- memoryConfigFile(scan2, join15(cwd, "CLAUDE.md"), "project", "Project memory");
35720
- mcpJsonConfigFile(scan2, join15(cwd, ".mcp.json"));
35721
- dirConfigFile(scan2, join15(cwd, ".claude", "commands"), "Slash commands", "command");
35722
- dirConfigFile(scan2, join15(cwd, ".claude", "agents"), "Subagents", "subagent");
36557
+ settingsConfigFile(scan2, join16(claudeDir, "settings.json"), "user", "User settings");
36558
+ settingsConfigFile(scan2, join16(cwd, ".claude", "settings.json"), "project", "Project settings");
36559
+ settingsConfigFile(scan2, join16(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
36560
+ memoryConfigFile(scan2, join16(claudeDir, "CLAUDE.md"), "user", "User memory");
36561
+ memoryConfigFile(scan2, join16(cwd, "CLAUDE.md"), "project", "Project memory");
36562
+ mcpJsonConfigFile(scan2, join16(cwd, ".mcp.json"));
36563
+ dirConfigFile(scan2, join16(cwd, ".claude", "commands"), "Slash commands", "command");
36564
+ dirConfigFile(scan2, join16(cwd, ".claude", "agents"), "Subagents", "subagent");
35723
36565
  }
35724
36566
  function configFileEntry(path, scope, kind) {
35725
36567
  try {
@@ -35794,7 +36636,7 @@ function countMarkdownFiles(dir, depth) {
35794
36636
  let count = 0;
35795
36637
  for (const dirent of readdirSync2(dir, { withFileTypes: true })) {
35796
36638
  if (dirent.name.startsWith(".")) continue;
35797
- if (dirent.isDirectory()) count += countMarkdownFiles(join15(dir, dirent.name), depth + 1);
36639
+ if (dirent.isDirectory()) count += countMarkdownFiles(join16(dir, dirent.name), depth + 1);
35798
36640
  else if (dirent.name.endsWith(".md")) count += 1;
35799
36641
  }
35800
36642
  return count;
@@ -35807,7 +36649,7 @@ function collectSkillsDir(scan2, dir, origin) {
35807
36649
  return;
35808
36650
  }
35809
36651
  for (const name of names) {
35810
- const skillFile = join15(dir, name, "SKILL.md");
36652
+ const skillFile = join16(dir, name, "SKILL.md");
35811
36653
  try {
35812
36654
  const raw = readOptional(skillFile);
35813
36655
  if (raw === void 0) continue;
@@ -35816,7 +36658,7 @@ function collectSkillsDir(scan2, dir, origin) {
35816
36658
  name: front.name ?? name,
35817
36659
  source: origin.source,
35818
36660
  scope: origin.scope,
35819
- location: join15(dir, name),
36661
+ location: join16(dir, name),
35820
36662
  updatedAt: statSync7(skillFile).mtime.toISOString()
35821
36663
  };
35822
36664
  const version2 = front.version ?? origin.defaultVersion;
@@ -35847,7 +36689,7 @@ function parseFrontmatter(raw) {
35847
36689
  return out;
35848
36690
  }
35849
36691
  function collectInstalledPlugins(scan2, claudeDir) {
35850
- const manifestPath = join15(claudeDir, "plugins", "installed_plugins.json");
36692
+ const manifestPath = join16(claudeDir, "plugins", "installed_plugins.json");
35851
36693
  const raw = readOptional(manifestPath);
35852
36694
  if (raw === void 0) return;
35853
36695
  let plugins;
@@ -35872,7 +36714,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
35872
36714
  if (typeof installPath !== "string" || seen.has(installPath)) continue;
35873
36715
  seen.add(installPath);
35874
36716
  const version2 = install.version;
35875
- const hooksPath = join15(installPath, "hooks", "hooks.json");
36717
+ const hooksPath = join16(installPath, "hooks", "hooks.json");
35876
36718
  const hooksRaw = readOptional(hooksPath);
35877
36719
  if (hooksRaw !== void 0) {
35878
36720
  try {
@@ -35892,22 +36734,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
35892
36734
  }
35893
36735
  const origin = { source: marketplace, scope: "plugin", pluginName };
35894
36736
  if (typeof version2 === "string") origin.defaultVersion = version2;
35895
- collectSkillsDir(scan2, join15(installPath, "skills"), origin);
36737
+ collectSkillsDir(scan2, join16(installPath, "skills"), origin);
35896
36738
  const mcpOrigin = { scope: "plugin", pluginName, marketplace };
35897
- collectMcpFile(scan2, join15(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
36739
+ collectMcpFile(scan2, join16(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
35898
36740
  collectPluginManifestMcp(scan2, installPath, mcpOrigin);
35899
36741
  }
35900
36742
  }
35901
36743
  }
35902
36744
  function collectMarketplaceSkills(scan2, claudeDir) {
35903
- for (const mp of readMarketplaces(join15(claudeDir, "plugins", "known_marketplaces.json"))) {
36745
+ for (const mp of readMarketplaces(join16(claudeDir, "plugins", "known_marketplaces.json"))) {
35904
36746
  if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
35905
- collectSkillsDir(scan2, join15(mp.installLocation, "skills"), {
36747
+ collectSkillsDir(scan2, join16(mp.installLocation, "skills"), {
35906
36748
  source: mp.name,
35907
36749
  scope: "plugin"
35908
36750
  });
35909
- collectPluginSkillDirs(scan2, join15(mp.installLocation, "plugins"), mp.name);
35910
- collectPluginSkillDirs(scan2, join15(mp.installLocation, "external_plugins"), mp.name);
36751
+ collectPluginSkillDirs(scan2, join16(mp.installLocation, "plugins"), mp.name);
36752
+ collectPluginSkillDirs(scan2, join16(mp.installLocation, "external_plugins"), mp.name);
35911
36753
  }
35912
36754
  }
35913
36755
  function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
@@ -35918,7 +36760,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
35918
36760
  return;
35919
36761
  }
35920
36762
  for (const plugin of plugins) {
35921
- collectSkillsDir(scan2, join15(pluginsDir, plugin, "skills"), {
36763
+ collectSkillsDir(scan2, join16(pluginsDir, plugin, "skills"), {
35922
36764
  source: marketplace,
35923
36765
  scope: "plugin",
35924
36766
  pluginName: plugin
@@ -35972,7 +36814,7 @@ function dedupeMcpServers(servers) {
35972
36814
  }
35973
36815
  function readOptional(path) {
35974
36816
  try {
35975
- return readFileSync9(path, "utf8");
36817
+ return readFileSync10(path, "utf8");
35976
36818
  } catch {
35977
36819
  return void 0;
35978
36820
  }
@@ -36006,11 +36848,11 @@ import { Worker } from "worker_threads";
36006
36848
 
36007
36849
  // ../../packages/plugin-sdk/src/ignore-layers.ts
36008
36850
  var import_ignore = __toESM(require_ignore(), 1);
36009
- import { readFileSync as readFileSync10 } from "fs";
36010
- import { join as join16 } from "path";
36851
+ import { readFileSync as readFileSync11 } from "fs";
36852
+ import { join as join17 } from "path";
36011
36853
  function readIgnoreLayer(dir, filename, anchorLen) {
36012
36854
  try {
36013
- return { matcher: (0, import_ignore.default)().add(readFileSync10(join16(dir, filename), "utf8")), anchorLen };
36855
+ return { matcher: (0, import_ignore.default)().add(readFileSync11(join17(dir, filename), "utf8")), anchorLen };
36014
36856
  } catch {
36015
36857
  return void 0;
36016
36858
  }
@@ -36072,18 +36914,18 @@ import {
36072
36914
  fstatSync,
36073
36915
  mkdirSync as mkdirSync2,
36074
36916
  openSync as openSync2,
36075
- readFileSync as readFileSync11,
36917
+ readFileSync as readFileSync12,
36076
36918
  readSync,
36077
36919
  writeFileSync as writeFileSync5
36078
36920
  } from "fs";
36079
- import { join as join17 } from "path";
36921
+ import { join as join18 } from "path";
36080
36922
  var SESSION_MODEL_MARKER = "session-model";
36081
36923
  function recordSessionModel(dataDir2, sessionId, model) {
36082
36924
  if (sessionId === void 0 || sessionId === "") return;
36083
36925
  if (model === void 0 || model === "") return;
36084
36926
  try {
36085
36927
  mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
36086
- writeFileSync5(join17(dataDir2, SESSION_MODEL_MARKER), JSON.stringify({ sessionId, model }), {
36928
+ writeFileSync5(join18(dataDir2, SESSION_MODEL_MARKER), JSON.stringify({ sessionId, model }), {
36087
36929
  encoding: "utf8",
36088
36930
  mode: DATA_FILE_MODE
36089
36931
  });
@@ -36093,17 +36935,17 @@ function recordSessionModel(dataDir2, sessionId, model) {
36093
36935
  var TAIL_BYTES = 256 * 1024;
36094
36936
 
36095
36937
  // ../../packages/plugin-sdk/src/nudge.ts
36096
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
36097
- import { join as join18 } from "path";
36938
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
36939
+ import { join as join19 } from "path";
36098
36940
  var SESSION_START_MARKER = "session-start-last";
36099
36941
  function claimSessionStart(dataDir2, sessionId) {
36100
36942
  return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
36101
36943
  }
36102
36944
  function claimOncePerSession(dataDir2, marker, sessionId) {
36103
36945
  if (!sessionId) return true;
36104
- const path = join18(dataDir2, marker);
36946
+ const path = join19(dataDir2, marker);
36105
36947
  try {
36106
- if (readFileSync12(path, "utf8") === sessionId) return false;
36948
+ if (readFileSync13(path, "utf8") === sessionId) return false;
36107
36949
  } catch {
36108
36950
  }
36109
36951
  try {
@@ -36116,11 +36958,11 @@ function claimOncePerSession(dataDir2, marker, sessionId) {
36116
36958
 
36117
36959
  // ../../packages/plugin-sdk/src/paths.ts
36118
36960
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
36119
- import { basename as basename5, dirname as dirname4, sep as sep3 } from "path";
36961
+ import { basename as basename5, dirname as dirname5, sep as sep3 } from "path";
36120
36962
 
36121
36963
  // ../../packages/plugin-sdk/src/project-files.ts
36122
36964
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
36123
- import { basename as basename6, join as join19 } from "path";
36965
+ import { basename as basename6, join as join20 } from "path";
36124
36966
  var SKIP_DIRS = /* @__PURE__ */ new Set([
36125
36967
  ".git",
36126
36968
  "node_modules",
@@ -36217,8 +37059,8 @@ function resolveProjectFiles(cwd, opts = {}) {
36217
37059
  }
36218
37060
  if (entry.isDirectory()) {
36219
37061
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, dirRel, entry.name, true)) continue;
36220
- const fullPath = join19(dir, entry.name);
36221
- if (existsSync10(join19(fullPath, ".git"))) continue;
37062
+ const fullPath = join20(dir, entry.name);
37063
+ if (existsSync10(join20(fullPath, ".git"))) continue;
36222
37064
  if (depth >= bounds.maxDepth) {
36223
37065
  walk.omitted = true;
36224
37066
  continue;
@@ -36291,9 +37133,9 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
36291
37133
 
36292
37134
  // ../../packages/plugin-sdk/src/throttle.ts
36293
37135
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
36294
- import { join as join20 } from "path";
37136
+ import { join as join21 } from "path";
36295
37137
  function throttled(dataDir2, markerName, windowMs) {
36296
- const marker = join20(dataDir2, markerName);
37138
+ const marker = join21(dataDir2, markerName);
36297
37139
  try {
36298
37140
  if (Date.now() - statSync8(marker).mtimeMs < windowMs) return true;
36299
37141
  } catch {
@@ -36306,25 +37148,17 @@ function throttled(dataDir2, markerName, windowMs) {
36306
37148
  return false;
36307
37149
  }
36308
37150
 
36309
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
36310
- var REQUEST_TIMEOUT_MS = 2e3;
36311
- function withTimeout(promise2, ms) {
36312
- let timer;
36313
- const timeout = new Promise((_, reject) => {
36314
- timer = setTimeout(() => {
36315
- reject(new Error("attached gateway request timed out"));
36316
- }, ms);
36317
- });
36318
- promise2.catch(() => void 0);
36319
- return Promise.race([promise2, timeout]).finally(() => {
36320
- clearTimeout(timer);
36321
- });
36322
- }
36323
-
36324
37151
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
36325
37152
  function isInvalidRequest(err) {
36326
37153
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
36327
37154
  }
37155
+ function isRouteAbsent(err) {
37156
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
37157
+ }
37158
+ function isServerRejection(err) {
37159
+ const status = statusOf(err);
37160
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
37161
+ }
36328
37162
  var FORWARD_BUDGET_MS = 1500;
36329
37163
  var DECISION_PATH_BUDGET_MS = 800;
36330
37164
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -36352,7 +37186,7 @@ function parseBreakerState(raw, nowMs) {
36352
37186
  }
36353
37187
  function createForwardPolicy(deps) {
36354
37188
  const now = deps.now ?? (() => Date.now());
36355
- const file2 = join21(deps.dir, STATE_FILENAME);
37189
+ const file2 = join22(deps.dir, STATE_FILENAME);
36356
37190
  let state = null;
36357
37191
  let loading = null;
36358
37192
  async function readState() {
@@ -36392,6 +37226,20 @@ function createForwardPolicy(deps) {
36392
37226
  } catch {
36393
37227
  current = { ...CLOSED };
36394
37228
  }
37229
+ const restoreOpenedAtMs = (openedAtMs) => persist({
37230
+ consecutiveFailures: current.consecutiveFailures,
37231
+ openedAtMs,
37232
+ lastFailure: current.lastFailure
37233
+ });
37234
+ const recordFailure = (cause) => {
37235
+ const failures = current.consecutiveFailures + 1;
37236
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
37237
+ return persist({
37238
+ consecutiveFailures: failures,
37239
+ openedAtMs: shouldOpen ? now() : null,
37240
+ lastFailure: cause
37241
+ });
37242
+ };
36395
37243
  const at = now();
36396
37244
  if (current.openedAtMs !== null) {
36397
37245
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -36410,15 +37258,20 @@ function createForwardPolicy(deps) {
36410
37258
  }
36411
37259
  return { ok: true, value };
36412
37260
  } catch (err) {
36413
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
37261
+ if (isInvalidRequest(err)) {
37262
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
37263
+ return { ok: false, reason: "invalid-request" };
37264
+ }
37265
+ if (isRouteAbsent(err)) {
37266
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
37267
+ return { ok: false, reason: "route-absent" };
37268
+ }
37269
+ if (isServerRejection(err)) {
37270
+ await recordFailure("unreachable");
37271
+ return { ok: false, reason: "rejected" };
37272
+ }
36414
37273
  const reason = classifyFailure(err);
36415
- const failures = current.consecutiveFailures + 1;
36416
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
36417
- await persist({
36418
- consecutiveFailures: failures,
36419
- openedAtMs: shouldOpen ? now() : null,
36420
- lastFailure: reason
36421
- });
37274
+ await recordFailure(reason);
36422
37275
  return { ok: false, reason };
36423
37276
  }
36424
37277
  }
@@ -36426,13 +37279,11 @@ function createForwardPolicy(deps) {
36426
37279
  }
36427
37280
 
36428
37281
  // ../../packages/plugin-runtime/src/attached/gateway.ts
36429
- var ACTION_STRENGTH = {
36430
- allow: 0,
36431
- log: 1,
36432
- warn: 2,
36433
- redact: 3,
36434
- block: 4
36435
- };
37282
+ function strongerOf(a, b) {
37283
+ if (a === null) return b;
37284
+ if (b === null) return a;
37285
+ return strongerAction(a, b);
37286
+ }
36436
37287
  function ruleCategoryMap(wireRules, localRules) {
36437
37288
  const map2 = /* @__PURE__ */ new Map();
36438
37289
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -36442,11 +37293,6 @@ function ruleCategoryMap(wireRules, localRules) {
36442
37293
  }
36443
37294
  return map2;
36444
37295
  }
36445
- function strongerOf(a, b) {
36446
- if (a === null) return b;
36447
- if (b === null) return a;
36448
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
36449
- }
36450
37296
  function policyKey(policy) {
36451
37297
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
36452
37298
  }
@@ -36465,7 +37311,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
36465
37311
  const floor = floorFor(policy, categoryByRuleId);
36466
37312
  remoteCategoryAction.set(
36467
37313
  policy.target.category,
36468
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
37314
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
36469
37315
  );
36470
37316
  }
36471
37317
  for (const policy of localPolicies) {
@@ -36482,7 +37328,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
36482
37328
  }
36483
37329
  merged.set(
36484
37330
  key,
36485
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
37331
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
36486
37332
  );
36487
37333
  }
36488
37334
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -36502,13 +37348,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
36502
37348
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
36503
37349
  }
36504
37350
  const effectiveFloor = strongerOf(floor, localFloor);
36505
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
37351
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
36506
37352
  const existing = merged.get(key);
36507
37353
  if (existing === void 0) {
36508
37354
  merged.set(key, clamped);
36509
37355
  continue;
36510
37356
  }
36511
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
37357
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
36512
37358
  merged.set(key, clamped);
36513
37359
  }
36514
37360
  }
@@ -36541,6 +37387,8 @@ var AttachedDataGateway = class {
36541
37387
  );
36542
37388
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
36543
37389
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
37390
+ } else {
37391
+ this.deps.local.markCaptureOwed(record2.event);
36544
37392
  }
36545
37393
  }
36546
37394
  async ensureInventory(ctx) {
@@ -36577,9 +37425,10 @@ var AttachedDataGateway = class {
36577
37425
  // a retried tool_call, exactly this path — can never stomp a populated row.
36578
37426
  async recordAuditEvent(event) {
36579
37427
  await this.deps.local.recordAuditEvent(event);
36580
- await this.deps.forward.run(
37428
+ const forwarded = await this.deps.forward.run(
36581
37429
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
36582
37430
  );
37431
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
36583
37432
  }
36584
37433
  // Attached `llm_call` is written locally by the inner gateway, then routed to
36585
37434
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -36588,44 +37437,170 @@ var AttachedDataGateway = class {
36588
37437
  // which would write the event to the local store a second time.
36589
37438
  async recordLlmCall(input2) {
36590
37439
  await this.deps.local.recordLlmCall(input2);
36591
- await this.deps.forward.run(
36592
- () => this.deps.client.recordAuditEvent(
36593
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
36594
- )
37440
+ const event = llmAuditEvent(input2);
37441
+ const forwarded = await this.deps.forward.run(
37442
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
36595
37443
  );
37444
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
36596
37445
  }
36597
37446
  /**
36598
- * Forward one batch, item by item, under ONE aggregate deadline.
37447
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
37448
+ *
37449
+ * This used to send one HTTP request per event, which is what made the batch
37450
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
37451
+ * away everything after them. The same rows now cross 50 at a time over
37452
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
37453
+ * used — so the same budget admits ~750. The wire cap is the server's own
37454
+ * constant, sized against server cost, and the client REFUSES a longer array
37455
+ * client-side, so the chunking here is not a convention.
36599
37456
  *
36600
- * Per-item budgets bound each request and nothing bounded their sum see
36601
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
36602
- * rather than sent: the local write has already succeeded, so every caller
36603
- * has a correct result to return, and a drop is the outcome this path is
36604
- * built to accept (G8) where a blown hook timeout is not.
37457
+ * Still serial, and still for the original reason: firing N requests at once
37458
+ * would trade a latency problem for a burst the plane's per-key rate limiting
37459
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
37460
+ * the fix; more concurrent ones is not.
36605
37461
  *
36606
- * Serial rather than concurrent on purpose. Firing N requests at once would
36607
- * trade a latency problem for a burst the plane's own per-key rate limiting
36608
- * would answer with the refusals the breaker then counts.
37462
+ * When the deadline passes the remainder is dropped rather than sent: the
37463
+ * local write has already succeeded, so every caller has a correct result to
37464
+ * return. What is dropped is COUNTED, everywhere it can happen — this path
37465
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
37466
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
37467
+ * the breaker closed, renders a healthy block, and discards the tail of every
37468
+ * batch indefinitely. The SAME tally also covers a single that fails inside
37469
+ * the per-item retry below — the breaker opening mid-retry is a failure the
37470
+ * breaker's own state DOES capture, but the events still in this chunk once
37471
+ * that happens are neither delivered nor otherwise counted anywhere, which is
37472
+ * the same invisibility with a different cause.
36609
37473
  *
36610
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
36611
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
36612
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
36613
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
36614
- * plane produces no failures, keeps the breaker closed, renders a healthy
36615
- * block, and discards the tail of every batch indefinitely.
37474
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
37475
+ * single-event ack and at fifty times the blast radius here:
37476
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
37477
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
37478
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
37479
+ * delivered and never re-offer the twenty the plane did not take. So success
37480
+ * is checked against `chunk.length`; anything short of it falls into the same
37481
+ * per-item pass as a refused chunk, which is the only way to recover the
37482
+ * rows that did not land, since the ack carries no per-row verdict to
37483
+ * resend by.
37484
+ *
37485
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
37486
+ * no-op rather than a second cost — an assumption this file cannot verify.
37487
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
37488
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
37489
+ * the batch size as the invariant `recordCapture` reads), so whether a
37490
+ * duplicate counts toward THIS route's `accepted` is not expressed
37491
+ * anywhere in this repo. If it follows its sibling's convention and does
37492
+ * NOT, a chunk containing even one already-delivered row — the ordinary
37493
+ * consequence of a lost stamp, which this file already treats as cheap —
37494
+ * answers short forever and enters the per-item pass on every pass it is
37495
+ * offered again. The cost of that is bounded rather than silent: the
37496
+ * pass converges (every row lands and stamps), so it is one wasted round
37497
+ * of singles rather than a stall, and it errs toward an extra resend
37498
+ * rather than toward the lost row the alternative risks.
37499
+ *
37500
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
37501
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
37502
+ * settles none — which is why the whole chunk is stamped together on a FULL
37503
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
37504
+ * treatment, alongside a short accept, and all are re-sent one event at a
37505
+ * time:
37506
+ *
37507
+ * `invalid-request` a chunk the client refused to send at all. One malformed
37508
+ * event would otherwise cost the 49 good ones beside it —
37509
+ * a new way to lose data introduced by the very change
37510
+ * meant to stop losing it.
37511
+ * `route-absent` a deployment that predates the batch route. The
37512
+ * single-event route is the one it serves, and re-sending
37513
+ * here rather than inside the client is what gives each
37514
+ * request its own budget instead of 50 inside one.
37515
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
37516
+ * a 4xx body refusal from schema drift on the other side
37517
+ * of the wire. Settlement is batch-atomic on this reason
37518
+ * exactly as on the others, so leaving it out would cost
37519
+ * the whole chunk for one event the DEPLOYMENT considers
37520
+ * malformed, where the per-item form cost only that one.
37521
+ *
37522
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
37523
+ * chunk, and re-sending it item by item would just spend the budget failing 50
37524
+ * more times — for those, the blast radius stays exactly what it was before
37525
+ * batching.
36616
37526
  */
36617
37527
  async forwardBatch(inputs, toEvent) {
36618
37528
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
36619
- for (let i = 0; i < inputs.length; i += 1) {
36620
- const now = Date.now();
36621
- if (now >= deadline) {
36622
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
36623
- return;
37529
+ const delivered = [];
37530
+ try {
37531
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
37532
+ const now = Date.now();
37533
+ if (now >= deadline) {
37534
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
37535
+ return;
37536
+ }
37537
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
37538
+ const forwarded = await this.deps.forward.run(
37539
+ () => this.deps.client.recordAuditEvents(
37540
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
37541
+ )
37542
+ );
37543
+ if (forwarded.ok) {
37544
+ if (forwarded.value.accepted === chunk.length) {
37545
+ delivered.push(...chunk);
37546
+ continue;
37547
+ }
37548
+ } else if (
37549
+ // THREE reasons are worth a second pass, one at a time, and they are
37550
+ // the three settled BEFORE the control plane refused anything, or
37551
+ // (for `rejected`) refused the BODY rather than the connection.
37552
+ //
37553
+ // `invalid-request` — the CLIENT refused the body before any request
37554
+ // went out: a defect in one event, not an outage. Re-sending singly
37555
+ // isolates the bad one instead of charging its 49 neighbours for it.
37556
+ //
37557
+ // `route-absent` — the deployment predates the batch route and serves
37558
+ // only the single-event one. The retry IS the compatibility path, and
37559
+ // it has to live HERE rather than inside the client: each single gets
37560
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
37561
+ // fallback would spend 50 sequential round trips inside the ONE
37562
+ // budget wrapping this call — turning a working older deployment into
37563
+ // a timeout, three of those into an open breaker, and every row into
37564
+ // a silent drop while the status surface called an answering
37565
+ // deployment down.
37566
+ //
37567
+ // `rejected` — the deployment's own 4xx refusal of the body, the
37568
+ // server-side twin of `invalid-request`: isolating it the same way
37569
+ // costs one event instead of the whole chunk for a defect the
37570
+ // deployment considers local to one row.
37571
+ //
37572
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
37573
+ // the whole chunk; re-sending it item by item would just spend the
37574
+ // budget failing 50 more times.
37575
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
37576
+ ) {
37577
+ continue;
37578
+ }
37579
+ for (const [j, event] of chunk.entries()) {
37580
+ const at = Date.now();
37581
+ if (at >= deadline) {
37582
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
37583
+ return;
37584
+ }
37585
+ const single = await this.deps.forward.run(
37586
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
37587
+ );
37588
+ if (single.ok) {
37589
+ delivered.push(event);
37590
+ continue;
37591
+ }
37592
+ if (single.reason === "breaker-open") {
37593
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
37594
+ return;
37595
+ }
37596
+ recordForwardDrops(this.deps.dataDir, 1, at);
37597
+ }
37598
+ }
37599
+ } finally {
37600
+ try {
37601
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
37602
+ } catch {
36624
37603
  }
36625
- const input2 = inputs[i];
36626
- await this.deps.forward.run(
36627
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
36628
- );
36629
37604
  }
36630
37605
  }
36631
37606
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -36668,9 +37643,10 @@ var AttachedDataGateway = class {
36668
37643
  // local store.
36669
37644
  async recordConfigScan(record2) {
36670
37645
  await this.deps.local.recordConfigScan(record2);
36671
- await this.deps.forward.run(
37646
+ const forwarded = await this.deps.forward.run(
36672
37647
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
36673
37648
  );
37649
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
36674
37650
  }
36675
37651
  async recordBlockedDetection(entry) {
36676
37652
  return this.deps.local.recordBlockedDetection(entry);
@@ -36804,6 +37780,18 @@ var AttachedDataGateway = class {
36804
37780
  // exactly what it did, leaving the whole control inert on every device
36805
37781
  // while every test around it stayed green.
36806
37782
  prohibitedModels: cached2.prohibitedModels
37783
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
37784
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
37785
+ // it emits, so an 'authored' policy arriving from the control plane
37786
+ // keeps that marker even where the clamp rebuilds it with a stronger
37787
+ // action. The device reads it in exactly one direction — the rules such a
37788
+ // policy targets are not locally re-assignable — so it sits on the
37789
+ // `prohibitedModels` side of the line for the same reason that field
37790
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
37791
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
37792
+ // would be the silent failure rather than the safe one — the action would
37793
+ // still be enforced while the local override the organization authored
37794
+ // away quietly came back.
36807
37795
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
36808
37796
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
36809
37797
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -36843,10 +37831,10 @@ var AttachedDataGateway = class {
36843
37831
  //
36844
37832
  // Implementing these is what actually closes the skipped-local-maintenance
36845
37833
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
36846
- // any object carrying all five, so the composite qualifies and SessionStart
37834
+ // any object carrying them all, so the composite qualifies and SessionStart
36847
37835
  // runs maintenance on the device's real store.
36848
37836
  //
36849
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
37837
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
36850
37838
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
36851
37839
  // return value directly; declaring them `async` here would hand those call
36852
37840
  // sites a Promise and silently break both.
@@ -36869,9 +37857,15 @@ var AttachedDataGateway = class {
36869
37857
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
36870
37858
  // gives: `recordCapture` calls it after the forward has already settled, on a
36871
37859
  // path that has nothing left to await.
37860
+ markCaptureOwed(event) {
37861
+ this.deps.local.markCaptureOwed(event);
37862
+ }
36872
37863
  markCaptureDelivered(event, atMs) {
36873
37864
  this.deps.local.markCaptureDelivered(event, atMs);
36874
37865
  }
37866
+ markAuditEventsDelivered(events, atMs) {
37867
+ this.deps.local.markAuditEventsDelivered(events, atMs);
37868
+ }
36875
37869
  };
36876
37870
  function reKeyForForward(event, remote) {
36877
37871
  if (remote === null) {
@@ -36914,281 +37908,17 @@ function toolAuditEvent(input2) {
36914
37908
  }
36915
37909
 
36916
37910
  // ../../packages/plugin-runtime/src/attached/history-state.ts
36917
- import { readFileSync as readFileSync14 } from "fs";
36918
- import { join as join22 } from "path";
37911
+ import { readFileSync as readFileSync15 } from "fs";
37912
+ import { join as join23 } from "path";
36919
37913
 
36920
37914
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
36921
37915
  import { createHash as createHash6 } from "crypto";
36922
37916
  import { hostname as hostname5 } from "os";
36923
37917
 
36924
- // ../../packages/remote/src/http.ts
36925
- import { request as httpRequest } from "http";
36926
- import { request as httpsRequest } from "https";
36927
- var DEFAULT_TIMEOUT_MS = 1e4;
36928
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
36929
- var RemoteRequestError = class extends Error {
36930
- constructor(status) {
36931
- super(`control-plane request failed with status ${String(status)}`);
36932
- this.status = status;
36933
- this.name = "RemoteRequestError";
36934
- }
36935
- status;
36936
- };
36937
- var RemoteRequestInvalid = class extends Error {
36938
- constructor(route, cause) {
36939
- super(`refusing to send a malformed body to ${route}`);
36940
- this.cause = cause;
36941
- this.name = "RemoteRequestInvalid";
36942
- }
36943
- cause;
36944
- };
36945
- var RemoteResponseInvalid = class extends Error {
36946
- constructor(route, detail) {
36947
- super(`control plane answered ${route} with ${detail}`);
36948
- this.name = "RemoteResponseInvalid";
36949
- }
36950
- };
36951
- var RemoteTransportError = class extends Error {
36952
- /**
36953
- * The status the peer sent, when headers arrived and only the BODY was
36954
- * refused.
36955
- *
36956
- * Undefined for the ordinary case this class was written for — no answer at
36957
- * all. It exists because two paths reject after a status has already been
36958
- * delivered: an oversized body and an aborted response. Discarding it there
36959
- * reported a deployment answering 401 with a verbose body as a network
36960
- * outage, which sends the reader to look at their network instead of their
36961
- * credential.
36962
- */
36963
- constructor(reason, status) {
36964
- super(`control-plane request did not complete: ${reason}`);
36965
- this.status = status;
36966
- this.name = "RemoteTransportError";
36967
- }
36968
- status;
36969
- };
36970
- async function send(options) {
36971
- const url2 = new URL(options.url);
36972
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
36973
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
36974
- const requestOptions = {
36975
- method: options.method,
36976
- headers: {
36977
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
36978
- // last they win, and two of the values below are ones no caller may
36979
- // replace: `x-api-key` is the credential, and `content-length` is the
36980
- // byte count that stops a multi-byte body being truncated by the
36981
- // receiver. `SendOptions.headers` is a free-form record on an exported
36982
- // function, so "no caller does that today" is not the guarantee to rely
36983
- // on. The one header any caller actually passes — `if-none-match` on the
36984
- // conditional GET — is untouched by this order.
36985
- ...options.headers,
36986
- // The credential. One header, matching what the deployment authenticates
36987
- // on; a second copy in an `Authorization` header would be one more place
36988
- // it can be logged by an intermediary for no gain.
36989
- //
36990
- // Spread conditionally rather than assigned as `undefined`: Node's header
36991
- // handling and `content-length` bookkeeping treat a present-but-undefined
36992
- // key differently from an absent one, and "the header is not there" is
36993
- // the property the attach flow needs.
36994
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
36995
- accept: "application/json",
36996
- ...options.body === void 0 ? {} : {
36997
- "content-type": "application/json",
36998
- // Byte length, not string length: a multi-byte body sent with a
36999
- // character count is truncated by the receiver.
37000
- "content-length": String(Buffer.byteLength(options.body))
37001
- }
37002
- }
37003
- };
37004
- return new Promise((resolve2, reject) => {
37005
- let settled = false;
37006
- const fail = (reason, status) => {
37007
- if (settled) return;
37008
- settled = true;
37009
- reject(new RemoteTransportError(reason, status));
37010
- };
37011
- const req = send_(url2, requestOptions, (res) => {
37012
- const chunks = [];
37013
- let size = 0;
37014
- res.on("data", (chunk) => {
37015
- size += chunk.length;
37016
- if (size > MAX_RESPONSE_BYTES) {
37017
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
37018
- res.destroy();
37019
- req.destroy();
37020
- return;
37021
- }
37022
- chunks.push(chunk);
37023
- });
37024
- res.on("aborted", () => {
37025
- fail("the response was aborted", res.statusCode);
37026
- });
37027
- res.on("end", () => {
37028
- if (settled) return;
37029
- settled = true;
37030
- resolve2({
37031
- status: res.statusCode ?? 0,
37032
- headers: res.headers,
37033
- body: Buffer.concat(chunks).toString("utf8")
37034
- });
37035
- });
37036
- });
37037
- const deadline = setTimeout(() => {
37038
- fail(`no response within ${String(timeoutMs)}ms`);
37039
- req.destroy();
37040
- }, timeoutMs);
37041
- deadline.unref();
37042
- req.on("upgrade", (_res, socket) => {
37043
- fail("the deployment answered with a protocol upgrade");
37044
- socket.destroy();
37045
- });
37046
- req.on("close", () => {
37047
- fail("the connection closed before a response was read");
37048
- clearTimeout(deadline);
37049
- });
37050
- req.on("error", (err) => {
37051
- fail(err.message);
37052
- });
37053
- if (options.body !== void 0) req.write(options.body);
37054
- req.end();
37055
- });
37056
- }
37057
-
37058
- // ../../packages/remote/src/client.ts
37059
- var ROUTES = {
37060
- events: "/v1/events",
37061
- auditEvents: "/v1/audit-events",
37062
- auditEventsBatch: "/v1/audit-events/batch",
37063
- inventory: "/v1/inventory",
37064
- storePosture: "/v1/store-posture",
37065
- policyBundle: "/v1/policy-bundle",
37066
- whoami: "/v1/plugin/whoami",
37067
- shares: "/v1/shares"
37068
- };
37069
- function headerValue(response, name) {
37070
- const raw = response.headers[name];
37071
- if (raw === void 0) return void 0;
37072
- return Array.isArray(raw) ? raw[0] : raw;
37073
- }
37074
- function okBody(response) {
37075
- if (response.status < 200 || response.status >= 300) {
37076
- throw new RemoteRequestError(response.status);
37077
- }
37078
- return response.body;
37079
- }
37080
- function parsed(schema, body, route) {
37081
- let json2;
37082
- try {
37083
- json2 = JSON.parse(body);
37084
- } catch {
37085
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
37086
- }
37087
- const result = schema.safeParse(json2);
37088
- if (!result.success) {
37089
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
37090
- }
37091
- return result.data;
37092
- }
37093
- function withoutTrailingSlashes(endpoint) {
37094
- let end = endpoint.length;
37095
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
37096
- return endpoint.slice(0, end);
37097
- }
37098
- var SLASH = "/".charCodeAt(0);
37099
- function createRemoteClient(options) {
37100
- const base = withoutTrailingSlashes(options.endpoint);
37101
- const url2 = (route) => `${base}${route}`;
37102
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
37103
- const sendOne = async (event) => {
37104
- const validated = RecordAuditEventRequest.safeParse(event);
37105
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
37106
- const response = await send({
37107
- ...common,
37108
- method: "POST",
37109
- url: url2(ROUTES.auditEvents),
37110
- body: JSON.stringify(validated.data)
37111
- });
37112
- okBody(response);
37113
- };
37114
- return {
37115
- async ingestEvents(batch) {
37116
- const response = await send({
37117
- ...common,
37118
- method: "POST",
37119
- url: url2(ROUTES.events),
37120
- body: JSON.stringify(batch)
37121
- });
37122
- return parsed(IngestAck, okBody(response), ROUTES.events);
37123
- },
37124
- async ingestInventory(context) {
37125
- const response = await send({
37126
- ...common,
37127
- method: "POST",
37128
- url: url2(ROUTES.inventory),
37129
- body: JSON.stringify(context)
37130
- });
37131
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
37132
- },
37133
- async recordAuditEvent(event) {
37134
- await sendOne(event);
37135
- },
37136
- async recordAuditEvents(events) {
37137
- const validated = RecordAuditEventBatch.safeParse({ events });
37138
- if (!validated.success) {
37139
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
37140
- }
37141
- const response = await send({
37142
- ...common,
37143
- method: "POST",
37144
- url: url2(ROUTES.auditEventsBatch),
37145
- body: JSON.stringify(validated.data)
37146
- });
37147
- if (response.status === 404) {
37148
- for (const event of validated.data.events) await sendOne(event);
37149
- return { accepted: validated.data.events.length };
37150
- }
37151
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
37152
- },
37153
- async reportStorePosture(snapshot) {
37154
- const response = await send({
37155
- ...common,
37156
- method: "POST",
37157
- url: url2(ROUTES.storePosture),
37158
- body: JSON.stringify(snapshot)
37159
- });
37160
- okBody(response);
37161
- },
37162
- async getPolicyBundle(etag) {
37163
- const response = await send({
37164
- ...common,
37165
- method: "GET",
37166
- url: url2(ROUTES.policyBundle),
37167
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
37168
- });
37169
- if (response.status === 304) {
37170
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
37171
- }
37172
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
37173
- return { changed: true, bundle, etag: headerValue(response, "etag") };
37174
- },
37175
- async whoami() {
37176
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
37177
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
37178
- },
37179
- async recordProjectEgress(request) {
37180
- const validated = EgressIngestRequest.safeParse(request);
37181
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
37182
- const response = await send({
37183
- ...common,
37184
- method: "POST",
37185
- url: url2(ROUTES.shares),
37186
- body: JSON.stringify(validated.data)
37187
- });
37188
- okBody(response);
37189
- }
37190
- };
37191
- }
37918
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
37919
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
37920
+ var TRACE_ID = EventMetadata.shape.traceId;
37921
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
37192
37922
 
37193
37923
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
37194
37924
  import { spawn } from "child_process";
@@ -37220,14 +37950,14 @@ function spawnDetached(scriptPath) {
37220
37950
  }
37221
37951
 
37222
37952
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
37223
- import { readFileSync as readFileSync15 } from "fs";
37953
+ import { readFileSync as readFileSync16 } from "fs";
37224
37954
  var manifestBuildCache = /* @__PURE__ */ new Map();
37225
37955
  function readManifestBuild(manifestUrl, packageName) {
37226
37956
  const key = manifestUrl.href;
37227
37957
  if (!manifestBuildCache.has(key)) {
37228
37958
  let build;
37229
37959
  try {
37230
- const manifest = JSON.parse(readFileSync15(manifestUrl, "utf8"));
37960
+ const manifest = JSON.parse(readFileSync16(manifestUrl, "utf8"));
37231
37961
  build = typeof manifest.version === "string" && manifest.version.length > 0 ? { package: packageName, version: manifest.version } : void 0;
37232
37962
  } catch {
37233
37963
  build = void 0;
@@ -37254,7 +37984,7 @@ function createPluginBlock(build, policyStore) {
37254
37984
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
37255
37985
  import { randomUUID as randomUUID16 } from "crypto";
37256
37986
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
37257
- import { join as join23 } from "path";
37987
+ import { join as join24 } from "path";
37258
37988
 
37259
37989
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
37260
37990
  import { rename as rename2 } from "fs/promises";
@@ -37278,7 +38008,7 @@ async function publishByRename(tmp, file2, move = rename2) {
37278
38008
 
37279
38009
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
37280
38010
  function createPolicyStore(dir = dataDir()) {
37281
- const file2 = join23(dir, "policy-cache.json");
38011
+ const file2 = join24(dir, "policy-cache.json");
37282
38012
  async function read() {
37283
38013
  try {
37284
38014
  const raw = await readFile2(file2, "utf8");
@@ -37287,22 +38017,32 @@ function createPolicyStore(dir = dataDir()) {
37287
38017
  const record2 = parsed2;
37288
38018
  const bundle = PolicyBundle.parse(record2.bundle);
37289
38019
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
37290
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
38020
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
38021
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
38022
+ const etag = replayable ? stored : void 0;
37291
38023
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
37292
38024
  } catch {
37293
38025
  return null;
37294
38026
  }
37295
38027
  }
37296
- async function write(bundle, etag) {
37297
- await ensureDataDir(dir);
37298
- const stored = {
37299
- bundle,
37300
- fetchedAtMs: Date.now(),
37301
- ...etag === void 0 ? {} : { etag }
37302
- };
38028
+ function knowsMoreThanThisBuild(shapeId) {
38029
+ if (typeof shapeId !== "string" || shapeId === "") return false;
38030
+ const theirs = new Set(shapeId.split(","));
38031
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
38032
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
38033
+ }
38034
+ async function priorRecord() {
38035
+ try {
38036
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
38037
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
38038
+ } catch {
38039
+ return null;
38040
+ }
38041
+ }
38042
+ async function publishRecord(record2) {
37303
38043
  const tmp = `${file2}.${randomUUID16()}.tmp`;
37304
38044
  try {
37305
- await writeFile2(tmp, JSON.stringify(stored), {
38045
+ await writeFile2(tmp, JSON.stringify(record2), {
37306
38046
  encoding: "utf8",
37307
38047
  mode: DATA_FILE_MODE,
37308
38048
  flag: "wx"
@@ -37313,6 +38053,27 @@ function createPolicyStore(dir = dataDir()) {
37313
38053
  throw err;
37314
38054
  }
37315
38055
  }
38056
+ async function write(bundle, etag) {
38057
+ await ensureDataDir(dir);
38058
+ const prior = await priorRecord();
38059
+ const priorVersion = prior?.bundle?.version;
38060
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
38061
+ await publishRecord({
38062
+ ...prior,
38063
+ fetchedAtMs: Date.now()
38064
+ });
38065
+ return;
38066
+ }
38067
+ await publishRecord({
38068
+ bundle,
38069
+ fetchedAtMs: Date.now(),
38070
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
38071
+ // bundle it already holds, and the point of the stamp is to describe the
38072
+ // build that last narrowed those bytes, which is this one.
38073
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
38074
+ ...etag === void 0 ? {} : { etag }
38075
+ });
38076
+ }
37316
38077
  return { read, write, file: file2 };
37317
38078
  }
37318
38079
 
@@ -37478,11 +38239,11 @@ function readStorePosture(dbPath2) {
37478
38239
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
37479
38240
  import { randomUUID as randomUUID17 } from "crypto";
37480
38241
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
37481
- import { join as join24 } from "path";
38242
+ import { join as join25 } from "path";
37482
38243
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
37483
38244
  function createPostureStore(dir = settingsDir(), legacyDir) {
37484
- const file2 = join24(dir, "posture-state.json");
37485
- const legacyFile = legacyDir === void 0 ? null : join24(legacyDir, "posture-state.json");
38245
+ const file2 = join25(dir, "posture-state.json");
38246
+ const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
37486
38247
  async function persist(state) {
37487
38248
  await ensureDataDir(dir);
37488
38249
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -37550,8 +38311,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
37550
38311
  }
37551
38312
 
37552
38313
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
37553
- import { readFileSync as readFileSync16 } from "fs";
37554
- import { join as join25 } from "path";
38314
+ import { readFileSync as readFileSync17 } from "fs";
38315
+ import { join as join26 } from "path";
37555
38316
 
37556
38317
  // ../../packages/plugin-runtime/src/attached/status.ts
37557
38318
  var REFUSAL_LINES = {
@@ -37893,9 +38654,21 @@ var StandaloneDataGateway = class {
37893
38654
  // for the whole of it, so a member that threw would make that answer a lie
37894
38655
  // the moment a composite delegated to it. A store-level no-op is the honest
37895
38656
  // shape — a standalone machine has nothing delivered to record.
38657
+ markCaptureOwed(event) {
38658
+ this.db.markCaptureOwed(event);
38659
+ }
37896
38660
  markCaptureDelivered(event, atMs) {
37897
38661
  this.db.markCaptureDelivered(event, atMs);
37898
38662
  }
38663
+ // Implemented, not stubbed, for the same reason its sibling above is: the
38664
+ // attached gateway is a DECORATOR over an instance of this class
38665
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
38666
+ // stamp the live forward makes lands here with a non-empty array. This is the
38667
+ // production write path for that feature, not a shape-satisfying no-op — a
38668
+ // machine that is merely standalone simply never calls it.
38669
+ markAuditEventsDelivered(events, atMs) {
38670
+ this.db.markAuditEventsDelivered(events, atMs);
38671
+ }
37899
38672
  staleBinaryNotice(currentVersion) {
37900
38673
  try {
37901
38674
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -38179,7 +38952,7 @@ function pluginBuild() {
38179
38952
 
38180
38953
  // src/history/reconcile-trigger.ts
38181
38954
  import { spawn as spawn3 } from "child_process";
38182
- import { dirname as dirname5, join as join27 } from "path";
38955
+ import { dirname as dirname6, join as join28 } from "path";
38183
38956
  import { fileURLToPath as fileURLToPath4 } from "url";
38184
38957
 
38185
38958
  // src/history/tail.ts
@@ -38189,11 +38962,11 @@ import {
38189
38962
  fstatSync as fstatSync2,
38190
38963
  mkdirSync as mkdirSync5,
38191
38964
  openSync as openSync3,
38192
- readFileSync as readFileSync17,
38965
+ readFileSync as readFileSync18,
38193
38966
  readSync as readSync2,
38194
38967
  writeFileSync as writeFileSync8
38195
38968
  } from "fs";
38196
- import { join as join26 } from "path";
38969
+ import { join as join27 } from "path";
38197
38970
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
38198
38971
  function safeSessionId(sessionId) {
38199
38972
  if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
@@ -38209,8 +38982,8 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
38209
38982
  try {
38210
38983
  const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
38211
38984
  if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
38212
- const here = dirname5(fileURLToPath4(import.meta.url));
38213
- const child = spawn3(process.execPath, [join27(here, "reconcile.js"), sessionId, transcriptPath], {
38985
+ const here = dirname6(fileURLToPath4(import.meta.url));
38986
+ const child = spawn3(process.execPath, [join28(here, "reconcile.js"), sessionId, transcriptPath], {
38214
38987
  detached: true,
38215
38988
  stdio: "ignore"
38216
38989
  });
@@ -38221,17 +38994,17 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
38221
38994
 
38222
38995
  // src/protocol/marker.ts
38223
38996
  import { randomBytes as randomBytes4 } from "crypto";
38224
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync18, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "fs";
38225
- import { join as join28 } from "path";
38997
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync19, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "fs";
38998
+ import { join as join29 } from "path";
38226
38999
  var MARKER_FILE = "protocol-marker";
38227
39000
  function mintMarker() {
38228
39001
  return randomBytes4(8).toString("hex");
38229
39002
  }
38230
39003
  function sessionProtocolMarker(dataDir2, sessionId) {
38231
39004
  if (!sessionId) return mintMarker();
38232
- const path = join28(dataDir2, MARKER_FILE);
39005
+ const path = join29(dataDir2, MARKER_FILE);
38233
39006
  try {
38234
- const stored = JSON.parse(readFileSync18(path, "utf8"));
39007
+ const stored = JSON.parse(readFileSync19(path, "utf8"));
38235
39008
  if (stored.sessionId === sessionId && typeof stored.marker === "string" && /^[0-9a-f]{16}$/.test(stored.marker)) {
38236
39009
  return stored.marker;
38237
39010
  }
@@ -38240,7 +39013,7 @@ function sessionProtocolMarker(dataDir2, sessionId) {
38240
39013
  const marker = mintMarker();
38241
39014
  try {
38242
39015
  mkdirSync6(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
38243
- const tmp = join28(dataDir2, `${MARKER_FILE}.tmp`);
39016
+ const tmp = join29(dataDir2, `${MARKER_FILE}.tmp`);
38244
39017
  writeFileSync9(tmp, JSON.stringify({ sessionId, marker }), { mode: DATA_FILE_MODE });
38245
39018
  renameSync5(tmp, path);
38246
39019
  } catch {
@@ -38303,16 +39076,16 @@ function emit(output2) {
38303
39076
  }
38304
39077
 
38305
39078
  // src/hooks/store-health.ts
38306
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
38307
- import { dirname as dirname6, join as join29 } from "path";
39079
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
39080
+ import { dirname as dirname7, join as join30 } from "path";
38308
39081
  var STORE_REDIRECT_MARKER = "store-redirect-last-session";
38309
39082
  function markerDirs(dataDir2) {
38310
- return [dataDir2, dirname6(dataDir2)];
39083
+ return [dataDir2, dirname7(dataDir2)];
38311
39084
  }
38312
39085
  function alreadyClaimed(dirs, marker, sessionId) {
38313
39086
  return dirs.some((dir) => {
38314
39087
  try {
38315
- return readFileSync19(join29(dir, marker), "utf8") === sessionId;
39088
+ return readFileSync20(join30(dir, marker), "utf8") === sessionId;
38316
39089
  } catch {
38317
39090
  return false;
38318
39091
  }
@@ -38322,7 +39095,7 @@ function recordClaim(dirs, marker, sessionId) {
38322
39095
  for (const dir of dirs) {
38323
39096
  try {
38324
39097
  mkdirSync7(dir, { recursive: true, mode: DATA_DIR_MODE });
38325
- writeFileSync10(join29(dir, marker), sessionId, { mode: DATA_FILE_MODE });
39098
+ writeFileSync10(join30(dir, marker), sessionId, { mode: DATA_FILE_MODE });
38326
39099
  return;
38327
39100
  } catch {
38328
39101
  }
@@ -38347,7 +39120,7 @@ function formatMode(mode) {
38347
39120
  }
38348
39121
  function warnIfStoreRedirected(config2, sessionId, write = (message2) => void process.stderr.write(message2)) {
38349
39122
  try {
38350
- const paths = symlinkedStorePaths(dirname6(config2.dataDir));
39123
+ const paths = symlinkedStorePaths(dirname7(config2.dataDir));
38351
39124
  if (paths.length === 0) return;
38352
39125
  if (!sessionId) {
38353
39126
  write(storeRedirectedMessage(paths));
@@ -38366,7 +39139,7 @@ function harnessVersion() {
38366
39139
  const manifestPath = process.argv[2];
38367
39140
  if (!manifestPath) return void 0;
38368
39141
  try {
38369
- const manifest = JSON.parse(readFileSync20(manifestPath, "utf8"));
39142
+ const manifest = JSON.parse(readFileSync21(manifestPath, "utf8"));
38370
39143
  return typeof manifest.version === "string" ? manifest.version : void 0;
38371
39144
  } catch {
38372
39145
  return void 0;