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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -493,11 +493,12 @@ var require_ignore = __commonJS({
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
495
  import { existsSync as existsSync7 } from "fs";
496
- import { join as join12 } from "path";
496
+ import { join as join13 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/attached-derived.ts
499
499
  import { rmSync } from "fs";
500
500
  import { join } from "path";
501
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
501
502
  var ATTACHED_FORWARD_STATE_FILENAME = "attached-state.json";
502
503
  var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
503
504
 
@@ -598,6 +599,30 @@ var SQLITE_MIGRATIONS = [
598
599
  {
599
600
  tag: "0022_audit_inspection_ms",
600
601
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
602
+ },
603
+ {
604
+ tag: "0023_secret_vault_user_authorized",
605
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
606
+ },
607
+ {
608
+ tag: "0024_finding_resolution_key_created_index",
609
+ sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
610
+ },
611
+ {
612
+ tag: "0025_audit_capture_attribute_columns",
613
+ sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
614
+ },
615
+ {
616
+ tag: "0026_audit_llm_call_usage_columns",
617
+ sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
618
+ },
619
+ {
620
+ tag: "0027_audit_llm_usage_index",
621
+ sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
622
+ },
623
+ {
624
+ tag: "0028_activity_session_probe_indexes",
625
+ sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
601
626
  }
602
627
  ];
603
628
 
@@ -22137,6 +22162,26 @@ var AttachTokenResponse = external_exports.union([
22137
22162
  AttachTokenExpired,
22138
22163
  external_exports.object({ status: printable(64) })
22139
22164
  ]);
22165
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22166
+ var DeviceCommand = external_exports.object({
22167
+ id: printable(128).min(1),
22168
+ kind: DeviceCommandKind,
22169
+ issuedAt: printable(64).min(1),
22170
+ expiresAt: printable(64).min(1)
22171
+ }).strict();
22172
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22173
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22174
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22175
+ external_exports.object({
22176
+ outcome: external_exports.literal("reported"),
22177
+ projectsScanned: external_exports.number().int().nonnegative()
22178
+ }).strict(),
22179
+ external_exports.object({
22180
+ outcome: external_exports.literal("failed"),
22181
+ reason: DeviceCommandFailureReason,
22182
+ projectsScanned: external_exports.number().int().nonnegative()
22183
+ }).strict()
22184
+ ]);
22140
22185
 
22141
22186
  // ../../packages/schema/src/zod/registry.ts
22142
22187
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22303,7 +22348,7 @@ var PackManifest = external_exports.object({
22303
22348
  }).meta({ id: "PackManifest" });
22304
22349
 
22305
22350
  // ../../packages/schema/src/zod/detection.ts
22306
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22351
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22307
22352
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22308
22353
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22309
22354
  var DetectionCounts = external_exports.object({
@@ -22440,14 +22485,17 @@ function optional2(key, parsed2, raw) {
22440
22485
  function isStringArray(value) {
22441
22486
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22442
22487
  }
22488
+ var ORIGIN_VALUES = { library: true, custom: true };
22489
+ function resolveOrigin(origin) {
22490
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22491
+ }
22443
22492
  function summaryToDetectionListItem(s) {
22444
22493
  return {
22445
22494
  id: `${s.namespace}/${s.packId}`,
22446
22495
  name: s.name,
22447
22496
  version: s.version,
22448
22497
  enabled: s.enabled,
22449
- origin: "library",
22450
- // v1: every installed pack is library origin
22498
+ origin: resolveOrigin(s.origin),
22451
22499
  namespace: s.namespace,
22452
22500
  packId: s.packId,
22453
22501
  ruleCount: s.ruleCount,
@@ -22499,7 +22547,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22499
22547
  name: row.name,
22500
22548
  version: row.version,
22501
22549
  enabled: row.enabled,
22502
- origin: "library",
22550
+ origin: resolveOrigin(row.origin),
22503
22551
  namespace: row.namespace,
22504
22552
  packId: row.packId,
22505
22553
  ruleCount: row.rules.length,
@@ -22519,16 +22567,20 @@ function splitDetectionId(id) {
22519
22567
  }
22520
22568
  function buildDetectionsList(summaries, query) {
22521
22569
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22570
+ const originOf = (s) => resolveOrigin(s.origin);
22522
22571
  const counts = {
22523
22572
  all: summaries.length,
22524
- library: summaries.length,
22525
- // all origin=library in v1
22526
- custom: 0,
22573
+ library: summaries.filter((s) => originOf(s) === "library").length,
22574
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22575
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22576
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22577
+ // place, and that state does not exist — editing a library pack forks it. See
22578
+ // OriginEnum.
22527
22579
  customized: 0,
22528
22580
  updates: withUpdate.length
22529
22581
  };
22530
22582
  const filter = query.filter;
22531
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22583
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22532
22584
  if (query.q) {
22533
22585
  const q = query.q.toLowerCase();
22534
22586
  filtered = filtered.filter(
@@ -22608,8 +22660,9 @@ var Event = external_exports.object({
22608
22660
  metadata: EventMetadata.optional()
22609
22661
  }).meta({ id: "Event" });
22610
22662
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22663
+ var INGEST_BATCH_MAX = 100;
22611
22664
  var IngestBatch = external_exports.object({
22612
- events: external_exports.array(IngestEvent).min(1).max(100),
22665
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22613
22666
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22614
22667
  // additionally rejects any event whose contentHash the store has already
22615
22668
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -23165,6 +23218,252 @@ var PatchInstalledPackRequest = external_exports.object({
23165
23218
  message: "At least one field must be provided"
23166
23219
  }).meta({ id: "PatchInstalledPackRequest" });
23167
23220
 
23221
+ // ../../packages/schema/src/zod/policy.ts
23222
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23223
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23224
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23225
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23226
+ var Policy = external_exports.object({
23227
+ id: external_exports.guid(),
23228
+ scope: PolicyScope,
23229
+ target: PolicyTarget,
23230
+ action: ActionTaken,
23231
+ enabled: external_exports.boolean().default(true),
23232
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23233
+ // Display name — optional so older policy rows without name still parse.
23234
+ // Added for the findings API (policy.name column migration).
23235
+ name: external_exports.string().optional(),
23236
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23237
+ // which row this is. A producer that collapses several rows onto one target
23238
+ // must carry the marker onto whichever row survives, or the collapse decides
23239
+ // the answer; a survivor may therefore be a built-in expansion still marked
23240
+ // 'authored' because an authored sibling targeted the same thing.
23241
+ // Optional so an older producer — and an older on-disk cache — still parses;
23242
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23243
+ //
23244
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23245
+ // built-in archetype catalog entry a policy is, which every catalog surface
23246
+ // reads and which a caller may state. This one is a statement the PRODUCER
23247
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23248
+ // — the CRUD routes neither accept nor set it.
23249
+ //
23250
+ // A device consumes this in exactly one direction: an 'authored' policy
23251
+ // arriving from a control plane marks the rules it targets as not
23252
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23253
+ // which is what makes it safe to honour from an unsigned cache — the same
23254
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23255
+ provenance: PolicyProvenance.optional()
23256
+ }).meta({ id: "Policy" });
23257
+ var PolicyBundle = external_exports.object({
23258
+ version: external_exports.string(),
23259
+ policies: external_exports.array(Policy),
23260
+ // Rules from the installed marketplace packs (snapshotted by the
23261
+ // control plane). The plugin registers these in addition to its bundled
23262
+ // packs. Optional so older backends — and older on-disk caches — that omit
23263
+ // the field still parse; consumers read `bundle.rules ?? []`.
23264
+ rules: external_exports.array(Rule).optional(),
23265
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23266
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23267
+ // after reading the user's installed snapshot (installed_packs, enabled
23268
+ // packs only), which is how detection updates stay manual: new bundled
23269
+ // rules run only after the user applies the pack update. Absent/false keeps
23270
+ // the historical composition (bundled packs + rules) — older caches.
23271
+ rulesComplete: external_exports.boolean().optional(),
23272
+ // Active detection exceptions, evaluation subset only (see
23273
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23274
+ // on-disk caches — that omit the field still parse; consumers read
23275
+ // `bundle.exceptions ?? []`.
23276
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23277
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23278
+ // A second axis over the same `redact` action, carried beside the policies
23279
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23280
+ // widening Policy itself would change a persisted shape to express something
23281
+ // only the in-memory bundle needs. Optional so an older producer — or an
23282
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23283
+ // pre-existing one-way behaviour, which is the safe direction to default.
23284
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23285
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23286
+ // from a versioned installed pack. Optional so older backends — and older
23287
+ // on-disk caches — that omit the field still parse; consumers fall back to
23288
+ // the rule's own spec version. NOT the bundle version above — see
23289
+ // installedRuleset's ruleVersions for the source of truth.
23290
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23291
+ // Model ids (the raw `model` string a harness reports, e.g.
23292
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23293
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23294
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23295
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23296
+ // unenforced behaviour that predates this field and the safe direction to
23297
+ // default.
23298
+ //
23299
+ // Ids, not display names: the governance decision is keyed on the exact
23300
+ // string the harness reports (`model_status_override.versionId` in the
23301
+ // control plane), so no name resolution stands between the decision and the
23302
+ // comparison.
23303
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23304
+ customKeywords: external_exports.array(external_exports.string()),
23305
+ fetchedAt: external_exports.iso.datetime()
23306
+ }).meta({ id: "PolicyBundle" });
23307
+ var POLICY_BUNDLE_SHAPE_ID = [
23308
+ ...Object.keys(PolicyBundle.shape),
23309
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23310
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23311
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23312
+ ].sort().join(",");
23313
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23314
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23315
+ var CATEGORY_PEAK_SEVERITY = {
23316
+ secret: "critical",
23317
+ financial: "critical",
23318
+ // core-financial/credit-card
23319
+ code_flaw: "critical",
23320
+ pii: "high",
23321
+ phi: "high",
23322
+ custom: "high",
23323
+ // user-defined; conservative
23324
+ code_context: "low",
23325
+ config: "low"
23326
+ // observe-only; floors to monitor regardless
23327
+ };
23328
+ function severityFloorPolicy(category) {
23329
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23330
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23331
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23332
+ }
23333
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23334
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23335
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23336
+ id: "RedactFallback"
23337
+ });
23338
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23339
+ var BUILTIN_POLICY_SPECS = {
23340
+ monitor: {
23341
+ name: "Monitor",
23342
+ action: "log",
23343
+ reversible: false,
23344
+ description: "Log every match for audit. The request is allowed through untouched."
23345
+ },
23346
+ warn: {
23347
+ name: "Warn",
23348
+ action: "warn",
23349
+ reversible: false,
23350
+ description: "Allow the request, but warn the user inline before it is sent."
23351
+ },
23352
+ redact: {
23353
+ name: "Redact",
23354
+ action: "redact",
23355
+ reversible: false,
23356
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23357
+ },
23358
+ vault: {
23359
+ name: "Redact & Vault",
23360
+ action: "redact",
23361
+ reversible: true,
23362
+ description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
23363
+ },
23364
+ block: {
23365
+ name: "Block",
23366
+ action: "block",
23367
+ reversible: false,
23368
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23369
+ }
23370
+ };
23371
+ function builtinPolicyToAction(id) {
23372
+ return BUILTIN_POLICY_SPECS[id].action;
23373
+ }
23374
+ var PALETTE_WEAKEST_FIRST = [
23375
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23376
+ ];
23377
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23378
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23379
+ );
23380
+ var ACTION_STRENGTH_ORDER = [
23381
+ ...BELOW_PALETTE,
23382
+ ...PALETTE_WEAKEST_FIRST
23383
+ ];
23384
+ function actionRank(action) {
23385
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23386
+ }
23387
+ function isActionAtLeast(action, floor) {
23388
+ return actionRank(action) >= actionRank(floor);
23389
+ }
23390
+ function strongerAction(a, b) {
23391
+ return actionRank(a) >= actionRank(b) ? a : b;
23392
+ }
23393
+ function weakestBuiltinAtLeast(floor) {
23394
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23395
+ }
23396
+ var PackPolicyFloor = external_exports.object({
23397
+ /**
23398
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23399
+ * rather than a raw ActionTaken because that is the vocabulary the user
23400
+ * picks from — a floor a UI cannot name is one it cannot explain.
23401
+ */
23402
+ floor: BuiltinPolicyId,
23403
+ /**
23404
+ * True when the organization AUTHORED a policy governing this pack rather
23405
+ * than stating a minimum: it gave the answer, so the pack is not
23406
+ * re-assignable locally in either direction.
23407
+ */
23408
+ locked: external_exports.boolean()
23409
+ }).describe("PackPolicyFloor");
23410
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23411
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23412
+ );
23413
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23414
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23415
+ );
23416
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23417
+ function builtinPolicyIsReversible(id) {
23418
+ return BUILTIN_POLICY_SPECS[id].reversible;
23419
+ }
23420
+ function policyIdIsReversible(policyId) {
23421
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23422
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23423
+ return builtinPolicyIsReversible(id);
23424
+ }
23425
+ var DEFAULT_ACTIONS = Object.fromEntries(
23426
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23427
+ );
23428
+ var BUILTIN_POLICIES = Object.fromEntries(
23429
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23430
+ );
23431
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23432
+ function policyIdToAction(policyId) {
23433
+ const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23434
+ const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23435
+ return BUILTIN_POLICIES[id].action;
23436
+ }
23437
+ var UsedByItem = external_exports.object({
23438
+ id: external_exports.string(),
23439
+ name: external_exports.string(),
23440
+ ruleCount: external_exports.number().int().nonnegative(),
23441
+ enabled: external_exports.boolean()
23442
+ }).meta({ id: "UsedByItem" });
23443
+ var PolicyListItem = external_exports.object({
23444
+ id: external_exports.string(),
23445
+ kind: PolicyKind,
23446
+ name: external_exports.string(),
23447
+ enabled: external_exports.boolean(),
23448
+ usedByCount: external_exports.number().int().nonnegative()
23449
+ }).meta({ id: "PolicyListItem" });
23450
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23451
+ var PolicyDetail = external_exports.object({
23452
+ specVersion: external_exports.literal(1),
23453
+ id: external_exports.string(),
23454
+ kind: PolicyKind,
23455
+ name: external_exports.string(),
23456
+ enabled: external_exports.boolean(),
23457
+ description: external_exports.string(),
23458
+ usedBy: external_exports.array(UsedByItem)
23459
+ }).meta({ id: "PolicyDetail" });
23460
+ var PolicyStatsResponse = external_exports.object({
23461
+ policies: external_exports.number().int().nonnegative(),
23462
+ builtin: external_exports.number().int().nonnegative(),
23463
+ custom: external_exports.number().int().nonnegative(),
23464
+ detectionsGoverned: external_exports.number().int().nonnegative()
23465
+ }).meta({ id: "PolicyStatsResponse" });
23466
+
23168
23467
  // ../../packages/schema/src/zod/vault.ts
23169
23468
  var POINTER_FORMAT_VERSION = 2;
23170
23469
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23205,6 +23504,14 @@ var VaultEntry = external_exports.object({
23205
23504
  // How many times this value has been detected on this machine — the reuse
23206
23505
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23207
23506
  occurrenceCount: external_exports.number().int().nonnegative(),
23507
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23508
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23509
+ // one row however many paths vault it, so this is what tells a policy sweep
23510
+ // that the row carries somebody's own instruction and not just an assignment
23511
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23512
+ // vaulting of the same value must never clear it — what the user said about
23513
+ // the value does not expire.
23514
+ userAuthorized: external_exports.boolean(),
23208
23515
  firstSeen: external_exports.string(),
23209
23516
  lastSeen: external_exports.string()
23210
23517
  });
@@ -23331,7 +23638,7 @@ function isVaultConsentValid(consent) {
23331
23638
  }
23332
23639
 
23333
23640
  // ../../packages/schema/src/zod/local.ts
23334
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23641
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23335
23642
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23336
23643
  var RunMode = external_exports.enum(["standalone", "attached"]);
23337
23644
  var ControlPlaneConnection = external_exports.object({
@@ -23373,6 +23680,19 @@ var WorkspaceSettings = external_exports.object({
23373
23680
  vaultKeyCustody: VaultKeyCustody.default("file"),
23374
23681
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23375
23682
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23683
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23684
+ // place. Not a handling policy: the policy has already resolved to redact,
23685
+ // and this only says what happens when the host offers no channel to carry it
23686
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23687
+ // Claude Code decline to mask a field that EXECUTES because masking would
23688
+ // change what runs. Per FIELD rather than per host, so a host that can
23689
+ // rewrite some inputs keeps true redaction on those.
23690
+ //
23691
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23692
+ // an attached machine's merge is `strongerAction` over the one action ladder
23693
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23694
+ // word and stays out of the stored value.
23695
+ redactFallback: RedactFallback.default("warn"),
23376
23696
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23377
23697
  onboardedAt: external_exports.iso.datetime().optional(),
23378
23698
  // Records that the user consented to sending findings to the model API for
@@ -23380,10 +23700,12 @@ var WorkspaceSettings = external_exports.object({
23380
23700
  // Absent until granted; a stale payloadVersion means the consent no longer
23381
23701
  // covers the current payload and must be re-granted.
23382
23702
  modelJudgeConsent: ModelJudgeConsent.optional(),
23383
- // Records that the user consented to sending the activity already recorded on
23384
- // this machine to the deployment it is attached to, along with the payload
23385
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23386
- // a different endpoint or an older payload no longer counts.
23703
+ // Records that the user consented to the DEFERRED send — the outbox along
23704
+ // with the payload shape and the endpoint they agreed to. Since payload v2
23705
+ // that covers both the pre-attach backlog and undelivered captures (which
23706
+ // carry prompt/reply text in `content`); the key name predates the widening.
23707
+ // Absent until granted, and a grant for a different endpoint or an older
23708
+ // payload no longer counts.
23387
23709
  historySyncConsent: HistorySyncConsent.optional()
23388
23710
  });
23389
23711
  function defaultWorkspaceSettings() {
@@ -23511,7 +23833,8 @@ var ManagedSettingKey = external_exports.enum([
23511
23833
  "vaultKeyCustody",
23512
23834
  "vaultInlineReveal",
23513
23835
  "modelJudgeConsent",
23514
- "dataSharesInPlace"
23836
+ "dataSharesInPlace",
23837
+ "redactFallback"
23515
23838
  ]).meta({ id: "ManagedSettingKey" });
23516
23839
  var ManagedSettingsValues = external_exports.object({
23517
23840
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
@@ -23524,7 +23847,8 @@ var ManagedSettingsValues = external_exports.object({
23524
23847
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23525
23848
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23526
23849
  modelJudgeConsent: external_exports.boolean().optional(),
23527
- dataSharesInPlace: external_exports.boolean().optional()
23850
+ dataSharesInPlace: external_exports.boolean().optional(),
23851
+ redactFallback: RedactFallback.optional()
23528
23852
  }).meta({ id: "ManagedSettingsValues" });
23529
23853
  var ManagedSettings = external_exports.object({
23530
23854
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23539,186 +23863,6 @@ var ManagedSettings = external_exports.object({
23539
23863
  lockedFields: external_exports.array(ManagedSettingKey).default([])
23540
23864
  }).meta({ id: "ManagedSettings" });
23541
23865
 
23542
- // ../../packages/schema/src/zod/policy.ts
23543
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23544
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23545
- var Policy = external_exports.object({
23546
- id: external_exports.guid(),
23547
- scope: PolicyScope,
23548
- target: PolicyTarget,
23549
- action: ActionTaken,
23550
- enabled: external_exports.boolean().default(true),
23551
- customKeywords: external_exports.array(external_exports.string()).optional(),
23552
- // Display name — optional so older policy rows without name still parse.
23553
- // Added for the findings API (policy.name column migration).
23554
- name: external_exports.string().optional()
23555
- }).meta({ id: "Policy" });
23556
- var PolicyBundle = external_exports.object({
23557
- version: external_exports.string(),
23558
- policies: external_exports.array(Policy),
23559
- // Rules from the installed marketplace packs (snapshotted by the
23560
- // control plane). The plugin registers these in addition to its bundled
23561
- // packs. Optional so older backends — and older on-disk caches — that omit
23562
- // the field still parse; consumers read `bundle.rules ?? []`.
23563
- rules: external_exports.array(Rule).optional(),
23564
- // When true, `rules` IS the complete effective ruleset and the runtime must
23565
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23566
- // after reading the user's installed snapshot (installed_packs, enabled
23567
- // packs only), which is how detection updates stay manual: new bundled
23568
- // rules run only after the user applies the pack update. Absent/false keeps
23569
- // the historical composition (bundled packs + rules) — older caches.
23570
- rulesComplete: external_exports.boolean().optional(),
23571
- // Active detection exceptions, evaluation subset only (see
23572
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23573
- // on-disk caches — that omit the field still parse; consumers read
23574
- // `bundle.exceptions ?? []`.
23575
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23576
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23577
- // A second axis over the same `redact` action, carried beside the policies
23578
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23579
- // widening Policy itself would change a persisted shape to express something
23580
- // only the in-memory bundle needs. Optional so an older producer — or an
23581
- // older on-disk cache — still parses; consumers read `?? []` and get the
23582
- // pre-existing one-way behaviour, which is the safe direction to default.
23583
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23584
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23585
- // from a versioned installed pack. Optional so older backends — and older
23586
- // on-disk caches — that omit the field still parse; consumers fall back to
23587
- // the rule's own spec version. NOT the bundle version above — see
23588
- // installedRuleset's ruleVersions for the source of truth.
23589
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23590
- // Model ids (the raw `model` string a harness reports, e.g.
23591
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23592
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23593
- // one (UserPromptSubmit). Optional so an older backend — and an older
23594
- // on-disk cache — still parses; consumers read `?? []`, which is the
23595
- // unenforced behaviour that predates this field and the safe direction to
23596
- // default.
23597
- //
23598
- // Ids, not display names: the governance decision is keyed on the exact
23599
- // string the harness reports (`model_status_override.versionId` in the
23600
- // control plane), so no name resolution stands between the decision and the
23601
- // comparison.
23602
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23603
- customKeywords: external_exports.array(external_exports.string()),
23604
- fetchedAt: external_exports.iso.datetime()
23605
- }).meta({ id: "PolicyBundle" });
23606
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23607
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23608
- var CATEGORY_PEAK_SEVERITY = {
23609
- secret: "critical",
23610
- financial: "critical",
23611
- // core-financial/credit-card
23612
- code_flaw: "critical",
23613
- pii: "high",
23614
- phi: "high",
23615
- custom: "high",
23616
- // user-defined; conservative
23617
- code_context: "low",
23618
- config: "low"
23619
- // observe-only; floors to monitor regardless
23620
- };
23621
- function severityFloorPolicy(category) {
23622
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23623
- const peak = CATEGORY_PEAK_SEVERITY[category];
23624
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23625
- }
23626
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23627
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23628
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23629
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23630
- var BUILTIN_POLICY_SPECS = {
23631
- monitor: {
23632
- name: "Monitor",
23633
- action: "log",
23634
- reversible: false,
23635
- description: "Log every match for audit. The request is allowed through untouched."
23636
- },
23637
- warn: {
23638
- name: "Warn",
23639
- action: "warn",
23640
- reversible: false,
23641
- description: "Allow the request, but warn the user inline before it is sent."
23642
- },
23643
- redact: {
23644
- name: "Redact",
23645
- action: "redact",
23646
- reversible: false,
23647
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23648
- },
23649
- vault: {
23650
- name: "Redact & Vault",
23651
- action: "redact",
23652
- reversible: true,
23653
- description: "Strip the matched value from the request and keep an encrypted, recoverable copy in the local vault, leaving a pointer in its place. Needs the vault consent granted under Settings; without it this behaves as Redact."
23654
- },
23655
- block: {
23656
- name: "Block",
23657
- action: "block",
23658
- reversible: false,
23659
- description: "Refuse the request entirely whenever any rule in this detection matches."
23660
- }
23661
- };
23662
- function builtinPolicyToAction(id) {
23663
- return BUILTIN_POLICY_SPECS[id].action;
23664
- }
23665
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23666
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23667
- );
23668
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23669
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23670
- );
23671
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23672
- function builtinPolicyIsReversible(id) {
23673
- return BUILTIN_POLICY_SPECS[id].reversible;
23674
- }
23675
- function policyIdIsReversible(policyId) {
23676
- const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23677
- const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23678
- return builtinPolicyIsReversible(id);
23679
- }
23680
- var DEFAULT_ACTIONS = Object.fromEntries(
23681
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23682
- );
23683
- var BUILTIN_POLICIES = Object.fromEntries(
23684
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23685
- );
23686
- var DEFAULT_PACK_POLICY_ID = "monitor";
23687
- function policyIdToAction(policyId) {
23688
- const parsed2 = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23689
- const id = parsed2.success ? parsed2.data : DEFAULT_PACK_POLICY_ID;
23690
- return BUILTIN_POLICIES[id].action;
23691
- }
23692
- var UsedByItem = external_exports.object({
23693
- id: external_exports.string(),
23694
- name: external_exports.string(),
23695
- ruleCount: external_exports.number().int().nonnegative(),
23696
- enabled: external_exports.boolean()
23697
- }).meta({ id: "UsedByItem" });
23698
- var PolicyListItem = external_exports.object({
23699
- id: external_exports.string(),
23700
- kind: PolicyKind,
23701
- name: external_exports.string(),
23702
- enabled: external_exports.boolean(),
23703
- usedByCount: external_exports.number().int().nonnegative()
23704
- }).meta({ id: "PolicyListItem" });
23705
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23706
- var PolicyDetail = external_exports.object({
23707
- specVersion: external_exports.literal(1),
23708
- id: external_exports.string(),
23709
- kind: PolicyKind,
23710
- name: external_exports.string(),
23711
- enabled: external_exports.boolean(),
23712
- description: external_exports.string(),
23713
- usedBy: external_exports.array(UsedByItem)
23714
- }).meta({ id: "PolicyDetail" });
23715
- var PolicyStatsResponse = external_exports.object({
23716
- policies: external_exports.number().int().nonnegative(),
23717
- builtin: external_exports.number().int().nonnegative(),
23718
- custom: external_exports.number().int().nonnegative(),
23719
- detectionsGoverned: external_exports.number().int().nonnegative()
23720
- }).meta({ id: "PolicyStatsResponse" });
23721
-
23722
23866
  // ../../packages/schema/src/zod/project-files.ts
23723
23867
  var ProjectFileInput = external_exports.object({
23724
23868
  path: external_exports.string().min(1),
@@ -23964,10 +24108,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23964
24108
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23965
24109
 
23966
24110
  // ../../packages/schema/src/zod/settings-action.ts
24111
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24112
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23967
24113
  var SaveSettingsInput = external_exports.object({
23968
24114
  historicalAccess: external_exports.string(),
23969
- modelJudgeConsent: external_exports.boolean(),
23970
- historySyncConsent: external_exports.boolean(),
24115
+ modelJudgeConsent: ModelJudgeConsentChoice,
24116
+ historySyncConsent: HistorySyncConsentChoice,
23971
24117
  vaultConsent: external_exports.string(),
23972
24118
  vaultInlineReveal: external_exports.string()
23973
24119
  });
@@ -24117,9 +24263,9 @@ function deriveReviewReasons(trust, transports) {
24117
24263
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24118
24264
  return reasons;
24119
24265
  }
24120
- function buildReviewInfo(trust, transports) {
24266
+ function buildReviewInfo(trust, transports, decided) {
24121
24267
  const reasons = deriveReviewReasons(trust, transports);
24122
- return { needsReview: reasons.length > 0, reasons };
24268
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24123
24269
  }
24124
24270
  function distinctTransports(transports) {
24125
24271
  return Array.from(new Set(transports));
@@ -24337,8 +24483,8 @@ function readControlPlaneCredential(settingsDir2, connection) {
24337
24483
  }
24338
24484
 
24339
24485
  // ../../packages/persistence/src/database.ts
24340
- import { randomUUID as randomUUID10 } from "crypto";
24341
- import { join as join4, sep } from "path";
24486
+ import { randomUUID as randomUUID11 } from "crypto";
24487
+ import { dirname as dirname2, join as join7, sep } from "path";
24342
24488
  import { DatabaseSync } from "node:sqlite";
24343
24489
 
24344
24490
  // ../../packages/persistence/src/ids.ts
@@ -24603,6 +24749,10 @@ function allRows(stmt, params) {
24603
24749
  if (Array.isArray(params)) return stmt.all(...params);
24604
24750
  return stmt.all(params);
24605
24751
  }
24752
+ function* iterateRows(stmt, params) {
24753
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24754
+ for (const row of rows) yield row;
24755
+ }
24606
24756
  function getRow(stmt, params) {
24607
24757
  if (params === void 0) return stmt.get();
24608
24758
  if (Array.isArray(params)) return stmt.get(...params);
@@ -25071,10 +25221,17 @@ function ensureSyncedAtColumn(db, table) {
25071
25221
  if (!columns.includes("sync_claimed_at")) {
25072
25222
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
25073
25223
  }
25224
+ if (!columns.includes("outbox_owed")) {
25225
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25226
+ }
25074
25227
  db.exec(
25075
25228
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25076
25229
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
25077
25230
  );
25231
+ db.exec(
25232
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25233
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25234
+ );
25078
25235
  db.exec(
25079
25236
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
25080
25237
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25179,7 +25336,6 @@ function decodeKeysetCursor(cursor) {
25179
25336
  // ../../packages/persistence/src/repositories/activity.ts
25180
25337
  var DAY_MS = 864e5;
25181
25338
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25182
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25183
25339
  function defaultTimeZone() {
25184
25340
  try {
25185
25341
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25234,6 +25390,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25234
25390
  error: "error",
25235
25391
  active: "active"
25236
25392
  };
25393
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25237
25394
  function safeParseStringArray(raw) {
25238
25395
  if (!raw) return [];
25239
25396
  const parsed2 = safeJson(raw, null);
@@ -25307,6 +25464,37 @@ var TIMELINE_COLUMNS = `
25307
25464
  json_extract(attributes, '$.targetId') AS target_id,
25308
25465
  json_extract(attributes, '$.internal') AS internal,
25309
25466
  json_extract(attributes, '$.flagged') AS flagged`;
25467
+ var LLM_USAGE_SELECT = `
25468
+ SELECT root_session_id AS sessionId,
25469
+ provider,
25470
+ model,
25471
+ service_tier AS serviceTier,
25472
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25473
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25474
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25475
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25476
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25477
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25478
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25479
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25480
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25481
+ function usageLeaves(rows) {
25482
+ return rows.map((row) => {
25483
+ const attributes = {
25484
+ input_tokens: row.inputTokens,
25485
+ output_tokens: row.outputTokens,
25486
+ cache_creation_input_tokens: row.cacheCreationTokens,
25487
+ cache_read_input_tokens: row.cacheReadTokens,
25488
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25489
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25490
+ web_search_requests: row.webSearchRequests
25491
+ };
25492
+ if (row.provider !== null) attributes.provider = row.provider;
25493
+ if (row.model !== null) attributes.model = row.model;
25494
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25495
+ return { sessionId: row.sessionId, attributes };
25496
+ });
25497
+ }
25310
25498
  var SESSION_ROOT = `event_type = 'session'`;
25311
25499
  var HAS_ACTIVITY = `EXISTS (
25312
25500
  SELECT 1 FROM audit_events c
@@ -25332,16 +25520,17 @@ var SqliteActivityRepository = class {
25332
25520
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25333
25521
  const liveNow = countScalar(
25334
25522
  this.db,
25335
- `SELECT count(*) AS n FROM audit_events s
25523
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25336
25524
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25337
- AND max(
25338
- s.started_at,
25339
- coalesce(
25340
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25341
- s.started_at
25342
- )
25343
- ) >= ?`,
25344
- [liveThreshold]
25525
+ AND s.id IN (
25526
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25527
+ UNION
25528
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25529
+ WHERE started_at >= ?
25530
+ UNION
25531
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25532
+ WHERE ended_at >= ?)`,
25533
+ [liveThreshold, liveThreshold, liveThreshold]
25345
25534
  );
25346
25535
  const toolCallsToday = countScalar(
25347
25536
  this.db,
@@ -25471,7 +25660,7 @@ var SqliteActivityRepository = class {
25471
25660
  this.db.prepare(
25472
25661
  `SELECT ${TIMELINE_COLUMNS}
25473
25662
  FROM audit_events
25474
- WHERE id = ? OR root_session_id = ?
25663
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25475
25664
  ORDER BY started_at ASC, id ASC`
25476
25665
  ),
25477
25666
  [sessionId, sessionId]
@@ -25484,14 +25673,14 @@ var SqliteActivityRepository = class {
25484
25673
  coalesce(sum(output_tokens), 0) AS output,
25485
25674
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25486
25675
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25487
- FROM audit_events
25676
+ FROM audit_events INDEXED BY idx_audit_session_type
25488
25677
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25489
25678
  ),
25490
25679
  [sessionId]
25491
25680
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25492
25681
  const primaryModel = getRow(
25493
25682
  this.db.prepare(
25494
- `SELECT model, provider FROM audit_events
25683
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25495
25684
  WHERE root_session_id = ? AND event_type = 'llm_call'
25496
25685
  ORDER BY started_at ASC, id ASC
25497
25686
  LIMIT 1`
@@ -25502,7 +25691,7 @@ var SqliteActivityRepository = class {
25502
25691
  this.db.prepare(
25503
25692
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25504
25693
  count(*) AS n
25505
- FROM audit_events
25694
+ FROM audit_events INDEXED BY idx_audit_session
25506
25695
  WHERE root_session_id = ? AND event_type = 'tool_call'
25507
25696
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25508
25697
  ),
@@ -25510,7 +25699,7 @@ var SqliteActivityRepository = class {
25510
25699
  );
25511
25700
  const modelRows = allRows(
25512
25701
  this.db.prepare(
25513
- `SELECT DISTINCT model FROM audit_events
25702
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25514
25703
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25515
25704
  ORDER BY model`
25516
25705
  ),
@@ -25519,7 +25708,7 @@ var SqliteActivityRepository = class {
25519
25708
  const derivedModels = modelRows.map((r) => r.model);
25520
25709
  const commits = countScalar(
25521
25710
  this.db,
25522
- `SELECT count(*) AS n FROM audit_events
25711
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25523
25712
  WHERE root_session_id = ? AND event_type = 'commit'`,
25524
25713
  [sessionId]
25525
25714
  );
@@ -25555,25 +25744,57 @@ var SqliteActivityRepository = class {
25555
25744
  return Promise.resolve(session);
25556
25745
  }
25557
25746
  /**
25558
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25559
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25560
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25561
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25562
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25563
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25747
+ * Cross-session token report — every `llm_call` in the store (or in a
25748
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25749
+ * session, with USD cost DERIVED at read time via the shared
25750
+ * `defaultCostModel` (never stored). The caller collapses these onto
25751
+ * per-model rows with `aggregateTokenUsage`.
25752
+ *
25753
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25754
+ * the members the rollup sums — and priced once per group, which is exact
25755
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25756
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25757
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25758
+ * the index stores the values once, at write, and answers the same window in
25759
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25760
+ * planner prefers the general event-type index and fetches every row to
25761
+ * recompute the columns it could have read. The index is one every open
25762
+ * store carries, since opening runs the migrations, so the hard requirement
25763
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25764
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25765
+ * per call, no bag parsed.
25564
25766
  */
25565
25767
  tokenReports(fromMs) {
25566
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25567
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25768
+ const rows = allRows(
25769
+ this.db.prepare(
25770
+ `${LLM_USAGE_SELECT}
25771
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25772
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25773
+ ${LLM_USAGE_GROUP}`
25774
+ ),
25775
+ fromMs === void 0 ? void 0 : [fromMs]
25776
+ );
25777
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25568
25778
  }
25569
25779
  /**
25570
- * One session's token report — its `llm_call` leaves grouped per (provider,
25571
- * model) with derived cost, or `null` when the session made no `llm_call`s
25572
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25573
- * breakdown + estimated cost.
25780
+ * One session's token report — its `llm_call`s grouped per (provider,
25781
+ * model, tier) with derived cost, or `null` when the session made no
25782
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25783
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25784
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25785
+ * it replaces walked every `llm_call` in the store to find one session's.
25574
25786
  */
25575
25787
  tokenReportForSession(sessionId) {
25576
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25788
+ const rows = allRows(
25789
+ this.db.prepare(
25790
+ `${LLM_USAGE_SELECT}
25791
+ FROM audit_events
25792
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25793
+ ${LLM_USAGE_GROUP}`
25794
+ ),
25795
+ [sessionId]
25796
+ );
25797
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25577
25798
  return Promise.resolve(reports[0] ?? null);
25578
25799
  }
25579
25800
  /**
@@ -25597,42 +25818,6 @@ var SqliteActivityRepository = class {
25597
25818
  for (const row of rows) seen.add(toHarness(row.harness));
25598
25819
  return Promise.resolve([...seen]);
25599
25820
  }
25600
- /**
25601
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25602
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25603
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25604
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25605
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25606
- */
25607
- readLlmCallLeaves(opts = {}) {
25608
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25609
- const params = [];
25610
- if (opts.sessionId !== void 0) {
25611
- conditions.push("root_session_id = ?");
25612
- params.push(opts.sessionId);
25613
- }
25614
- if (opts.fromMs !== void 0) {
25615
- conditions.push("started_at >= ?");
25616
- params.push(opts.fromMs);
25617
- }
25618
- const rows = allRows(
25619
- this.db.prepare(
25620
- `SELECT root_session_id AS sessionId, attributes
25621
- FROM audit_events
25622
- WHERE ${conditions.join(" AND ")}`
25623
- ),
25624
- params
25625
- );
25626
- return mapRowsTolerant(
25627
- rows.filter(
25628
- (row) => row.sessionId !== null
25629
- ),
25630
- (row) => ({
25631
- sessionId: row.sessionId,
25632
- attributes: JSON.parse(row.attributes)
25633
- })
25634
- );
25635
- }
25636
25821
  /**
25637
25822
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25638
25823
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25647,20 +25832,23 @@ var SqliteActivityRepository = class {
25647
25832
  const inClause = placeholders(sessionIds.length);
25648
25833
  const lastActivityRows = allRows(
25649
25834
  this.db.prepare(
25650
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25651
- WHERE root_session_id IN (${inClause})
25652
- GROUP BY root_session_id`
25835
+ `SELECT ids.value AS id,
25836
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25837
+ (SELECT max(ended_at) FROM audit_events e
25838
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25839
+ FROM json_each(?) AS ids`
25653
25840
  ),
25654
- sessionIds
25841
+ [JSON.stringify(sessionIds)]
25655
25842
  );
25656
25843
  for (const row of lastActivityRows) {
25657
- if (row.id === null) continue;
25658
25844
  const entry = result.get(row.id);
25659
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25845
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25846
+ if (entry && last > 0) entry.lastActivityMs = last;
25660
25847
  }
25661
25848
  const turnsRows = allRows(
25662
25849
  this.db.prepare(
25663
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25850
+ `SELECT root_session_id AS id, count(*) AS n
25851
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25664
25852
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25665
25853
  GROUP BY root_session_id`
25666
25854
  ),
@@ -25675,7 +25863,7 @@ var SqliteActivityRepository = class {
25675
25863
  this.db.prepare(
25676
25864
  `SELECT root_session_id AS id,
25677
25865
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25678
- FROM audit_events
25866
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25679
25867
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25680
25868
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25681
25869
  GROUP BY root_session_id`
@@ -25705,7 +25893,7 @@ var SqliteActivityRepository = class {
25705
25893
  this.db.prepare(
25706
25894
  `SELECT root_session_id AS id,
25707
25895
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25708
- FROM audit_events
25896
+ FROM audit_events INDEXED BY idx_audit_session_share
25709
25897
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25710
25898
  GROUP BY root_session_id`
25711
25899
  ),
@@ -26734,7 +26922,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26734
26922
 
26735
26923
  // ../../packages/persistence/src/repositories/findings.ts
26736
26924
  var PREVIEW_INSTANCES_PER_GROUP = 200;
26737
- var SCAN_BATCH_ROWS = 1e3;
26738
26925
  var DEFAULT_LOCATIONS_LIMIT = 100;
26739
26926
  var LOCATION_RULE_IDS_CAP = 20;
26740
26927
  function compareLocationOrder(a, b) {
@@ -26763,6 +26950,25 @@ function deriveInstanceStatus(row) {
26763
26950
  latestResolutionStatus: row.latest_status
26764
26951
  });
26765
26952
  }
26953
+ function toFlatFindingRow(r) {
26954
+ return {
26955
+ id: r.id,
26956
+ ruleId: r.rule_id,
26957
+ category: r.category,
26958
+ severity: r.severity,
26959
+ maskedMatch: r.masked_match,
26960
+ actionTaken: r.action_taken,
26961
+ confidence: r.confidence,
26962
+ occurredAt: epochMillisToIso(r.occurred_at),
26963
+ sourceTool: r.source_tool,
26964
+ repo: r.repo ?? "",
26965
+ file: r.file ?? "",
26966
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26967
+ eventId: r.event_id,
26968
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26969
+ status: deriveInstanceStatus(r)
26970
+ };
26971
+ }
26766
26972
  function encodeGroupCursor(group) {
26767
26973
  const payload = {
26768
26974
  sev: group.severity,
@@ -26838,7 +27044,7 @@ var SqliteFindingsRepository = class {
26838
27044
  this.db.prepare(
26839
27045
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26840
27046
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26841
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27047
+ e.source_tool AS source_tool,
26842
27048
  e.event_type AS kind
26843
27049
  FROM audit_events e
26844
27050
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26946,56 +27152,11 @@ var SqliteFindingsRepository = class {
26946
27152
  predicate,
26947
27153
  params: sessionParams
26948
27154
  });
26949
- const rows = allRows(
26950
- this.db.prepare(
26951
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26952
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26953
- kind, finding_key, latest_status
26954
- FROM (
26955
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26956
- d.severity AS severity, f.masked_match AS masked_match,
26957
- f.action_taken AS action_taken, f.confidence AS confidence,
26958
- e.started_at AS occurred_at,
26959
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26960
- json_extract(e.attributes, '$.repo') AS repo,
26961
- json_extract(e.attributes, '$.file_path') AS file,
26962
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26963
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26964
- e.event_type AS kind, f.finding_key AS finding_key,
26965
- latest.status AS latest_status,
26966
- ROW_NUMBER() OVER (
26967
- PARTITION BY d.rule_id
26968
- ORDER BY e.started_at DESC, f.id DESC
26969
- ) AS rn
26970
- FROM inspection_findings f
26971
- JOIN audit_events e ON e.id = f.audit_event_id
26972
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26973
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26974
- ON latest.finding_key = f.finding_key
26975
- ${predicate}
26976
- )
26977
- WHERE rn <= :cap
26978
- ORDER BY occurred_at DESC, id DESC`
26979
- ),
26980
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26981
- );
26982
- const groupable = rows.map((r) => ({
26983
- id: r.id,
26984
- ruleId: r.rule_id,
26985
- category: r.category,
26986
- severity: r.severity,
26987
- maskedMatch: r.masked_match,
26988
- actionTaken: r.action_taken,
26989
- confidence: r.confidence,
26990
- occurredAt: epochMillisToIso(r.occurred_at),
26991
- sourceTool: r.source_tool,
26992
- repo: r.repo ?? "",
26993
- file: r.file ?? "",
26994
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26995
- eventId: r.event_id,
26996
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26997
- status: deriveInstanceStatus(r)
26998
- }));
27155
+ const rows = this.previewRows(aggregates, {
27156
+ sessionId: query.sessionId,
27157
+ from: query.from
27158
+ });
27159
+ const groupable = rows.map(toFlatFindingRow);
26999
27160
  const allGroups = buildFindingGroups(groupable, { aggregates });
27000
27161
  const filterOpts = {
27001
27162
  severity: query.severity,
@@ -27081,8 +27242,10 @@ var SqliteFindingsRepository = class {
27081
27242
  *
27082
27243
  * The scan runs from the top of the scope on every request, not from the
27083
27244
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
27084
- * move as the caller pages. Rows are pulled in batches so memory stays flat
27085
- * while the counting runs, and only the page itself is retained.
27245
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27246
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27247
+ * counting runs — a generator streaming the index order, not a sequence of
27248
+ * fetched batches; only the page itself is retained.
27086
27249
  */
27087
27250
  listFindingInstances(query) {
27088
27251
  const opts = {
@@ -27098,6 +27261,10 @@ var SqliteFindingsRepository = class {
27098
27261
  };
27099
27262
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27100
27263
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27264
+ const isPastCursor = cursor === null ? () => true : (row) => {
27265
+ const rowMs = isoToEpochMillis(row.occurredAt);
27266
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27267
+ };
27101
27268
  const accumulator = createInstanceFacetAccumulator(opts);
27102
27269
  const items = [];
27103
27270
  let total = 0;
@@ -27110,6 +27277,7 @@ var SqliteFindingsRepository = class {
27110
27277
  accumulator.add(row);
27111
27278
  if (!matchesInstanceFilters(row, opts)) continue;
27112
27279
  total += 1;
27280
+ if (!isPastCursor(row)) continue;
27113
27281
  if (items.length < limit) {
27114
27282
  items.push(toInstanceDetail(row));
27115
27283
  last = row;
@@ -27118,15 +27286,6 @@ var SqliteFindingsRepository = class {
27118
27286
  }
27119
27287
  }
27120
27288
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27121
- if (cursor !== null) {
27122
- const resumed = this.pageAfter(cursor, opts, limit, query);
27123
- return Promise.resolve({
27124
- totals: { findings: total },
27125
- facets: accumulator.facets(),
27126
- items: resumed.items,
27127
- nextCursor: resumed.nextCursor
27128
- });
27129
- }
27130
27289
  return Promise.resolve({
27131
27290
  totals: { findings: total },
27132
27291
  facets: accumulator.facets(),
@@ -27134,35 +27293,6 @@ var SqliteFindingsRepository = class {
27134
27293
  nextCursor
27135
27294
  });
27136
27295
  }
27137
- /**
27138
- * The page of matching rows strictly after `cursor`. Separate from the
27139
- * counting pass because that one starts at the top of the scope by design;
27140
- * this one narrows the scan with the same keyset predicate the activity list
27141
- * uses, so a later page costs less than the first rather than more.
27142
- */
27143
- pageAfter(cursor, opts, limit, query) {
27144
- const items = [];
27145
- let last;
27146
- let hasMore = false;
27147
- for (const row of this.scanFindingRows({
27148
- sessionId: query.sessionId,
27149
- from: query.from,
27150
- after: cursor
27151
- })) {
27152
- if (!matchesInstanceFilters(row, opts)) continue;
27153
- if (items.length < limit) {
27154
- items.push(toInstanceDetail(row));
27155
- last = row;
27156
- } else {
27157
- hasMore = true;
27158
- break;
27159
- }
27160
- }
27161
- return {
27162
- items,
27163
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27164
- };
27165
- }
27166
27296
  /**
27167
27297
  * The same findings folded by location: repository, then file within it.
27168
27298
  *
@@ -27245,25 +27375,111 @@ var SqliteFindingsRepository = class {
27245
27375
  });
27246
27376
  }
27247
27377
  /**
27248
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27378
+ * Each group's newest instances, for the table's expanded rows.
27379
+ *
27380
+ * ONE index-ordered scan with early termination, and the shape is the point.
27381
+ * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27382
+ * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27383
+ * through a temp B-tree to keep a bounded preview of each group, and then
27384
+ * sorts the survivors again for the page order. Both sorts grow with the
27385
+ * store while the answer does not.
27386
+ *
27387
+ * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27388
+ * (or the session or window index the scope names — see `findingScanSql`),
27389
+ * which is already the order the page wants, and keeps rows per rule until
27390
+ * each rule has as many as it can show. The aggregate the caller already holds
27391
+ * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27392
+ * per rule, summed, is the number of rows this scan has to find, and it stops
27393
+ * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27394
+ * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27395
+ * store with many firing rules widens it. The bound that DOES hold
27396
+ * unconditionally is the sorted form's floor: this scan visits at most as
27397
+ * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27398
+ * sorted, and stops the moment every rule has its cap, where the sorted form
27399
+ * sorts the whole scope regardless. The true worst case — the rarest rule's
27400
+ * wanted instances sitting at the tail of the scope — is one pass over
27401
+ * everything in scope with a block sort of the id tie-break only, never a
27402
+ * sort of the scope, which is still that floor.
27403
+ *
27404
+ * A row whose rule the aggregate did not see is skipped: the two statements
27405
+ * run without a shared snapshot, so a capture landing between them can add a
27406
+ * rule here that has no counts there, and the counts are what the group is
27407
+ * built from.
27408
+ */
27409
+ previewRows(aggregates, scope) {
27410
+ const wanted = /* @__PURE__ */ new Map();
27411
+ let remaining = 0;
27412
+ for (const [ruleId, agg] of aggregates) {
27413
+ const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27414
+ wanted.set(ruleId, n);
27415
+ remaining += n;
27416
+ }
27417
+ const rows = [];
27418
+ if (remaining === 0) return rows;
27419
+ const { sql, params } = this.findingScanSql(scope);
27420
+ const taken = /* @__PURE__ */ new Map();
27421
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27422
+ const want = wanted.get(r.rule_id);
27423
+ if (want === void 0) continue;
27424
+ const have = taken.get(r.rule_id) ?? 0;
27425
+ if (have >= want) continue;
27426
+ taken.set(r.rule_id, have + 1);
27427
+ rows.push(r);
27428
+ remaining -= 1;
27429
+ if (remaining === 0) break;
27430
+ }
27431
+ return rows;
27432
+ }
27433
+ /**
27434
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27249
27435
  *
27250
27436
  * A generator so a caller streams the scope without it ever being an array:
27251
27437
  * the flat list counts and facets the whole filtered scope, which on a large
27252
- * store is far more rows than any page. Each batch advances the same keyset
27253
- * predicate the page read uses, so the scan is a sequence of bounded reads
27254
- * rather than one unbounded result set.
27255
- *
27256
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27257
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27258
- * makes it a point lookup per row, and the derived table would re-materialize
27259
- * a window over the whole resolution table once per batch.
27438
+ * store is far more rows than any page. The rows come off ONE statement,
27439
+ * iterated rather than materialized, in the index order `findingScanSql`
27440
+ * arranges so the scan is a single pass with a block sort of the id
27441
+ * tie-break only, never a sort of the scope, where a sequence of
27442
+ * keyset-bounded batches re-sorted everything below the cursor on every
27443
+ * batch and cost the square of the scope.
27260
27444
  *
27261
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27262
- * would be missing from its own facet, which is computed by excluding that
27263
- * dimension see listFindingInstances.
27445
+ * `sessionId` and `from` carry ONLY what no facet counts a filter
27446
+ * dimension narrowed here would be missing from its own facet, which is
27447
+ * computed by excluding that dimension (see listFindingInstances). There is
27448
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27449
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27450
+ * narrower statement, since the counting pass already visits every row a
27451
+ * page-2+ request would otherwise re-seek for.
27264
27452
  */
27265
27453
  *scanFindingRows(scope) {
27266
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27454
+ const { sql, params } = this.findingScanSql(scope);
27455
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27456
+ yield toFlatFindingRow(r);
27457
+ }
27458
+ }
27459
+ /**
27460
+ * The one statement both instance-level scans run: every finding in scope,
27461
+ * joined to its event and definition, newest first.
27462
+ *
27463
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27464
+ * the same two `recentFindings` documents at length, for the same reason:
27465
+ *
27466
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27467
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27468
+ * yields `started_at` order per event type, not across the four, so
27469
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27470
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27471
+ * `idx_audit_session` for a session scope, which is also `started_at`
27472
+ * ordered within the session — and the order falls out of the index.
27473
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27474
+ * JOINs the planner drives from the findings and sorts everything.
27475
+ *
27476
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27477
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27478
+ * index probe per keyed row, and a derived table over the whole resolution
27479
+ * table would be materialized before the first row streamed.
27480
+ */
27481
+ findingScanSql(scope) {
27482
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27267
27483
  const params = [];
27268
27484
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27269
27485
  conditions.push("e.root_session_id = ?");
@@ -27277,58 +27493,24 @@ var SqliteFindingsRepository = class {
27277
27493
  d.severity AS severity, f.masked_match AS masked_match,
27278
27494
  f.action_taken AS action_taken, f.confidence AS confidence,
27279
27495
  e.started_at AS occurred_at,
27280
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27281
- json_extract(e.attributes, '$.repo') AS repo,
27282
- json_extract(e.attributes, '$.file_path') AS file,
27283
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27496
+ e.source_tool AS source_tool,
27497
+ e.repo AS repo,
27498
+ e.file_path AS file,
27499
+ e.tool_name AS tool_name,
27284
27500
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27285
27501
  e.event_type AS kind, f.finding_key AS finding_key,
27286
27502
  ${latestResolutionStatusSql("f")} AS latest_status
27287
- FROM inspection_findings f
27288
- JOIN audit_events e ON e.id = f.audit_event_id
27289
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27503
+ FROM audit_events e
27504
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27505
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27290
27506
  WHERE ${conditions.join(" AND ")}
27291
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27292
- ORDER BY e.started_at DESC, f.id DESC
27293
- LIMIT ?`;
27294
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27295
- for (; ; ) {
27296
- const rows = allRows(this.db.prepare(sql), [
27297
- ...params,
27298
- after.startedAtMs,
27299
- after.startedAtMs,
27300
- after.id,
27301
- SCAN_BATCH_ROWS
27302
- ]);
27303
- for (const r of rows) {
27304
- yield {
27305
- id: r.id,
27306
- ruleId: r.rule_id,
27307
- category: r.category,
27308
- severity: r.severity,
27309
- maskedMatch: r.masked_match,
27310
- actionTaken: r.action_taken,
27311
- confidence: r.confidence,
27312
- occurredAt: epochMillisToIso(r.occurred_at),
27313
- sourceTool: r.source_tool,
27314
- repo: r.repo ?? "",
27315
- file: r.file ?? "",
27316
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27317
- eventId: r.event_id,
27318
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27319
- status: deriveInstanceStatus(r)
27320
- };
27321
- }
27322
- if (rows.length < SCAN_BATCH_ROWS) return;
27323
- const lastRow = rows[rows.length - 1];
27324
- if (lastRow === void 0) return;
27325
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27326
- }
27507
+ ORDER BY e.started_at DESC, f.id DESC`;
27508
+ return { sql, params };
27327
27509
  }
27328
27510
  groupAggregates(withSearchText, scope) {
27329
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27330
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27331
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27511
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27512
+ group_concat(DISTINCT e.file_path) AS files,
27513
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27332
27514
  const rows = this.db.prepare(
27333
27515
  `SELECT rule_id,
27334
27516
  sum(tuple_count) AS instance_count,
@@ -27346,7 +27528,7 @@ var SqliteFindingsRepository = class {
27346
27528
  coalesce(latest.status, '') AS status_tuple,
27347
27529
  count(*) AS tuple_count,
27348
27530
  max(e.started_at) AS latest_at,
27349
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27531
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27350
27532
  group_concat(DISTINCT f.action_taken) AS actions_taken
27351
27533
  ${innerSearchColumns}
27352
27534
  FROM inspection_findings f
@@ -27477,6 +27659,8 @@ function isoDay(ms) {
27477
27659
  // ../../packages/persistence/src/repositories/history-sync.ts
27478
27660
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27479
27661
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27662
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27663
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27480
27664
  var SKIPPED = -1;
27481
27665
  var ROW_COLUMNS = `id,
27482
27666
  parent_id AS parentId,
@@ -27516,6 +27700,20 @@ var SqliteHistorySyncRepository = class {
27516
27700
  ORDER BY (event_type = 'session') DESC, started_at
27517
27701
  LIMIT :limit`
27518
27702
  );
27703
+ this.captureRowsStmt = db.prepare(
27704
+ `SELECT ${ROW_COLUMNS}
27705
+ FROM audit_events
27706
+ WHERE synced_at IS NULL
27707
+ AND sync_claimed_at IS NULL
27708
+ AND outbox_owed = 1
27709
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27710
+ AND started_at < :before
27711
+ ORDER BY started_at
27712
+ LIMIT :limit`
27713
+ );
27714
+ this.markOwedStmt = db.prepare(
27715
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27716
+ );
27519
27717
  this.stampStmt = db.prepare(
27520
27718
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27521
27719
  );
@@ -27547,6 +27745,12 @@ var SqliteHistorySyncRepository = class {
27547
27745
  FROM audit_events
27548
27746
  WHERE event_type IN (${TYPE_LIST})`
27549
27747
  );
27748
+ this.captureSkipCountStmt = db.prepare(
27749
+ `SELECT COUNT(*) AS skipped
27750
+ FROM audit_events
27751
+ WHERE synced_at = ${String(SKIPPED)}
27752
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27753
+ );
27550
27754
  this.fingerprintStmt = db.prepare(
27551
27755
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27552
27756
  FROM history_sync WHERE id = 1`
@@ -27556,6 +27760,10 @@ var SqliteHistorySyncRepository = class {
27556
27760
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27557
27761
  WHERE id = 1`
27558
27762
  );
27763
+ this.disownCapturesStmt = db.prepare(
27764
+ `UPDATE audit_events SET outbox_owed = NULL
27765
+ WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27766
+ );
27559
27767
  this.rearmStmt = db.prepare(
27560
27768
  `UPDATE audit_events SET synced_at = NULL
27561
27769
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27628,6 +27836,10 @@ var SqliteHistorySyncRepository = class {
27628
27836
  closeWindowStmt;
27629
27837
  releaseBoundaryStmt;
27630
27838
  freezeBoundaryStmt;
27839
+ captureRowsStmt;
27840
+ markOwedStmt;
27841
+ captureSkipCountStmt;
27842
+ disownCapturesStmt;
27631
27843
  partitionStmt;
27632
27844
  claimRowStmt;
27633
27845
  releaseRowStmt;
@@ -27661,6 +27873,34 @@ var SqliteHistorySyncRepository = class {
27661
27873
  pendingRows(sessionId, limit, before) {
27662
27874
  return allRows(this.rowsStmt, { sessionId, limit, before });
27663
27875
  }
27876
+ /**
27877
+ * Captures this machine still owes the deployment, oldest first.
27878
+ *
27879
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27880
+ * by a time window — see captureRowsStmt for why a window could not express
27881
+ * this. `before` is the grace window that leaves a just-recorded capture to
27882
+ * the live path.
27883
+ */
27884
+ pendingCaptureRows(limit, before) {
27885
+ return allRows(this.captureRowsStmt, { limit, before });
27886
+ }
27887
+ /**
27888
+ * Record that a capture is OWED to the deployment.
27889
+ *
27890
+ * Written by the attached forward path when a live send did not confirm
27891
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27892
+ * a fact rather than an inference: the machine was attached, the send did not
27893
+ * land, so the row is owed — which no time window can state, because the same
27894
+ * window that holds the rows a past attachment left owed also holds every
27895
+ * capture recorded while the machine was DETACHED, and those were never
27896
+ * offered to anyone.
27897
+ *
27898
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27899
+ * out of the drain's read.
27900
+ */
27901
+ markCaptureOwed(id) {
27902
+ this.markOwedStmt.run({ id });
27903
+ }
27664
27904
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27665
27905
  markSynced(ids, atMs) {
27666
27906
  this.stampAll(ids, atMs);
@@ -27744,10 +27984,12 @@ var SqliteHistorySyncRepository = class {
27744
27984
  this.countsStmt,
27745
27985
  { before }
27746
27986
  );
27987
+ const captures = getRow(this.captureSkipCountStmt);
27747
27988
  return {
27748
27989
  pending: row?.pending ?? 0,
27749
27990
  sent: row?.sent ?? 0,
27750
- skipped: row?.skipped ?? 0
27991
+ skipped: row?.skipped ?? 0,
27992
+ capturesSkipped: captures?.skipped ?? 0
27751
27993
  };
27752
27994
  }
27753
27995
  /**
@@ -27788,7 +28030,11 @@ var SqliteHistorySyncRepository = class {
27788
28030
  withTransaction(
27789
28031
  this.db,
27790
28032
  () => {
28033
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27791
28034
  this.rearmStmt.run();
28035
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28036
+ this.disownCapturesStmt.run();
28037
+ }
27792
28038
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27793
28039
  },
27794
28040
  "IMMEDIATE"
@@ -27985,7 +28231,259 @@ var SqliteInspectionFindingsRepository = class {
27985
28231
  };
27986
28232
 
27987
28233
  // ../../packages/persistence/src/repositories/installed-packs.ts
27988
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28234
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28235
+
28236
+ // ../../packages/persistence/src/policy-floor.ts
28237
+ import { readFileSync as readFileSync5 } from "fs";
28238
+ import { join as join6 } from "path";
28239
+
28240
+ // ../../packages/persistence/src/local-layout.ts
28241
+ import { renameSync as renameSync3 } from "fs";
28242
+ import { mkdir } from "fs/promises";
28243
+ import { homedir } from "os";
28244
+ import { join as join4 } from "path";
28245
+ function defaultDataDir() {
28246
+ return join4(homedir(), ".aka");
28247
+ }
28248
+ function settingsDir(base = defaultDataDir()) {
28249
+ return join4(base, "settings");
28250
+ }
28251
+ function dataDir(base = defaultDataDir()) {
28252
+ return join4(base, "data");
28253
+ }
28254
+ function dbPath(base = defaultDataDir()) {
28255
+ return join4(dataDir(base), "aka.db");
28256
+ }
28257
+ function keysDir(base = defaultDataDir()) {
28258
+ return join4(base, "keys");
28259
+ }
28260
+ async function ensureDataDir(dir = defaultDataDir()) {
28261
+ await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
28262
+ tightenDir(dir);
28263
+ }
28264
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28265
+ ensureDataDirSync(dir);
28266
+ }
28267
+ function migrateLegacyLayout(base = defaultDataDir()) {
28268
+ const moves = [
28269
+ { name: "config.json", dest: settingsDir(base) },
28270
+ { name: "policy-cache.json", dest: dataDir(base) }
28271
+ ];
28272
+ for (const { name, dest } of moves) {
28273
+ try {
28274
+ ensureDataDirSync(dest);
28275
+ const moved = join4(dest, name);
28276
+ renameSync3(join4(base, name), moved);
28277
+ tightenFile(moved);
28278
+ } catch {
28279
+ }
28280
+ }
28281
+ }
28282
+
28283
+ // ../../packages/persistence/src/settings.ts
28284
+ import { readFileSync as readFileSync4 } from "fs";
28285
+ import { join as join5 } from "path";
28286
+
28287
+ // ../../packages/persistence/src/file-lock.ts
28288
+ import { randomUUID as randomUUID3 } from "crypto";
28289
+ import {
28290
+ closeSync,
28291
+ existsSync as existsSync2,
28292
+ openSync,
28293
+ readFileSync as readFileSync2,
28294
+ rmSync as rmSync5,
28295
+ statSync as statSync3,
28296
+ writeFileSync as writeFileSync2
28297
+ } from "fs";
28298
+ import { hostname as hostname3 } from "os";
28299
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28300
+
28301
+ // ../../packages/persistence/src/managed-settings.ts
28302
+ import { readFileSync as readFileSync3 } from "fs";
28303
+ import { posix, win32 } from "path";
28304
+ function managedSettingsPaths(platform2 = process.platform) {
28305
+ if (platform2 === "darwin") {
28306
+ return [
28307
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28308
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28309
+ ];
28310
+ }
28311
+ if (platform2 === "win32") {
28312
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28313
+ }
28314
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28315
+ }
28316
+ function readManagedSettings(paths = managedSettingsPaths()) {
28317
+ for (const path of paths) {
28318
+ let text;
28319
+ try {
28320
+ text = readFileSync3(path, "utf8");
28321
+ } catch {
28322
+ continue;
28323
+ }
28324
+ const record2 = parseJsonObject(text);
28325
+ if (!record2) continue;
28326
+ const parsed2 = ManagedSettings.safeParse(record2);
28327
+ if (parsed2.success) return parsed2.data;
28328
+ }
28329
+ return null;
28330
+ }
28331
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28332
+ if (!managed) return settings;
28333
+ const { values } = managed;
28334
+ const merged = { ...settings };
28335
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28336
+ if (values.controlPlane !== void 0) {
28337
+ merged.controlPlane = {
28338
+ ...values.controlPlane,
28339
+ // The administrator pinned WHICH deployment, not WHEN this machine
28340
+ // joined it. Keep the user's own attach time when the endpoint is
28341
+ // unchanged, so a managed machine does not appear to re-attach on every
28342
+ // read; stamp a fresh one when the administrator moved it.
28343
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28344
+ };
28345
+ }
28346
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28347
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28348
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28349
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28350
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28351
+ if (values.vaultConsent !== void 0) {
28352
+ merged.vaultConsent = values.vaultConsent ? (
28353
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28354
+ // at the current version otherwise.
28355
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28356
+ ) : void 0;
28357
+ }
28358
+ if (values.modelJudgeConsent !== void 0) {
28359
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28360
+ acknowledgedAt: now().toISOString(),
28361
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28362
+ } : void 0;
28363
+ }
28364
+ return merged;
28365
+ }
28366
+
28367
+ // ../../packages/persistence/src/settings.ts
28368
+ var SETTINGS_FILENAME = "settings.json";
28369
+ function readWorkspaceSettings(base = defaultDataDir()) {
28370
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28371
+ }
28372
+ function readUserSettings(base) {
28373
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28374
+ if (!record2) return defaultWorkspaceSettings();
28375
+ try {
28376
+ return WorkspaceSettings.parse(record2);
28377
+ } catch {
28378
+ return defaultWorkspaceSettings();
28379
+ }
28380
+ }
28381
+ function readJson(file2) {
28382
+ let text;
28383
+ try {
28384
+ text = readFileSync4(file2, "utf8");
28385
+ } catch {
28386
+ return null;
28387
+ }
28388
+ return parseJsonObject(text) ?? null;
28389
+ }
28390
+
28391
+ // ../../packages/persistence/src/policy-floor.ts
28392
+ function refusalMessage(pack, attempted, floor, refusal) {
28393
+ switch (refusal) {
28394
+ case "lock":
28395
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28396
+ case "disable":
28397
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28398
+ case "floor":
28399
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28400
+ }
28401
+ }
28402
+ var PolicyFloorError = class extends Error {
28403
+ /** `namespace/packId` of the detection whose write was refused. */
28404
+ pack;
28405
+ /**
28406
+ * The archetype the caller asked for, or null when the write named none —
28407
+ * clearing the assignment, or switching the detection off.
28408
+ */
28409
+ attempted;
28410
+ /** The weakest archetype the control plane permits for this pack. */
28411
+ floor;
28412
+ refusal;
28413
+ constructor(pack, attempted, floor, refusal) {
28414
+ super(refusalMessage(pack, attempted, floor, refusal));
28415
+ this.name = "PolicyFloorError";
28416
+ this.pack = pack;
28417
+ this.attempted = attempted;
28418
+ this.floor = floor;
28419
+ this.refusal = refusal;
28420
+ }
28421
+ };
28422
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28423
+ try {
28424
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28425
+ const parsed2 = JSON.parse(raw);
28426
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
28427
+ return PolicyBundle.parse(parsed2.bundle);
28428
+ } catch {
28429
+ return null;
28430
+ }
28431
+ }
28432
+ function indexEnabled(policies) {
28433
+ const byRuleId = /* @__PURE__ */ new Map();
28434
+ const byCategory = /* @__PURE__ */ new Map();
28435
+ for (const policy of policies) {
28436
+ if (!policy.enabled) continue;
28437
+ if ("ruleId" in policy.target) {
28438
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28439
+ } else if (!byCategory.has(policy.target.category)) {
28440
+ byCategory.set(policy.target.category, policy.action);
28441
+ }
28442
+ }
28443
+ return { byRuleId, byCategory };
28444
+ }
28445
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28446
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28447
+ const categories = new Set(rules.map((rule) => rule.category));
28448
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28449
+ return policies.some((policy) => {
28450
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28451
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28452
+ });
28453
+ }
28454
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28455
+ const floors = openControlPlaneFloors(base);
28456
+ return floors === null ? null : floors.floorFor(rules);
28457
+ }
28458
+ function openControlPlaneFloors(base = defaultDataDir()) {
28459
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28460
+ const bundle = readCachedPolicyBundle(base);
28461
+ if (bundle === null) return null;
28462
+ const indexes = indexEnabled(bundle.policies);
28463
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28464
+ }
28465
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28466
+ let action = null;
28467
+ for (const rule of rules) {
28468
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28469
+ if (resolved === void 0) continue;
28470
+ action = action === null ? resolved : strongerAction(action, resolved);
28471
+ }
28472
+ if (action === null) return null;
28473
+ return {
28474
+ floor: weakestBuiltinAtLeast(action),
28475
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28476
+ };
28477
+ }
28478
+ function policyAssignmentRefusal(policyId, floor) {
28479
+ if (floor.locked) return "lock";
28480
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28481
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28482
+ }
28483
+ function packEnablementRefusal(enabled, floor) {
28484
+ if (floor === null || enabled) return null;
28485
+ return "disable";
28486
+ }
27989
28487
 
27990
28488
  // ../../packages/persistence/src/semver.ts
27991
28489
  function parse3(version2) {
@@ -28079,8 +28577,19 @@ function ruleIdsOf(rulesJson) {
28079
28577
  return ids;
28080
28578
  }
28081
28579
  var SqliteInstalledPacksRepository = class {
28082
- constructor(db) {
28580
+ /**
28581
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28582
+ * floor needs both halves of it (settings/ says whether this machine is
28583
+ * attached, data/ holds the cached bundle). It is optional because a caller
28584
+ * holding only a DatabaseSync — every test construction site, and any embedder
28585
+ * that opens the store itself — has no layout to point at, and such a caller
28586
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28587
+ * from `openLocalDatabase`, which is the single construction site that owns a
28588
+ * real `~/.aka`.
28589
+ */
28590
+ constructor(db, baseDir) {
28083
28591
  this.db = db;
28592
+ this.baseDir = baseDir;
28084
28593
  this.insertMissingStmt = db.prepare(
28085
28594
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
28086
28595
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28102,11 +28611,17 @@ var SqliteInstalledPacksRepository = class {
28102
28611
  this.signatureStmt = db.prepare(
28103
28612
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28104
28613
  );
28614
+ this.packRulesStmt = db.prepare(
28615
+ `SELECT rules_json AS rulesJson FROM installed_packs
28616
+ WHERE namespace = ? AND pack_id = ?`
28617
+ );
28105
28618
  }
28106
28619
  db;
28620
+ baseDir;
28107
28621
  insertMissingStmt;
28108
28622
  upsertAvailableStmt;
28109
28623
  signatureStmt;
28624
+ packRulesStmt;
28110
28625
  /**
28111
28626
  * Record the running binary's detection inventory. Refreshes the
28112
28627
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28148,7 +28663,7 @@ var SqliteInstalledPacksRepository = class {
28148
28663
  let behind = false;
28149
28664
  for (const row of rows) {
28150
28665
  const params = {
28151
- id: randomUUID3(),
28666
+ id: randomUUID4(),
28152
28667
  namespace: row.namespace,
28153
28668
  packId: row.packId,
28154
28669
  version: row.version,
@@ -28160,7 +28675,7 @@ var SqliteInstalledPacksRepository = class {
28160
28675
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28161
28676
  this.upsertAvailableStmt.run({
28162
28677
  ...params,
28163
- id: randomUUID3(),
28678
+ id: randomUUID4(),
28164
28679
  recordedBy: meta4?.recordedBy ?? null
28165
28680
  });
28166
28681
  } else {
@@ -28406,9 +28921,65 @@ var SqliteInstalledPacksRepository = class {
28406
28921
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28407
28922
  // caller rather than swallowing them. Each returns whether a row matched, so the
28408
28923
  // caller can tell an edit from a no-such-detection.
28924
+ /**
28925
+ * The rules one installed pack owns, reduced to what a floor computation
28926
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28927
+ * unreadable contributes no rules to a scan either, so it is not a detection
28928
+ * the control plane can be governing, and an empty list correctly imposes no
28929
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28930
+ * the user can re-enable, and its assignment stays governed meanwhile.
28931
+ */
28932
+ packFloorRules(namespace, packId) {
28933
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28934
+ if (!row) return [];
28935
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28936
+ }
28937
+ /**
28938
+ * What the connected control plane imposes on one installed pack, or null on a
28939
+ * machine that is its own authority (standalone, no cached bundle, or a
28940
+ * repository constructed without a layout base).
28941
+ *
28942
+ * Exposed as a READ so a surface can render the constraint — grey out the
28943
+ * choices below the floor, mark a locked detection as locked — rather than
28944
+ * offer the user a picker whose selections it will then be told it may not
28945
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28946
+ */
28947
+ policyFloor(namespace, packId) {
28948
+ if (this.baseDir === void 0) return null;
28949
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28950
+ }
28951
+ /**
28952
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28953
+ * entry only for a pack the control plane actually governs.
28954
+ *
28955
+ * A surface listing every detection asks per pack, and asking through
28956
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28957
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28958
+ * answer, repeated for each row, on every render. This reads all of that once.
28959
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28960
+ * exactly as the single-pack read returns null for them.
28961
+ */
28962
+ policyFloors(packs2) {
28963
+ const floors = /* @__PURE__ */ new Map();
28964
+ if (this.baseDir === void 0) return floors;
28965
+ const source = openControlPlaneFloors(this.baseDir);
28966
+ if (source === null) return floors;
28967
+ for (const pack of packs2) {
28968
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28969
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28970
+ }
28971
+ return floors;
28972
+ }
28409
28973
  /**
28410
28974
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28411
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28975
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28976
+ *
28977
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28978
+ * write below, and a detection the organization has authored a policy for is
28979
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28980
+ * a throw rather than a silently substituted value. This is the one device-local
28981
+ * write path for the assignment, so the check belongs here rather than on any
28982
+ * surface that offers the choice.
28412
28983
  */
28413
28984
  setPolicy(namespace, packId, policyId) {
28414
28985
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28416,14 +28987,38 @@ var SqliteInstalledPacksRepository = class {
28416
28987
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28417
28988
  );
28418
28989
  }
28990
+ const requested = policyId;
28991
+ const floor = this.policyFloor(namespace, packId);
28992
+ if (floor !== null) {
28993
+ const refusal = policyAssignmentRefusal(requested, floor);
28994
+ if (refusal !== null) {
28995
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
28996
+ }
28997
+ }
28419
28998
  const res = this.db.prepare(
28420
28999
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28421
29000
  WHERE namespace = :namespace AND pack_id = :packId`
28422
29001
  ).run({ policyId, now: Date.now(), namespace, packId });
28423
29002
  return Number(res.changes) > 0;
28424
29003
  }
28425
- /** Enable or disable one installed pack. */
29004
+ /**
29005
+ * Enable or disable one installed pack.
29006
+ *
29007
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29008
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29009
+ * merely another point below the floor, and why re-enabling stays open. Like
29010
+ * the assignment above, the check belongs at this write path rather than on a
29011
+ * surface: this is the one device-local writer of the column, and a refusal
29012
+ * that lived in a page would leave the CLI free.
29013
+ */
28426
29014
  setEnabled(namespace, packId, enabled) {
29015
+ const floor = this.policyFloor(namespace, packId);
29016
+ if (floor !== null) {
29017
+ const refusal = packEnablementRefusal(enabled, floor);
29018
+ if (refusal !== null) {
29019
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29020
+ }
29021
+ }
28427
29022
  const res = this.db.prepare(
28428
29023
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28429
29024
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28509,7 +29104,7 @@ var SqliteInventoryRepository = class {
28509
29104
  };
28510
29105
 
28511
29106
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28512
- import { randomUUID as randomUUID4 } from "crypto";
29107
+ import { randomUUID as randomUUID5 } from "crypto";
28513
29108
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28514
29109
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28515
29110
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28998,7 +29593,7 @@ var SqliteInventoryAssetsRepository = class {
28998
29593
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28999
29594
  VALUES (:id, :projectId, :path, :access, :now, :now)
29000
29595
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
29001
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29596
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
29002
29597
  }
29003
29598
  return true;
29004
29599
  }
@@ -29019,7 +29614,7 @@ var SqliteInventoryAssetsRepository = class {
29019
29614
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
29020
29615
  VALUES (:id, :assetId, :trust, :now, :now)
29021
29616
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
29022
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29617
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
29023
29618
  }
29024
29619
  this.configRowsCache = void 0;
29025
29620
  return "ok";
@@ -29316,7 +29911,7 @@ var SqliteInventoryAssetsRepository = class {
29316
29911
  };
29317
29912
 
29318
29913
  // ../../packages/persistence/src/repositories/policies.ts
29319
- import { randomUUID as randomUUID5 } from "crypto";
29914
+ import { randomUUID as randomUUID6 } from "crypto";
29320
29915
  var SqlitePoliciesRepository = class {
29321
29916
  constructor(db) {
29322
29917
  this.db = db;
@@ -29351,7 +29946,7 @@ var SqlitePoliciesRepository = class {
29351
29946
  failOpenTransaction(this.db, () => {
29352
29947
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29353
29948
  stmt.run({
29354
- id: randomUUID5(),
29949
+ id: randomUUID6(),
29355
29950
  target: JSON.stringify({ category }),
29356
29951
  action,
29357
29952
  now: Date.now()
@@ -29371,7 +29966,7 @@ var SqlitePoliciesRepository = class {
29371
29966
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29372
29967
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29373
29968
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29374
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29969
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29375
29970
  }
29376
29971
  // Caps every global per-category policy currently set to block/redact down
29377
29972
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29439,7 +30034,7 @@ var SqlitePolicyCatalogRepository = class {
29439
30034
  };
29440
30035
 
29441
30036
  // ../../packages/persistence/src/repositories/project-files.ts
29442
- import { randomUUID as randomUUID6 } from "crypto";
30037
+ import { randomUUID as randomUUID7 } from "crypto";
29443
30038
  var SqliteProjectFilesRepository = class {
29444
30039
  constructor(db) {
29445
30040
  this.db = db;
@@ -29471,7 +30066,7 @@ var SqliteProjectFilesRepository = class {
29471
30066
  const stamp = Math.max(now, maxStamp + 1);
29472
30067
  for (const file2 of scan2.files) {
29473
30068
  this.upsertStmt.run({
29474
- id: randomUUID6(),
30069
+ id: randomUUID7(),
29475
30070
  projectId,
29476
30071
  path: file2.path,
29477
30072
  name: file2.name,
@@ -29485,9 +30080,9 @@ var SqliteProjectFilesRepository = class {
29485
30080
  };
29486
30081
 
29487
30082
  // ../../packages/persistence/src/repositories/resolutions.ts
29488
- import { randomUUID as randomUUID7 } from "crypto";
30083
+ import { randomUUID as randomUUID8 } from "crypto";
29489
30084
  var SqliteResolutionsRepository = class {
29490
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30085
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29491
30086
  this.db = db;
29492
30087
  this.now = now;
29493
30088
  this.newId = newId;
@@ -29700,7 +30295,7 @@ var SqliteScanLedgerRepository = class {
29700
30295
  };
29701
30296
 
29702
30297
  // ../../packages/persistence/src/repositories/secret-vault.ts
29703
- import { randomUUID as randomUUID8 } from "crypto";
30298
+ import { randomUUID as randomUUID9 } from "crypto";
29704
30299
  function pageLimit(requested, fallback) {
29705
30300
  if (requested === void 0) return fallback;
29706
30301
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29746,12 +30341,14 @@ var SELECT_COLUMNS = `
29746
30341
  ciphertext,
29747
30342
  nonce,
29748
30343
  auth_tag AS authTag,
30344
+ user_authorized AS userAuthorized,
29749
30345
  occurrence_count AS occurrenceCount,
29750
30346
  first_seen AS firstSeen,
29751
30347
  last_seen AS lastSeen`;
29752
30348
  function toRow(raw) {
29753
- const { provider, ...rest } = raw;
29754
- return provider === null ? rest : { ...rest, provider };
30349
+ const { provider, userAuthorized, ...rest } = raw;
30350
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30351
+ return provider === null ? row : { ...row, provider };
29755
30352
  }
29756
30353
  var SqliteSecretVaultRepository = class {
29757
30354
  constructor(db) {
@@ -29761,17 +30358,18 @@ var SqliteSecretVaultRepository = class {
29761
30358
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29762
30359
  format_version, category, rule_id, masked_match, provider,
29763
30360
  ciphertext, nonce, auth_tag,
29764
- occurrence_count, first_seen, last_seen
30361
+ user_authorized, occurrence_count, first_seen, last_seen
29765
30362
  ) VALUES (
29766
30363
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29767
30364
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29768
30365
  :ciphertext, :nonce, :authTag,
29769
- 1, :now, :now
30366
+ :userAuthorized, 1, :now, :now
29770
30367
  )`
29771
30368
  );
29772
30369
  this.bumpStmt = db.prepare(
29773
30370
  `UPDATE secret_vault
29774
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30371
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30372
+ user_authorized = max(user_authorized, :userAuthorized)
29775
30373
  WHERE value_fingerprint = :valueFingerprint`
29776
30374
  );
29777
30375
  this.byPointerStmt = db.prepare(
@@ -29791,6 +30389,7 @@ var SqliteSecretVaultRepository = class {
29791
30389
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29792
30390
  WHERE pointer_id = :pointerId`
29793
30391
  );
30392
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29794
30393
  this.derefStmt = db.prepare(
29795
30394
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29796
30395
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29804,6 +30403,7 @@ var SqliteSecretVaultRepository = class {
29804
30403
  listStmt;
29805
30404
  replaceCiphertextStmt;
29806
30405
  refreshFingerprintStmt;
30406
+ deleteByPointerStmt;
29807
30407
  derefStmt;
29808
30408
  /**
29809
30409
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29812,6 +30412,11 @@ var SqliteSecretVaultRepository = class {
29812
30412
  * pointer, category and ciphertext, so the same secret always resolves to one
29813
30413
  * wire token. `minted` is true only when this call created the row.
29814
30414
  *
30415
+ * `userAuthorized` is the one field a repeat call may still change, and only
30416
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30417
+ * the row is shared with every automatic path that vaults the same value. See
30418
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30419
+ *
29815
30420
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29816
30421
  * writers cannot both decide they are minting.
29817
30422
  */
@@ -29838,13 +30443,18 @@ var SqliteSecretVaultRepository = class {
29838
30443
  ciphertext: input2.ciphertext,
29839
30444
  nonce: input2.nonce,
29840
30445
  authTag: input2.authTag,
30446
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29841
30447
  now
29842
30448
  })
29843
30449
  );
29844
30450
  minted = true;
29845
30451
  return;
29846
30452
  }
29847
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30453
+ this.bumpStmt.run({
30454
+ valueFingerprint: input2.valueFingerprint,
30455
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30456
+ now
30457
+ });
29848
30458
  },
29849
30459
  "IMMEDIATE"
29850
30460
  );
@@ -29904,6 +30514,42 @@ var SqliteSecretVaultRepository = class {
29904
30514
  );
29905
30515
  return destroyed;
29906
30516
  }
30517
+ /**
30518
+ * Destroy the named entries and report WHICH ones went — the scoped
30519
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30520
+ * values back where they came from. Ids the store does not hold are absent
30521
+ * from the answer rather than an error, so a set assembled from a stale read
30522
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30523
+ * it.
30524
+ *
30525
+ * The ids come back rather than a count because the caller's next act is to
30526
+ * write a purge row per destroyed entry, and a record of destruction has to
30527
+ * be a record of what was really destroyed: a selection is a claim about a
30528
+ * read that has since gone stale, and auditing from it invents a purge for an
30529
+ * entry still sitting in the vault.
30530
+ *
30531
+ * One transaction over the whole set rather than a statement per id: the
30532
+ * caller hands this the result of a restore pass it has completed, and a
30533
+ * fault partway through must leave the vault as it was found rather than
30534
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30535
+ * stands for, so half a delete is not a state anything can recover from.
30536
+ */
30537
+ deleteByPointerIds(pointerIds) {
30538
+ if (pointerIds.length === 0) return [];
30539
+ const deleted = [];
30540
+ withTransaction(
30541
+ this.db,
30542
+ () => {
30543
+ for (const pointerId of pointerIds) {
30544
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30545
+ deleted.push(pointerId);
30546
+ }
30547
+ }
30548
+ },
30549
+ "IMMEDIATE"
30550
+ );
30551
+ return deleted;
30552
+ }
29907
30553
  /**
29908
30554
  * Record (or re-stamp) one place a pointer has been written. One row per
29909
30555
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29916,7 +30562,7 @@ var SqliteSecretVaultRepository = class {
29916
30562
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29917
30563
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29918
30564
  ).run({
29919
- id: randomUUID8(),
30565
+ id: randomUUID9(),
29920
30566
  pointerId: entry.pointerId,
29921
30567
  location: entry.location,
29922
30568
  kind: entry.kind,
@@ -30429,15 +31075,15 @@ var SqliteSecurityRepository = class {
30429
31075
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30430
31076
  const rows = allRows(
30431
31077
  this.db.prepare(
30432
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31078
+ `SELECT e.repo AS repo, count(*) AS c
30433
31079
  FROM inspection_findings f
30434
31080
  JOIN audit_events e ON e.id = f.audit_event_id
30435
31081
  WHERE e.started_at >= :from AND e.started_at < :to
30436
31082
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30437
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30438
- AND json_extract(e.attributes, '$.repo') != ''
30439
- GROUP BY repo
30440
- ORDER BY c DESC, repo
31083
+ AND e.repo IS NOT NULL
31084
+ AND e.repo != ''
31085
+ GROUP BY e.repo
31086
+ ORDER BY c DESC, e.repo
30441
31087
  LIMIT :limit`
30442
31088
  ),
30443
31089
  { from, to: now, limit }
@@ -30499,7 +31145,7 @@ var SqliteSecurityRepository = class {
30499
31145
  `SELECT f.finding_key AS finding_key,
30500
31146
  d.rule_id AS rule_id,
30501
31147
  d.severity AS severity,
30502
- json_extract(e.attributes, '$.file_path') AS path,
31148
+ e.file_path AS path,
30503
31149
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30504
31150
  latest.resolved_at AS latest_resolved_at
30505
31151
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30552,7 +31198,7 @@ var SqliteSecurityRepository = class {
30552
31198
  };
30553
31199
 
30554
31200
  // ../../packages/persistence/src/repositories/shares.ts
30555
- import { randomUUID as randomUUID9 } from "crypto";
31201
+ import { randomUUID as randomUUID10 } from "crypto";
30556
31202
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30557
31203
  var IN_CHUNK = 500;
30558
31204
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30640,7 +31286,7 @@ function buildSummary(dest, endpoints) {
30640
31286
  callSiteCount,
30641
31287
  transports: distinctTransports(transports),
30642
31288
  dataClasses: distinctDataClasses(dataClasses),
30643
- review: buildReviewInfo(dest.trust, transports),
31289
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30644
31290
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30645
31291
  endpoints: endpoints.map(toEndpointSummary)
30646
31292
  };
@@ -30667,7 +31313,7 @@ function buildDetail(dest, endpoints, callSites) {
30667
31313
  lastSeen: new Date(lastSeenMs).toISOString(),
30668
31314
  transports: distinctTransports(transports),
30669
31315
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30670
- review: buildReviewInfo(dest.trust, transports),
31316
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30671
31317
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30672
31318
  note: dest.note,
30673
31319
  endpoints: endpoints.map((ep) => ({
@@ -30696,7 +31342,11 @@ var SqliteSharesRepository = class {
30696
31342
  FROM share_destination d
30697
31343
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30698
31344
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30699
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31345
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31346
+ AND NOT EXISTS (
31347
+ SELECT 1 FROM egress_decision_override o
31348
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31349
+ )`
30700
31350
  );
30701
31351
  const kindCounts = countBy(
30702
31352
  this.db,
@@ -30808,7 +31458,7 @@ var SqliteSharesRepository = class {
30808
31458
  (id, destination_id, host, decision, created_at, updated_at)
30809
31459
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30810
31460
  ).run({
30811
- id: randomUUID9(),
31461
+ id: randomUUID10(),
30812
31462
  destinationId,
30813
31463
  host: dest.host,
30814
31464
  decision,
@@ -30957,7 +31607,7 @@ var SqliteSharesRepository = class {
30957
31607
  let destinationId = destIds.get(hit.host);
30958
31608
  if (destinationId === void 0) {
30959
31609
  destStmt.run({
30960
- id: randomUUID9(),
31610
+ id: randomUUID10(),
30961
31611
  kind: hit.kind,
30962
31612
  name: hit.name,
30963
31613
  host: hit.host,
@@ -30973,7 +31623,7 @@ var SqliteSharesRepository = class {
30973
31623
  let endpointId = endpointIds.get(endpointKey);
30974
31624
  if (endpointId === void 0) {
30975
31625
  endpointStmt.run({
30976
- id: randomUUID9(),
31626
+ id: randomUUID10(),
30977
31627
  destinationId,
30978
31628
  method: hit.method,
30979
31629
  transport: hit.transport,
@@ -30986,7 +31636,7 @@ var SqliteSharesRepository = class {
30986
31636
  endpointIds.set(endpointKey, endpointId);
30987
31637
  }
30988
31638
  siteStmt.run({
30989
- id: randomUUID9(),
31639
+ id: randomUUID10(),
30990
31640
  endpointId,
30991
31641
  project: input2.project,
30992
31642
  projectKey: input2.projectKey,
@@ -31351,6 +32001,7 @@ function purgeSampleData(db) {
31351
32001
  }
31352
32002
 
31353
32003
  // ../../packages/persistence/src/database.ts
32004
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31354
32005
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31355
32006
  "aka.persistence.unsafeTestOnlyRawHandle"
31356
32007
  );
@@ -31398,7 +32049,7 @@ function backupLegacyStore(db, file2) {
31398
32049
  discardStore(file2, backup);
31399
32050
  return backup;
31400
32051
  }
31401
- function openAndInitialize(file2) {
32052
+ function openAndInitialize(file2, base) {
31402
32053
  let db = openWithPragmas(file2);
31403
32054
  try {
31404
32055
  if (isForeignSqliteLineage(db)) {
@@ -31411,7 +32062,7 @@ function openAndInitialize(file2) {
31411
32062
  applyMigrations(db, file2);
31412
32063
  tightenPerms(file2);
31413
32064
  const policies = new SqlitePoliciesRepository(db);
31414
- const installedPacks = new SqliteInstalledPacksRepository(db);
32065
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31415
32066
  const repositories = {
31416
32067
  events: new SqliteEventsRepository(db),
31417
32068
  findings: new SqliteFindingsRepository(db),
@@ -31447,7 +32098,7 @@ function openAndInitialize(file2) {
31447
32098
  }
31448
32099
  function openLocalDatabase(dir) {
31449
32100
  ensureDataDirSync(dir);
31450
- const file2 = join4(dir, DB_FILENAME);
32101
+ const file2 = join7(dir, DB_FILENAME);
31451
32102
  reapStalePartials(file2);
31452
32103
  const {
31453
32104
  db,
@@ -31475,7 +32126,13 @@ function openLocalDatabase(dir) {
31475
32126
  inspectionDefinitions,
31476
32127
  inspectionFindings,
31477
32128
  configInventory
31478
- } = openAndInitialize(file2);
32129
+ } = openAndInitialize(
32130
+ file2,
32131
+ // `dir` is always `<base>/data` — every caller resolves it through
32132
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32133
+ // settings/ and data/, and the pack-policy floor needs both halves.
32134
+ dirname2(dir)
32135
+ );
31479
32136
  function captureRowId(event) {
31480
32137
  return captureId(
31481
32138
  event.metadata?.sessionId ?? null,
@@ -31488,6 +32145,21 @@ function openLocalDatabase(dir) {
31488
32145
  historySync.markSynced([captureRowId(event)], atMs);
31489
32146
  });
31490
32147
  }
32148
+ function markCaptureOwed(event) {
32149
+ failOpenTransaction(db, () => {
32150
+ historySync.markCaptureOwed(captureRowId(event));
32151
+ });
32152
+ }
32153
+ function markAuditEventsDelivered(events2, atMs) {
32154
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32155
+ if (stampable.length === 0) return;
32156
+ failOpenTransaction(db, () => {
32157
+ historySync.markSynced(
32158
+ stampable.map((event) => event.id),
32159
+ atMs
32160
+ );
32161
+ });
32162
+ }
31491
32163
  function recordCapture(event, detected) {
31492
32164
  failOpenTransaction(db, () => {
31493
32165
  const sessionId = event.metadata?.sessionId;
@@ -31574,7 +32246,7 @@ function openLocalDatabase(dir) {
31574
32246
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31575
32247
  if (!definitionId) continue;
31576
32248
  inspectionFindings.insertFinding({
31577
- id: randomUUID10(),
32249
+ id: randomUUID11(),
31578
32250
  auditEventId: record2.scanEvent.id,
31579
32251
  inspectionDefinitionId: definitionId,
31580
32252
  span: finding.span,
@@ -31670,6 +32342,8 @@ function openLocalDatabase(dir) {
31670
32342
  inspectionFindings,
31671
32343
  recordCapture,
31672
32344
  markCaptureDelivered,
32345
+ markCaptureOwed,
32346
+ markAuditEventsDelivered,
31673
32347
  ensureInventory,
31674
32348
  recordConfigScan,
31675
32349
  recordProjectFiles,
@@ -31708,20 +32382,6 @@ var UserGrantPolicyProvider = class {
31708
32382
  }
31709
32383
  };
31710
32384
 
31711
- // ../../packages/persistence/src/file-lock.ts
31712
- import { randomUUID as randomUUID11 } from "crypto";
31713
- import {
31714
- closeSync,
31715
- existsSync as existsSync2,
31716
- openSync,
31717
- readFileSync as readFileSync2,
31718
- rmSync as rmSync5,
31719
- statSync as statSync3,
31720
- writeFileSync as writeFileSync2
31721
- } from "fs";
31722
- import { hostname as hostname3 } from "os";
31723
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31724
-
31725
32385
  // ../../packages/persistence/src/finding-key.ts
31726
32386
  import { createHash as createHash3 } from "crypto";
31727
32387
  function normalizeFilePath(filePath) {
@@ -31734,13 +32394,13 @@ function computeFindingKey(input2) {
31734
32394
 
31735
32395
  // ../../packages/persistence/src/fingerprint.ts
31736
32396
  import { createHmac, randomBytes } from "crypto";
31737
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31738
- import { join as join5 } from "path";
32397
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32398
+ import { join as join8 } from "path";
31739
32399
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31740
32400
  var EXCEPTION_KEY_FILENAME = "exception.key";
31741
32401
  var KEY_MATERIAL_BYTES = 32;
31742
32402
  function keyFilePath(dataDir2) {
31743
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32403
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31744
32404
  }
31745
32405
  function parseKeyFile(raw) {
31746
32406
  const parsed2 = JSON.parse(raw);
@@ -31778,7 +32438,7 @@ var FloorUnreadableError = class extends Error {
31778
32438
  }
31779
32439
  };
31780
32440
  function storedKeyVersionFloor(dataDir2) {
31781
- const file2 = join5(dataDir2, DB_FILENAME);
32441
+ const file2 = join8(dataDir2, DB_FILENAME);
31782
32442
  if (!existsSync3(file2)) return 0;
31783
32443
  let db;
31784
32444
  try {
@@ -31833,7 +32493,7 @@ function occupantMessage(file2, kind) {
31833
32493
  function readFingerprintKey(dataDir2) {
31834
32494
  let raw;
31835
32495
  try {
31836
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32496
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31837
32497
  } catch (err) {
31838
32498
  if (err.code === "ENOENT") return null;
31839
32499
  throw err instanceof Error ? err : new Error(String(err));
@@ -31857,146 +32517,12 @@ function fingerprintValue(key, raw) {
31857
32517
 
31858
32518
  // ../../packages/persistence/src/history-preview.ts
31859
32519
  import { existsSync as existsSync4 } from "fs";
31860
- import { join as join6 } from "path";
32520
+ import { join as join9 } from "path";
31861
32521
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31862
32522
 
31863
- // ../../packages/persistence/src/local-layout.ts
31864
- import { renameSync as renameSync3 } from "fs";
31865
- import { mkdir } from "fs/promises";
31866
- import { homedir } from "os";
31867
- import { join as join7 } from "path";
31868
- function defaultDataDir() {
31869
- return join7(homedir(), ".aka");
31870
- }
31871
- function settingsDir(base = defaultDataDir()) {
31872
- return join7(base, "settings");
31873
- }
31874
- function dataDir(base = defaultDataDir()) {
31875
- return join7(base, "data");
31876
- }
31877
- function dbPath(base = defaultDataDir()) {
31878
- return join7(dataDir(base), "aka.db");
31879
- }
31880
- function keysDir(base = defaultDataDir()) {
31881
- return join7(base, "keys");
31882
- }
31883
- async function ensureDataDir(dir = defaultDataDir()) {
31884
- await mkdir(dir, { recursive: true, mode: DATA_DIR_MODE });
31885
- tightenDir(dir);
31886
- }
31887
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31888
- ensureDataDirSync(dir);
31889
- }
31890
- function migrateLegacyLayout(base = defaultDataDir()) {
31891
- const moves = [
31892
- { name: "config.json", dest: settingsDir(base) },
31893
- { name: "policy-cache.json", dest: dataDir(base) }
31894
- ];
31895
- for (const { name, dest } of moves) {
31896
- try {
31897
- ensureDataDirSync(dest);
31898
- const moved = join7(dest, name);
31899
- renameSync3(join7(base, name), moved);
31900
- tightenFile(moved);
31901
- } catch {
31902
- }
31903
- }
31904
- }
31905
-
31906
- // ../../packages/persistence/src/managed-settings.ts
31907
- import { readFileSync as readFileSync4 } from "fs";
31908
- import { posix, win32 } from "path";
31909
- function managedSettingsPaths(platform2 = process.platform) {
31910
- if (platform2 === "darwin") {
31911
- return [
31912
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31913
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31914
- ];
31915
- }
31916
- if (platform2 === "win32") {
31917
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31918
- }
31919
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31920
- }
31921
- function readManagedSettings(paths = managedSettingsPaths()) {
31922
- for (const path of paths) {
31923
- let text;
31924
- try {
31925
- text = readFileSync4(path, "utf8");
31926
- } catch {
31927
- continue;
31928
- }
31929
- const record2 = parseJsonObject(text);
31930
- if (!record2) continue;
31931
- const parsed2 = ManagedSettings.safeParse(record2);
31932
- if (parsed2.success) return parsed2.data;
31933
- }
31934
- return null;
31935
- }
31936
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31937
- if (!managed) return settings;
31938
- const { values } = managed;
31939
- const merged = { ...settings };
31940
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31941
- if (values.controlPlane !== void 0) {
31942
- merged.controlPlane = {
31943
- ...values.controlPlane,
31944
- // The administrator pinned WHICH deployment, not WHEN this machine
31945
- // joined it. Keep the user's own attach time when the endpoint is
31946
- // unchanged, so a managed machine does not appear to re-attach on every
31947
- // read; stamp a fresh one when the administrator moved it.
31948
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31949
- };
31950
- }
31951
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31952
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31953
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31954
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31955
- if (values.vaultConsent !== void 0) {
31956
- merged.vaultConsent = values.vaultConsent ? (
31957
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31958
- // at the current version otherwise.
31959
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31960
- ) : void 0;
31961
- }
31962
- if (values.modelJudgeConsent !== void 0) {
31963
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31964
- acknowledgedAt: now().toISOString(),
31965
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31966
- } : void 0;
31967
- }
31968
- return merged;
31969
- }
31970
-
31971
- // ../../packages/persistence/src/settings.ts
31972
- import { readFileSync as readFileSync5 } from "fs";
31973
- import { join as join8 } from "path";
31974
- var SETTINGS_FILENAME = "settings.json";
31975
- function readWorkspaceSettings(base = defaultDataDir()) {
31976
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31977
- }
31978
- function readUserSettings(base) {
31979
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31980
- if (!record2) return defaultWorkspaceSettings();
31981
- try {
31982
- return WorkspaceSettings.parse(record2);
31983
- } catch {
31984
- return defaultWorkspaceSettings();
31985
- }
31986
- }
31987
- function readJson(file2) {
31988
- let text;
31989
- try {
31990
- text = readFileSync5(file2, "utf8");
31991
- } catch {
31992
- return null;
31993
- }
31994
- return parseJsonObject(text) ?? null;
31995
- }
31996
-
31997
32523
  // ../../packages/persistence/src/store-symlinks.ts
31998
32524
  import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31999
- import { dirname as dirname2, join as join9, resolve } from "path";
32525
+ import { dirname as dirname3, join as join10, resolve } from "path";
32000
32526
  var STORE_DB = "the store database (including the prompt corpus)";
32001
32527
  var STORE_SETTINGS = "your settings file";
32002
32528
  function storeContents(home) {
@@ -32005,7 +32531,7 @@ function storeContents(home) {
32005
32531
  [settingsDir(home), STORE_SETTINGS],
32006
32532
  [dataDir(home), STORE_DB],
32007
32533
  [keysDir(home), "the vault key"],
32008
- [join9(settingsDir(home), "settings.json"), STORE_SETTINGS],
32534
+ [join10(settingsDir(home), "settings.json"), STORE_SETTINGS],
32009
32535
  [dbPath(home), STORE_DB]
32010
32536
  ]);
32011
32537
  }
@@ -32033,7 +32559,7 @@ function linkTarget(path) {
32033
32559
  try {
32034
32560
  return realpathSync(path);
32035
32561
  } catch {
32036
- return resolve(dirname2(path), readlinkSync(path));
32562
+ return resolve(dirname3(path), readlinkSync(path));
32037
32563
  }
32038
32564
  }
32039
32565
  function targetMode(path, platform2) {
@@ -32156,8 +32682,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32156
32682
  // ../../packages/persistence/src/vault/key-provider.ts
32157
32683
  import { execFileSync } from "child_process";
32158
32684
  import { randomBytes as randomBytes2 } from "crypto";
32159
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32160
- import { join as join10 } from "path";
32685
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32686
+ import { join as join11 } from "path";
32161
32687
  var VAULT_OCCUPANT_REASON = {
32162
32688
  symlink: "the path is a symlink; remove it so a keyring can be created",
32163
32689
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32256,7 +32782,7 @@ function claimRotationLock(lock, owner) {
32256
32782
  throw asError(err);
32257
32783
  }
32258
32784
  try {
32259
- writeFileSync3(join10(lock, LOCK_OWNER_FILE), `${owner}
32785
+ writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
32260
32786
  `, { mode: DATA_FILE_MODE });
32261
32787
  return true;
32262
32788
  } catch (err) {
@@ -32265,7 +32791,7 @@ function claimRotationLock(lock, owner) {
32265
32791
  }
32266
32792
  }
32267
32793
  function acquireRotationLock(keysDir2) {
32268
- const lock = join10(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32794
+ const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32269
32795
  const owner = randomBytes2(16).toString("hex");
32270
32796
  if (claimRotationLock(lock, owner)) return { lock, owner };
32271
32797
  let held;
@@ -32292,7 +32818,7 @@ function acquireRotationLock(keysDir2) {
32292
32818
  }
32293
32819
  function releaseRotationLock(lease) {
32294
32820
  try {
32295
- if (readFileSync6(join10(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32821
+ if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32296
32822
  } catch {
32297
32823
  return;
32298
32824
  }
@@ -32313,7 +32839,7 @@ var FileKeyProvider = class {
32313
32839
  this.#keysDir = keysDir2;
32314
32840
  }
32315
32841
  get filePath() {
32316
- return join10(this.#keysDir, VAULT_KEY_FILENAME);
32842
+ return join11(this.#keysDir, VAULT_KEY_FILENAME);
32317
32843
  }
32318
32844
  loadOrCreate() {
32319
32845
  return asAsync(() => {
@@ -32343,7 +32869,7 @@ var FileKeyProvider = class {
32343
32869
  #read() {
32344
32870
  let raw;
32345
32871
  try {
32346
- raw = readFileSync6(this.filePath, "utf8");
32872
+ raw = readFileSync7(this.filePath, "utf8");
32347
32873
  } catch (err) {
32348
32874
  if (err.code === "ENOENT") return null;
32349
32875
  throw err instanceof Error ? err : new Error(String(err));
@@ -32630,7 +33156,14 @@ var SecretVault = class {
32630
33156
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
32631
33157
  const now = this.#now();
32632
33158
  if (existing) {
32633
- this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
33159
+ this.#repo.upsert(
33160
+ {
33161
+ ...existing,
33162
+ provider: existing.provider ?? void 0,
33163
+ userAuthorized: meta4.userAuthorized === true
33164
+ },
33165
+ now
33166
+ );
32634
33167
  return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
32635
33168
  }
32636
33169
  const { material, version: version2 } = await this.#keys.loadOrCreate();
@@ -32652,6 +33185,7 @@ var SecretVault = class {
32652
33185
  ruleId: meta4.ruleId,
32653
33186
  maskedMatch: meta4.maskedMatch,
32654
33187
  provider: meta4.provider,
33188
+ userAuthorized: meta4.userAuthorized === true,
32655
33189
  ciphertext: sealed.ciphertext.toString("base64"),
32656
33190
  nonce: sealed.nonce.toString("base64"),
32657
33191
  authTag: sealed.authTag.toString("base64")
@@ -32971,11 +33505,11 @@ var SecretVault = class {
32971
33505
 
32972
33506
  // ../../packages/persistence/src/warn-era-cap.ts
32973
33507
  import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32974
- import { join as join11 } from "path";
33508
+ import { join as join12 } from "path";
32975
33509
  var MARKER = "warn-era-capped";
32976
33510
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32977
33511
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32978
- const marker = join11(dataDir2, MARKER);
33512
+ const marker = join12(dataDir2, MARKER);
32979
33513
  if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32980
33514
  const capped = db.policies.capCategoryActions();
32981
33515
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -33035,7 +33569,7 @@ function resolveProvider() {
33035
33569
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33036
33570
  try {
33037
33571
  ensureLayoutDirSync(base);
33038
- const settingsFile = join12(settingsDir(base), "settings.json");
33572
+ const settingsFile = join13(settingsDir(base), "settings.json");
33039
33573
  if (existsSync7(settingsFile)) tightenFile(settingsFile);
33040
33574
  } catch {
33041
33575
  }
@@ -33059,9 +33593,9 @@ function resolveProviderSafe(resolveProviderFn) {
33059
33593
  }
33060
33594
 
33061
33595
  // ../../packages/plugin-sdk/src/config-inventory.ts
33062
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33596
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33063
33597
  import { homedir as homedir2 } from "os";
33064
- import { basename as basename3, join as join14 } from "path";
33598
+ import { basename as basename3, join as join15 } from "path";
33065
33599
 
33066
33600
  // ../../packages/detections/src/egress/registry.ts
33067
33601
  var EXTRACTOR_VERSION = "1";
@@ -36150,8 +36684,8 @@ function bundledDetections() {
36150
36684
  }
36151
36685
 
36152
36686
  // ../../packages/plugin-sdk/src/repo.ts
36153
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
36154
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
36687
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36688
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
36155
36689
  function resolveRepo(cwd) {
36156
36690
  try {
36157
36691
  const root = findGitRoot(cwd);
@@ -36166,36 +36700,36 @@ function resolveRepo(cwd) {
36166
36700
  function findGitRoot(start) {
36167
36701
  let dir = start;
36168
36702
  for (; ; ) {
36169
- if (existsSync8(join13(dir, ".git"))) return dir;
36170
- const parent = dirname3(dir);
36703
+ if (existsSync8(join14(dir, ".git"))) return dir;
36704
+ const parent = dirname4(dir);
36171
36705
  if (parent === dir) return void 0;
36172
36706
  dir = parent;
36173
36707
  }
36174
36708
  }
36175
36709
  function resolveGitContext(root) {
36176
- const dotGit = join13(root, ".git");
36710
+ const dotGit = join14(root, ".git");
36177
36711
  try {
36178
36712
  if (statSync6(dotGit).isDirectory()) {
36179
- return { configPath: join13(dotGit, "config"), headRoot: root };
36713
+ return { configPath: join14(dotGit, "config"), headRoot: root };
36180
36714
  }
36181
36715
  } catch {
36182
36716
  return void 0;
36183
36717
  }
36184
36718
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
36185
36719
  if (!target) return void 0;
36186
- const gitdir = isAbsolute(target) ? target : join13(root, target);
36187
- if (existsSync8(join13(gitdir, "config"))) {
36188
- return { configPath: join13(gitdir, "config"), headRoot: root };
36720
+ const gitdir = isAbsolute(target) ? target : join14(root, target);
36721
+ if (existsSync8(join14(gitdir, "config"))) {
36722
+ return { configPath: join14(gitdir, "config"), headRoot: root };
36189
36723
  }
36190
- const commonRaw = safeRead(join13(gitdir, "commondir"))?.trim();
36724
+ const commonRaw = safeRead(join14(gitdir, "commondir"))?.trim();
36191
36725
  if (!commonRaw) return void 0;
36192
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join13(gitdir, commonRaw);
36193
- const headRoot = basename2(commonGitDir) === ".git" ? dirname3(commonGitDir) : root;
36194
- return { configPath: join13(commonGitDir, "config"), headRoot };
36726
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join14(gitdir, commonRaw);
36727
+ const headRoot = basename2(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
36728
+ return { configPath: join14(commonGitDir, "config"), headRoot };
36195
36729
  }
36196
36730
  function safeRead(path) {
36197
36731
  try {
36198
- return readFileSync7(path, "utf8");
36732
+ return readFileSync8(path, "utf8");
36199
36733
  } catch {
36200
36734
  return void 0;
36201
36735
  }
@@ -36740,8 +37274,8 @@ function createGuardedScanner(partition, gateway, opts) {
36740
37274
 
36741
37275
  // ../../packages/plugin-sdk/src/ignore-layers.ts
36742
37276
  var import_ignore = __toESM(require_ignore(), 1);
36743
- import { readFileSync as readFileSync9 } from "fs";
36744
- import { join as join15 } from "path";
37277
+ import { readFileSync as readFileSync10 } from "fs";
37278
+ import { join as join16 } from "path";
36745
37279
 
36746
37280
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
36747
37281
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -36752,24 +37286,115 @@ import {
36752
37286
  fstatSync,
36753
37287
  mkdirSync as mkdirSync2,
36754
37288
  openSync as openSync2,
36755
- readFileSync as readFileSync10,
37289
+ readFileSync as readFileSync11,
36756
37290
  readSync,
36757
37291
  writeFileSync as writeFileSync5
36758
37292
  } from "fs";
36759
- import { join as join16 } from "path";
37293
+ import { join as join17 } from "path";
37294
+ var DATE_SUFFIX = /-\d{8}$/;
37295
+ function normalizeModelId(model) {
37296
+ return model.trim().toLowerCase().replace(DATE_SUFFIX, "");
37297
+ }
37298
+ var SPAWN_INHERIT_WORDS = /* @__PURE__ */ new Set(["inherit", "default"]);
37299
+ function isBoundary(char) {
37300
+ return char === "" || /[^a-z0-9]/.test(char);
37301
+ }
37302
+ function matchProhibitedSpawnModel(requested, prohibited) {
37303
+ if (requested === void 0 || requested === "") return void 0;
37304
+ if (prohibited === void 0 || prohibited.length === 0) return void 0;
37305
+ const needle = normalizeModelId(requested);
37306
+ if (needle === "" || SPAWN_INHERIT_WORDS.has(needle)) return void 0;
37307
+ const exact = prohibited.find((p) => normalizeModelId(p) === needle);
37308
+ if (exact !== void 0) return exact;
37309
+ if (!needle.includes("-")) {
37310
+ return prohibited.find((p) => normalizeModelId(p).split("-").includes(needle));
37311
+ }
37312
+ return prohibited.find((p) => {
37313
+ const base = normalizeModelId(p);
37314
+ if (base === "") return false;
37315
+ let at = needle.indexOf(base);
37316
+ while (at !== -1) {
37317
+ if (isBoundary(at === 0 ? "" : needle.charAt(at - 1)) && isBoundary(needle.charAt(at + base.length))) {
37318
+ return true;
37319
+ }
37320
+ at = needle.indexOf(base, at + 1);
37321
+ }
37322
+ return false;
37323
+ });
37324
+ }
36760
37325
  var TAIL_BYTES = 256 * 1024;
37326
+ function prohibitedModelMessage(model, action) {
37327
+ const subject = action === "switch" ? `Cannot switch to ${model}` : action === "spawn" ? `Cannot start a subagent on ${model}` : `This session is running on ${model}, which cannot be used`;
37328
+ const remedy = action === "spawn" ? "Name an approved model on the subagent" : "Switch to an approved model with /model";
37329
+ return `${subject} \u2014 your organization has prohibited this model. ${remedy}, or ask an administrator to change its status in AKA under Govern \u2192 LLM Providers.`;
37330
+ }
37331
+ function buildModelRefusalEvent(input2) {
37332
+ return {
37333
+ id: input2.id,
37334
+ eventType: "model_refusal",
37335
+ startedAt: input2.occurredAt,
37336
+ // Omitted rather than nulled when unknown: `root_session_id` is a self-FK,
37337
+ // and a session id naming no row would fail the insert outright.
37338
+ ...input2.sessionId === void 0 || input2.sessionId === "" ? {} : { rootSessionId: input2.sessionId },
37339
+ attributes: {
37340
+ // `model` is a generated column on audit_events, so the refused model is
37341
+ // queryable without unpacking the bag — which is what lets the control
37342
+ // plane group refusals by the same id the prohibition was keyed on.
37343
+ model: input2.model,
37344
+ refusal_seam: input2.seam,
37345
+ source_tool: input2.sourceTool,
37346
+ // Omitted rather than duplicated when the caller named the id itself.
37347
+ ...input2.requestedModel === void 0 || input2.requestedModel === input2.model ? {} : { requested_model: input2.requestedModel }
37348
+ }
37349
+ };
37350
+ }
36761
37351
 
36762
37352
  // ../../packages/plugin-sdk/src/nudge.ts
36763
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
36764
- import { join as join17 } from "path";
37353
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
37354
+ import { join as join18 } from "path";
36765
37355
 
36766
37356
  // ../../packages/plugin-sdk/src/paths.ts
36767
37357
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
36768
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
37358
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
37359
+
37360
+ // ../../packages/plugin-sdk/src/policy-resolver.ts
37361
+ function createPolicyResolver(bundle) {
37362
+ const byRule = /* @__PURE__ */ new Map();
37363
+ const byCategory = /* @__PURE__ */ new Map();
37364
+ let reversible = /* @__PURE__ */ new Set();
37365
+ try {
37366
+ for (const policy of bundle.policies) {
37367
+ if (!policy.enabled) continue;
37368
+ if ("ruleId" in policy.target) {
37369
+ if (!byRule.has(policy.target.ruleId)) byRule.set(policy.target.ruleId, policy.action);
37370
+ } else if (!byCategory.has(policy.target.category)) {
37371
+ byCategory.set(policy.target.category, policy.action);
37372
+ }
37373
+ }
37374
+ reversible = new Set(bundle.reversibleRuleIds ?? []);
37375
+ } catch {
37376
+ byRule.clear();
37377
+ byCategory.clear();
37378
+ reversible = /* @__PURE__ */ new Set();
37379
+ }
37380
+ return {
37381
+ actionFor(ruleId, category) {
37382
+ const byRuleAction = byRule.get(ruleId);
37383
+ if (byRuleAction !== void 0) return byRuleAction;
37384
+ const byCategoryAction = byCategory.get(category);
37385
+ if (byCategoryAction !== void 0) return byCategoryAction;
37386
+ const fallback = DEFAULT_ACTIONS[category];
37387
+ return fallback ?? "log";
37388
+ },
37389
+ isReversible(ruleId) {
37390
+ return reversible.has(ruleId);
37391
+ }
37392
+ };
37393
+ }
36769
37394
 
36770
37395
  // ../../packages/plugin-sdk/src/project-files.ts
36771
37396
  import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
36772
- import { basename as basename5, join as join18 } from "path";
37397
+ import { basename as basename5, join as join19 } from "path";
36773
37398
 
36774
37399
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
36775
37400
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -36800,7 +37425,6 @@ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
36800
37425
  // ../../packages/plugin-sdk/src/runtime.ts
36801
37426
  import { randomUUID as randomUUID14 } from "crypto";
36802
37427
  var ENFORCEMENT_CEILING_ENABLED = false;
36803
- var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
36804
37428
  function startTiming() {
36805
37429
  try {
36806
37430
  return performance.now();
@@ -36837,28 +37461,22 @@ function createPluginRuntime(gateway, settings, opts) {
36837
37461
  bundlesPacked = true;
36838
37462
  }
36839
37463
  const policyMode = settings.policy;
37464
+ const redactFallback = settings.redactFallback;
36840
37465
  const dataDir2 = opts?.dataDir;
36841
- let policies = [];
36842
37466
  let rules = [];
36843
37467
  let scanner;
36844
37468
  let bundleExceptions = [];
36845
37469
  let initialized = false;
36846
- const ruleActionIndex = /* @__PURE__ */ new Map();
36847
- const categoryActionIndex = /* @__PURE__ */ new Map();
36848
- let reversibleRuleIndex = /* @__PURE__ */ new Set();
37470
+ let resolver = createPolicyResolver({
37471
+ version: "",
37472
+ policies: [],
37473
+ customKeywords: [],
37474
+ fetchedAt: ""
37475
+ });
36849
37476
  async function ensureInitialized() {
36850
37477
  if (initialized) return;
36851
37478
  const bundle = await gateway.getPolicyBundle();
36852
- policies = bundle.policies;
36853
- for (const p of policies) {
36854
- if (!p.enabled) continue;
36855
- if ("ruleId" in p.target) {
36856
- if (!ruleActionIndex.has(p.target.ruleId)) ruleActionIndex.set(p.target.ruleId, p.action);
36857
- } else if (!categoryActionIndex.has(p.target.category)) {
36858
- categoryActionIndex.set(p.target.category, p.action);
36859
- }
36860
- }
36861
- reversibleRuleIndex = new Set(bundle.reversibleRuleIds ?? []);
37479
+ resolver = createPolicyResolver(bundle);
36862
37480
  const bundledProbeKeys = new Set(
36863
37481
  getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
36864
37482
  );
@@ -36911,33 +37529,28 @@ function createPluginRuntime(gateway, settings, opts) {
36911
37529
  return cachedKey;
36912
37530
  }
36913
37531
  function resolveAction(ruleId, category) {
36914
- const byRule = ruleActionIndex.get(ruleId);
36915
- if (byRule !== void 0) return byRule;
36916
- const byCategory = categoryActionIndex.get(category);
36917
- if (byCategory !== void 0) return byCategory;
36918
- const fallback = DEFAULT_ACTIONS[category];
36919
- return fallback ?? "log";
36920
- }
36921
- function actionForFinding(finding, excepted) {
37532
+ return resolver.actionFor(ruleId, category);
37533
+ }
37534
+ function actionForFinding(finding, excepted, rewritable = true) {
36922
37535
  if (excepted?.has(finding)) return "allow";
36923
37536
  const action = resolveAction(finding.ruleId, finding.category);
37537
+ if (!rewritable && action === "redact") return builtinPolicyToAction(redactFallback);
36924
37538
  if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
36925
37539
  return "warn";
36926
37540
  }
36927
37541
  return action;
36928
37542
  }
36929
- function decide(findings, text, excepted) {
37543
+ function decide(findings, text, excepted, rewritable = true) {
36930
37544
  if (findings.length === 0) return { action: "log", text, findings: [] };
36931
- const actionFor = (finding) => actionForFinding(finding, excepted);
37545
+ const actionFor = (finding) => actionForFinding(finding, excepted, rewritable);
36932
37546
  let worst = "log";
36933
37547
  for (const finding of findings) {
36934
- const action = actionFor(finding);
36935
- if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
37548
+ worst = strongerAction(worst, actionFor(finding));
36936
37549
  }
36937
37550
  if (worst === "block") return { action: "block", text: null, findings };
36938
37551
  if (worst === "redact") {
36939
37552
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
36940
- const reversibleFindings = redactFindings.filter((f) => reversibleRuleIndex.has(f.ruleId));
37553
+ const reversibleFindings = redactFindings.filter((f) => resolver.isReversible(f.ruleId));
36941
37554
  return {
36942
37555
  action: "redact",
36943
37556
  text: redact(text, redactFindings),
@@ -37013,7 +37626,7 @@ function createPluginRuntime(gateway, settings, opts) {
37013
37626
  return { excepted: /* @__PURE__ */ new Set(), exceptionIds: [] };
37014
37627
  }
37015
37628
  }
37016
- async function recordBlockedDetections(decision, excepted, ctx, fpCache) {
37629
+ async function recordBlockedDetections(decision, excepted, ctx, fpCache, rewritable = true) {
37017
37630
  const references = [];
37018
37631
  try {
37019
37632
  if (decision.action !== "block" && decision.action !== "redact") return references;
@@ -37021,7 +37634,7 @@ function createPluginRuntime(gateway, settings, opts) {
37021
37634
  if (!key) return references;
37022
37635
  const seen = /* @__PURE__ */ new Set();
37023
37636
  for (const finding of decision.findings) {
37024
- const action = actionForFinding(finding, excepted);
37637
+ const action = actionForFinding(finding, excepted, rewritable);
37025
37638
  if (action !== "block" && action !== "redact") continue;
37026
37639
  const fp = fingerprintOf(key, finding, fpCache);
37027
37640
  const pair = `${finding.ruleId}:${fp}`;
@@ -37048,7 +37661,7 @@ function createPluginRuntime(gateway, settings, opts) {
37048
37661
  }
37049
37662
  return references;
37050
37663
  }
37051
- async function evaluate(text, context, ctx) {
37664
+ async function evaluate(text, context, ctx, rewritable = true) {
37052
37665
  try {
37053
37666
  await ensureInitialized();
37054
37667
  if (!scanner) throw new Error("the runtime initialized without a scanner");
@@ -37057,8 +37670,14 @@ function createPluginRuntime(gateway, settings, opts) {
37057
37670
  const findings = dropShieldedFindings(matched, shielded.spans);
37058
37671
  const fpCache = /* @__PURE__ */ new Map();
37059
37672
  const { excepted, exceptionIds } = await applyExceptions(findings, ctx, fpCache);
37060
- const decision = decide(findings, text, excepted);
37061
- const blockedReferences = await recordBlockedDetections(decision, excepted, ctx, fpCache);
37673
+ const decision = decide(findings, text, excepted, rewritable);
37674
+ const blockedReferences = await recordBlockedDetections(
37675
+ decision,
37676
+ excepted,
37677
+ ctx,
37678
+ fpCache,
37679
+ rewritable
37680
+ );
37062
37681
  if (blockedReferences.length > 0) decision.blockedReferences = blockedReferences;
37063
37682
  return { decision, excepted, exceptionIds };
37064
37683
  } catch {
@@ -37082,12 +37701,16 @@ function createPluginRuntime(gateway, settings, opts) {
37082
37701
  sourceTool: input2.sourceTool,
37083
37702
  metadata: input2.metadata,
37084
37703
  preAuthorizedGrantIds: opts2.preAuthorizedGrantIds
37085
- }
37704
+ },
37705
+ opts2.rewritable
37086
37706
  );
37087
37707
  if (opts2.persist === "with-findings" && decision.findings.length === 0) return decision;
37088
37708
  try {
37089
37709
  const contentHash = contentHashOf(input2.text);
37090
- const storedContent = decision.findings.length > 0 ? redact(input2.text, decision.findings) : input2.text;
37710
+ const maskedFindings = decision.findings.filter(
37711
+ (match) => isActionAtLeast(actionForFinding(match, excepted, opts2.rewritable), "redact")
37712
+ );
37713
+ const storedContent = maskedFindings.length > 0 ? redact(input2.text, maskedFindings) : input2.text;
37091
37714
  const inspectionMs = elapsedMs(timingStartedAt);
37092
37715
  const metadata = exceptionIds.length > 0 || inspectionMs !== void 0 ? {
37093
37716
  ...input2.metadata,
@@ -37124,7 +37747,7 @@ function createPluginRuntime(gateway, settings, opts) {
37124
37747
  severity: match.severity,
37125
37748
  span: match.span,
37126
37749
  maskedMatch,
37127
- actionTaken: actionForFinding(match, excepted),
37750
+ actionTaken: actionForFinding(match, excepted, opts2.rewritable),
37128
37751
  confidence: match.confidence,
37129
37752
  ...findingKey ? { findingKey } : {}
37130
37753
  };
@@ -37164,7 +37787,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
37164
37787
 
37165
37788
  // ../../packages/plugin-sdk/src/throttle.ts
37166
37789
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
37167
- import { join as join19 } from "path";
37790
+ import { join as join20 } from "path";
37168
37791
 
37169
37792
  // ../../packages/plugin-sdk/src/tokenize.ts
37170
37793
  function redactedPlaceholder(category) {
@@ -37226,14 +37849,26 @@ var SecretVaultGlue = class {
37226
37849
  }
37227
37850
  async tokenizeText(text, opts) {
37228
37851
  try {
37229
- const findings = opts?.findings ?? this.#selfScan(text);
37230
- const reversible = opts?.reversible;
37231
- const keeps = (finding) => reversible === void 0 || reversible.has(finding);
37232
- if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
37233
- if (findings.length === 0) return { text, pointers: [], degraded: [] };
37852
+ const supplied = opts?.findings;
37853
+ const resolver = opts?.resolver;
37854
+ const scanned = supplied ?? this.#selfScan(text);
37855
+ if (scanned === null) {
37856
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
37857
+ }
37858
+ const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
37859
+ (f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
37860
+ ) : scanned;
37861
+ let reversible = opts?.reversible;
37862
+ if (resolver !== void 0 && reversible === void 0) {
37863
+ reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
37864
+ }
37865
+ const reversibleSet = reversible;
37866
+ const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
37867
+ if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
37234
37868
  const groups = groupSpans(text, findings);
37235
37869
  const pointers = [];
37236
37870
  const degraded = [];
37871
+ const redacted = [];
37237
37872
  let out = text;
37238
37873
  for (const group of [...groups].reverse()) {
37239
37874
  const original = text.slice(group.start, group.end);
@@ -37247,6 +37882,7 @@ var SecretVaultGlue = class {
37247
37882
  degraded.unshift({ category: group.category });
37248
37883
  } else if (!keeps(finding)) {
37249
37884
  replacement = redactedPlaceholder(finding.category);
37885
+ redacted.unshift({ category: finding.category });
37250
37886
  } else {
37251
37887
  replacement = await this.tokenizeValue(finding.rawMatch, {
37252
37888
  ruleId: finding.ruleId,
@@ -37267,9 +37903,9 @@ var SecretVaultGlue = class {
37267
37903
  }
37268
37904
  }
37269
37905
  }
37270
- return { text: out, pointers, degraded };
37906
+ return { text: out, pointers, degraded, redacted };
37271
37907
  } catch {
37272
- return { text: "[REDACTED]", pointers: [], degraded: [] };
37908
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
37273
37909
  }
37274
37910
  }
37275
37911
  async detokenizeText(text, opts) {
@@ -37471,17 +38107,17 @@ var UNOPENABLE_VAULT = {
37471
38107
 
37472
38108
  // src/protocol/marker.ts
37473
38109
  import { randomBytes as randomBytes4 } from "crypto";
37474
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync12, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
37475
- import { join as join20 } from "path";
38110
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync13, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
38111
+ import { join as join21 } from "path";
37476
38112
  var MARKER_FILE = "protocol-marker";
37477
38113
  function mintMarker() {
37478
38114
  return randomBytes4(8).toString("hex");
37479
38115
  }
37480
38116
  function sessionProtocolMarker(dataDir2, sessionId) {
37481
38117
  if (!sessionId) return mintMarker();
37482
- const path = join20(dataDir2, MARKER_FILE);
38118
+ const path = join21(dataDir2, MARKER_FILE);
37483
38119
  try {
37484
- const stored = JSON.parse(readFileSync12(path, "utf8"));
38120
+ const stored = JSON.parse(readFileSync13(path, "utf8"));
37485
38121
  if (stored.sessionId === sessionId && typeof stored.marker === "string" && /^[0-9a-f]{16}$/.test(stored.marker)) {
37486
38122
  return stored.marker;
37487
38123
  }
@@ -37490,7 +38126,7 @@ function sessionProtocolMarker(dataDir2, sessionId) {
37490
38126
  const marker = mintMarker();
37491
38127
  try {
37492
38128
  mkdirSync5(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
37493
- const tmp = join20(dataDir2, `${MARKER_FILE}.tmp`);
38129
+ const tmp = join21(dataDir2, `${MARKER_FILE}.tmp`);
37494
38130
  writeFileSync8(tmp, JSON.stringify({ sessionId, marker }), { mode: DATA_FILE_MODE });
37495
38131
  renameSync5(tmp, path);
37496
38132
  } catch {
@@ -37543,6 +38179,117 @@ function userDisclosure(opts) {
37543
38179
  return sentences.join(" ");
37544
38180
  }
37545
38181
 
38182
+ // src/hooks/model-guard.ts
38183
+ import { randomUUID as randomUUID15 } from "crypto";
38184
+ import { readFileSync as readFileSync14, statSync as statSync9 } from "fs";
38185
+ import { homedir as homedir3 } from "os";
38186
+ import { dirname as dirname6, join as join22 } from "path";
38187
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Task", "Agent"]);
38188
+ var SAFE_SUBAGENT_TYPE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
38189
+ function projectAgentsRoot(from) {
38190
+ if (from === void 0 || from === "") return void 0;
38191
+ const home = homedir3();
38192
+ let dir = from;
38193
+ for (let depth = 0; depth < 24; depth += 1) {
38194
+ if (dir === home) return void 0;
38195
+ try {
38196
+ if (statSync9(join22(dir, ".claude", "agents")).isDirectory()) return dir;
38197
+ } catch {
38198
+ }
38199
+ const parent = dirname6(dir);
38200
+ if (parent === dir) return void 0;
38201
+ dir = parent;
38202
+ }
38203
+ return void 0;
38204
+ }
38205
+ function modelFromAgentDefinition(subagentType, cwd) {
38206
+ if (subagentType === void 0 || !SAFE_SUBAGENT_TYPE.test(subagentType)) return void 0;
38207
+ const roots = [projectAgentsRoot(cwd), homedir3()].filter(
38208
+ (r) => r !== void 0 && r !== ""
38209
+ );
38210
+ for (const root of roots) {
38211
+ try {
38212
+ const raw = readFileSync14(join22(root, ".claude", "agents", `${subagentType}.md`), "utf8");
38213
+ const lines = raw.split("\n");
38214
+ if (lines[0]?.trim() !== "---") continue;
38215
+ for (const line of lines.slice(1)) {
38216
+ if (line.trim() === "---") break;
38217
+ const sep4 = line.indexOf(":");
38218
+ if (sep4 === -1) continue;
38219
+ if (line.slice(0, sep4).trim() !== "model") continue;
38220
+ const value = line.slice(sep4 + 1).trim().replace(/^['"]|['"]$/g, "");
38221
+ if (value !== "") return value;
38222
+ }
38223
+ } catch {
38224
+ }
38225
+ }
38226
+ return void 0;
38227
+ }
38228
+ function resolveSpawnModel(toolInput, cwd) {
38229
+ const explicit = toolInput.model;
38230
+ if (typeof explicit === "string" && explicit !== "") return explicit;
38231
+ const subagentType = toolInput.subagent_type;
38232
+ return modelFromAgentDefinition(typeof subagentType === "string" ? subagentType : void 0, cwd);
38233
+ }
38234
+ function decideSubagentSpawn(requested, prohibitedModels) {
38235
+ if (requested === void 0 || requested === "") return null;
38236
+ const matched = matchProhibitedSpawnModel(requested, prohibitedModels);
38237
+ if (matched === void 0) return null;
38238
+ return {
38239
+ matched,
38240
+ output: {
38241
+ hookSpecificOutput: {
38242
+ hookEventName: "PreToolUse",
38243
+ permissionDecision: "deny",
38244
+ permissionDecisionReason: prohibitedModelMessage(requested, "spawn")
38245
+ }
38246
+ }
38247
+ };
38248
+ }
38249
+ async function handleSubagentSpawn(openGateway, toolName, toolInput, sessionId, cwd, emit2) {
38250
+ if (!SUBAGENT_TOOLS.has(toolName)) return false;
38251
+ const gateway = openGateway();
38252
+ if (gateway === null) return false;
38253
+ let decision;
38254
+ let requested;
38255
+ try {
38256
+ const { prohibitedModels } = await gateway.getPolicyBundle();
38257
+ if (prohibitedModels === void 0 || prohibitedModels.length === 0) {
38258
+ await gateway.close();
38259
+ return false;
38260
+ }
38261
+ requested = resolveSpawnModel(toolInput, cwd);
38262
+ decision = decideSubagentSpawn(requested, prohibitedModels);
38263
+ } catch {
38264
+ await gateway.close();
38265
+ return false;
38266
+ }
38267
+ if (decision === null || requested === void 0) {
38268
+ await gateway.close();
38269
+ return false;
38270
+ }
38271
+ try {
38272
+ await gateway.recordAuditEvent(
38273
+ buildModelRefusalEvent({
38274
+ id: randomUUID15(),
38275
+ sessionId,
38276
+ model: decision.matched,
38277
+ requestedModel: requested,
38278
+ seam: "spawn",
38279
+ sourceTool: SOURCE_TOOL.ClaudeCode,
38280
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
38281
+ })
38282
+ );
38283
+ } catch {
38284
+ }
38285
+ await emit2(decision.output);
38286
+ try {
38287
+ await gateway.close();
38288
+ } catch {
38289
+ }
38290
+ return true;
38291
+ }
38292
+
37546
38293
  // src/hooks/paths.ts
37547
38294
  function stringAtPath(root, path) {
37548
38295
  let current = root;
@@ -37774,7 +38521,13 @@ var STATIC_FIELDS = {
37774
38521
  // machine, but it is still a channel a secret can ride into a context the
37775
38522
  // user never sees. Scanned as data: the masked prompt is a coherent
37776
38523
  // instruction, so redaction is the intended end state and block still blocks.
37777
- Task: [{ path: ["prompt"], executable: false }]
38524
+ //
38525
+ // BOTH spellings. The harness renamed this tool — older builds send `Task`,
38526
+ // current ones send `Agent` — and a table naming only the old one scans
38527
+ // nothing at all on a current build, silently, because an unknown tool yields
38528
+ // no fields and the hook returns before any decision.
38529
+ Task: [{ path: ["prompt"], executable: false }],
38530
+ Agent: [{ path: ["prompt"], executable: false }]
37778
38531
  };
37779
38532
  var MCP_MAX_DEPTH = 6;
37780
38533
  var MCP_MAX_LEAF_CHARS = 1e6;
@@ -37811,6 +38564,7 @@ function multiEditFields(toolInput) {
37811
38564
  executable: false
37812
38565
  }));
37813
38566
  }
38567
+ var SCANNED_TOOL_NAMES = Object.keys(STATIC_FIELDS);
37814
38568
  function scannableInputFields(toolName, toolInput) {
37815
38569
  const candidates = toolName.startsWith("mcp__") ? mcpFields(toolInput) : toolName === "MultiEdit" ? multiEditFields(toolInput) : (
37816
38570
  // hasOwn guard: a bare index would resolve Object.prototype members for
@@ -37886,8 +38640,8 @@ function baseMetadata(input2) {
37886
38640
  }
37887
38641
 
37888
38642
  // src/hooks/store-health.ts
37889
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
37890
- import { dirname as dirname5, join as join27 } from "path";
38643
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync20, writeFileSync as writeFileSync9 } from "fs";
38644
+ import { dirname as dirname7, join as join29 } from "path";
37891
38645
 
37892
38646
  // ../../packages/plugin-runtime/src/attached/egress-wire.ts
37893
38647
  import { createHash as createHash5 } from "crypto";
@@ -37926,6 +38680,307 @@ function toEgressIngestRequest(input2) {
37926
38680
  };
37927
38681
  }
37928
38682
 
38683
+ // ../../packages/remote/src/http.ts
38684
+ import { request as httpRequest } from "http";
38685
+ import { request as httpsRequest } from "https";
38686
+ var DEFAULT_TIMEOUT_MS = 1e4;
38687
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
38688
+ var RemoteRequestError = class extends Error {
38689
+ constructor(status) {
38690
+ super(`control-plane request failed with status ${String(status)}`);
38691
+ this.status = status;
38692
+ this.name = "RemoteRequestError";
38693
+ }
38694
+ status;
38695
+ };
38696
+ var RemoteRouteAbsent = class extends Error {
38697
+ constructor(route) {
38698
+ super(`control plane does not serve ${route}`);
38699
+ this.route = route;
38700
+ this.name = "RemoteRouteAbsent";
38701
+ }
38702
+ route;
38703
+ };
38704
+ var RemoteRequestInvalid = class extends Error {
38705
+ constructor(route, cause) {
38706
+ super(`refusing to send a malformed body to ${route}`);
38707
+ this.cause = cause;
38708
+ this.name = "RemoteRequestInvalid";
38709
+ }
38710
+ cause;
38711
+ };
38712
+ var RemoteResponseInvalid = class extends Error {
38713
+ constructor(route, detail) {
38714
+ super(`control plane answered ${route} with ${detail}`);
38715
+ this.name = "RemoteResponseInvalid";
38716
+ }
38717
+ };
38718
+ var RemoteTransportError = class extends Error {
38719
+ /**
38720
+ * The status the peer sent, when headers arrived and only the BODY was
38721
+ * refused.
38722
+ *
38723
+ * Undefined for the ordinary case this class was written for — no answer at
38724
+ * all. It exists because two paths reject after a status has already been
38725
+ * delivered: an oversized body and an aborted response. Discarding it there
38726
+ * reported a deployment answering 401 with a verbose body as a network
38727
+ * outage, which sends the reader to look at their network instead of their
38728
+ * credential.
38729
+ */
38730
+ constructor(reason, status) {
38731
+ super(`control-plane request did not complete: ${reason}`);
38732
+ this.status = status;
38733
+ this.name = "RemoteTransportError";
38734
+ }
38735
+ status;
38736
+ };
38737
+ async function send(options) {
38738
+ const url2 = new URL(options.url);
38739
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38740
+ const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
38741
+ const requestOptions = {
38742
+ method: options.method,
38743
+ headers: {
38744
+ // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
38745
+ // last they win, and two of the values below are ones no caller may
38746
+ // replace: `x-api-key` is the credential, and `content-length` is the
38747
+ // byte count that stops a multi-byte body being truncated by the
38748
+ // receiver. `SendOptions.headers` is a free-form record on an exported
38749
+ // function, so "no caller does that today" is not the guarantee to rely
38750
+ // on. The one header any caller actually passes — `if-none-match` on the
38751
+ // conditional GET — is untouched by this order.
38752
+ ...options.headers,
38753
+ // The credential. One header, matching what the deployment authenticates
38754
+ // on; a second copy in an `Authorization` header would be one more place
38755
+ // it can be logged by an intermediary for no gain.
38756
+ //
38757
+ // Spread conditionally rather than assigned as `undefined`: Node's header
38758
+ // handling and `content-length` bookkeeping treat a present-but-undefined
38759
+ // key differently from an absent one, and "the header is not there" is
38760
+ // the property the attach flow needs.
38761
+ ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
38762
+ accept: "application/json",
38763
+ ...options.body === void 0 ? {} : {
38764
+ "content-type": "application/json",
38765
+ // Byte length, not string length: a multi-byte body sent with a
38766
+ // character count is truncated by the receiver.
38767
+ "content-length": String(Buffer.byteLength(options.body))
38768
+ }
38769
+ }
38770
+ };
38771
+ return new Promise((resolve2, reject) => {
38772
+ let settled = false;
38773
+ const fail = (reason, status) => {
38774
+ if (settled) return;
38775
+ settled = true;
38776
+ reject(new RemoteTransportError(reason, status));
38777
+ };
38778
+ const req = send_(url2, requestOptions, (res) => {
38779
+ const chunks = [];
38780
+ let size = 0;
38781
+ res.on("data", (chunk) => {
38782
+ size += chunk.length;
38783
+ if (size > MAX_RESPONSE_BYTES) {
38784
+ fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
38785
+ res.destroy();
38786
+ req.destroy();
38787
+ return;
38788
+ }
38789
+ chunks.push(chunk);
38790
+ });
38791
+ res.on("aborted", () => {
38792
+ fail("the response was aborted", res.statusCode);
38793
+ });
38794
+ res.on("end", () => {
38795
+ if (settled) return;
38796
+ settled = true;
38797
+ resolve2({
38798
+ status: res.statusCode ?? 0,
38799
+ headers: res.headers,
38800
+ body: Buffer.concat(chunks).toString("utf8")
38801
+ });
38802
+ });
38803
+ });
38804
+ const deadline = setTimeout(() => {
38805
+ fail(`no response within ${String(timeoutMs)}ms`);
38806
+ req.destroy();
38807
+ }, timeoutMs);
38808
+ deadline.unref();
38809
+ req.on("upgrade", (_res, socket) => {
38810
+ fail("the deployment answered with a protocol upgrade");
38811
+ socket.destroy();
38812
+ });
38813
+ req.on("close", () => {
38814
+ fail("the connection closed before a response was read");
38815
+ clearTimeout(deadline);
38816
+ });
38817
+ req.on("error", (err) => {
38818
+ fail(err.message);
38819
+ });
38820
+ if (options.body !== void 0) req.write(options.body);
38821
+ req.end();
38822
+ });
38823
+ }
38824
+
38825
+ // ../../packages/remote/src/client.ts
38826
+ var ROUTES = {
38827
+ events: "/v1/events",
38828
+ auditEvents: "/v1/audit-events",
38829
+ auditEventsBatch: "/v1/audit-events/batch",
38830
+ inventory: "/v1/inventory",
38831
+ storePosture: "/v1/store-posture",
38832
+ policyBundle: "/v1/policy-bundle",
38833
+ whoami: "/v1/plugin/whoami",
38834
+ shares: "/v1/shares",
38835
+ commands: "/v1/plugin/commands"
38836
+ };
38837
+ function ackRoute(id) {
38838
+ return `${ROUTES.commands}/${encodeURIComponent(id)}/ack`;
38839
+ }
38840
+ function headerValue(response, name) {
38841
+ const raw = response.headers[name];
38842
+ if (raw === void 0) return void 0;
38843
+ return Array.isArray(raw) ? raw[0] : raw;
38844
+ }
38845
+ function okBody(response) {
38846
+ if (response.status < 200 || response.status >= 300) {
38847
+ throw new RemoteRequestError(response.status);
38848
+ }
38849
+ return response.body;
38850
+ }
38851
+ function parsed(schema, body, route) {
38852
+ let json2;
38853
+ try {
38854
+ json2 = JSON.parse(body);
38855
+ } catch {
38856
+ throw new RemoteResponseInvalid(route, "a body that is not JSON");
38857
+ }
38858
+ const result = schema.safeParse(json2);
38859
+ if (!result.success) {
38860
+ throw new RemoteResponseInvalid(route, "a body this client cannot read");
38861
+ }
38862
+ return result.data;
38863
+ }
38864
+ function withoutTrailingSlashes(endpoint) {
38865
+ let end = endpoint.length;
38866
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
38867
+ return endpoint.slice(0, end);
38868
+ }
38869
+ var SLASH = "/".charCodeAt(0);
38870
+ function createRemoteClient(options) {
38871
+ const base = withoutTrailingSlashes(options.endpoint);
38872
+ const url2 = (route) => `${base}${route}`;
38873
+ const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
38874
+ const sendOne = async (event) => {
38875
+ const validated = RecordAuditEventRequest.safeParse(event);
38876
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
38877
+ const response = await send({
38878
+ ...common,
38879
+ method: "POST",
38880
+ url: url2(ROUTES.auditEvents),
38881
+ body: JSON.stringify(validated.data)
38882
+ });
38883
+ okBody(response);
38884
+ };
38885
+ return {
38886
+ async ingestEvents(batch) {
38887
+ const response = await send({
38888
+ ...common,
38889
+ method: "POST",
38890
+ url: url2(ROUTES.events),
38891
+ body: JSON.stringify(batch)
38892
+ });
38893
+ return parsed(IngestAck, okBody(response), ROUTES.events);
38894
+ },
38895
+ async ingestInventory(context) {
38896
+ const response = await send({
38897
+ ...common,
38898
+ method: "POST",
38899
+ url: url2(ROUTES.inventory),
38900
+ body: JSON.stringify(context)
38901
+ });
38902
+ return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
38903
+ },
38904
+ async recordAuditEvent(event) {
38905
+ await sendOne(event);
38906
+ },
38907
+ async recordAuditEvents(events, opts) {
38908
+ const validated = RecordAuditEventBatch.safeParse({ events });
38909
+ if (!validated.success) {
38910
+ throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
38911
+ }
38912
+ const response = await send({
38913
+ ...common,
38914
+ method: "POST",
38915
+ url: url2(ROUTES.auditEventsBatch),
38916
+ body: JSON.stringify(validated.data)
38917
+ });
38918
+ if (response.status === 404) {
38919
+ if (opts?.fallbackToSingleEvents !== true) {
38920
+ throw new RemoteRouteAbsent(ROUTES.auditEventsBatch);
38921
+ }
38922
+ for (const event of validated.data.events) await sendOne(event);
38923
+ return { accepted: validated.data.events.length };
38924
+ }
38925
+ return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
38926
+ },
38927
+ async reportStorePosture(snapshot) {
38928
+ const response = await send({
38929
+ ...common,
38930
+ method: "POST",
38931
+ url: url2(ROUTES.storePosture),
38932
+ body: JSON.stringify(snapshot)
38933
+ });
38934
+ okBody(response);
38935
+ },
38936
+ async getPolicyBundle(etag) {
38937
+ const response = await send({
38938
+ ...common,
38939
+ method: "GET",
38940
+ url: url2(ROUTES.policyBundle),
38941
+ ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
38942
+ });
38943
+ if (response.status === 304) {
38944
+ return { changed: false, etag: headerValue(response, "etag") ?? etag };
38945
+ }
38946
+ const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
38947
+ return { changed: true, bundle, etag: headerValue(response, "etag") };
38948
+ },
38949
+ async whoami() {
38950
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
38951
+ return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
38952
+ },
38953
+ async recordProjectEgress(request) {
38954
+ const validated = EgressIngestRequest.safeParse(request);
38955
+ if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
38956
+ const response = await send({
38957
+ ...common,
38958
+ method: "POST",
38959
+ url: url2(ROUTES.shares),
38960
+ body: JSON.stringify(validated.data)
38961
+ });
38962
+ okBody(response);
38963
+ },
38964
+ async pollCommand() {
38965
+ const response = await send({ ...common, method: "GET", url: url2(ROUTES.commands) });
38966
+ if (response.status === 404) return null;
38967
+ return parsed(DeviceCommandPollResponse, okBody(response), ROUTES.commands).command;
38968
+ },
38969
+ async ackCommand(id, body) {
38970
+ const validated = DeviceCommandAckBody.safeParse(body);
38971
+ const route = ackRoute(id);
38972
+ if (!validated.success) throw new RemoteRequestInvalid(route, validated.error);
38973
+ const response = await send({
38974
+ ...common,
38975
+ method: "POST",
38976
+ url: url2(route),
38977
+ body: JSON.stringify(validated.data)
38978
+ });
38979
+ okBody(response);
38980
+ }
38981
+ };
38982
+ }
38983
+
37929
38984
  // ../../packages/plugin-runtime/src/attached/failure.ts
37930
38985
  function statusOf(err) {
37931
38986
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
@@ -37944,12 +38999,27 @@ function classifyFailure(err) {
37944
38999
  }
37945
39000
  }
37946
39001
 
39002
+ // ../../packages/plugin-runtime/src/attached/with-timeout.ts
39003
+ var REQUEST_TIMEOUT_MS = 2e3;
39004
+ function withTimeout(promise2, ms) {
39005
+ let timer;
39006
+ const timeout = new Promise((_, reject) => {
39007
+ timer = setTimeout(() => {
39008
+ reject(new Error("attached gateway request timed out"));
39009
+ }, ms);
39010
+ });
39011
+ promise2.catch(() => void 0);
39012
+ return Promise.race([promise2, timeout]).finally(() => {
39013
+ clearTimeout(timer);
39014
+ });
39015
+ }
39016
+
37947
39017
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
37948
- import { readFileSync as readFileSync13 } from "fs";
37949
- import { join as join21 } from "path";
39018
+ import { readFileSync as readFileSync15 } from "fs";
39019
+ import { join as join23 } from "path";
37950
39020
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
37951
39021
  function forwardDropsPath(dataDir2) {
37952
- return join21(dataDir2, FORWARD_DROPS_FILENAME);
39022
+ return join23(dataDir2, FORWARD_DROPS_FILENAME);
37953
39023
  }
37954
39024
  function recordForwardDrops(dataDir2, count, nowMs) {
37955
39025
  if (count <= 0) return;
@@ -37967,7 +39037,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
37967
39037
  }
37968
39038
  function readForwardDrops(dataDir2) {
37969
39039
  try {
37970
- const parsed2 = JSON.parse(readFileSync13(forwardDropsPath(dataDir2), "utf8"));
39040
+ const parsed2 = JSON.parse(readFileSync15(forwardDropsPath(dataDir2), "utf8"));
37971
39041
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
37972
39042
  const record2 = parsed2;
37973
39043
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -37984,30 +39054,20 @@ function readForwardDrops(dataDir2) {
37984
39054
  }
37985
39055
 
37986
39056
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
37987
- import { randomUUID as randomUUID15 } from "crypto";
37988
- import { readFileSync as readFileSync14 } from "fs";
39057
+ import { randomUUID as randomUUID16 } from "crypto";
39058
+ import { readFileSync as readFileSync16 } from "fs";
37989
39059
  import { readFile, rename, writeFile } from "fs/promises";
37990
- import { join as join22 } from "path";
37991
-
37992
- // ../../packages/plugin-runtime/src/attached/with-timeout.ts
37993
- var REQUEST_TIMEOUT_MS = 2e3;
37994
- function withTimeout(promise2, ms) {
37995
- let timer;
37996
- const timeout = new Promise((_, reject) => {
37997
- timer = setTimeout(() => {
37998
- reject(new Error("attached gateway request timed out"));
37999
- }, ms);
38000
- });
38001
- promise2.catch(() => void 0);
38002
- return Promise.race([promise2, timeout]).finally(() => {
38003
- clearTimeout(timer);
38004
- });
38005
- }
38006
-
38007
- // ../../packages/plugin-runtime/src/attached/forward-policy.ts
39060
+ import { join as join24 } from "path";
38008
39061
  function isInvalidRequest(err) {
38009
39062
  return typeof err === "object" && err !== null && err.name === "RemoteRequestInvalid";
38010
39063
  }
39064
+ function isRouteAbsent(err) {
39065
+ return typeof err === "object" && err !== null && err.name === "RemoteRouteAbsent";
39066
+ }
39067
+ function isServerRejection(err) {
39068
+ const status = statusOf(err);
39069
+ return status !== null && status >= 400 && status <= 499 && status !== 401 && status !== 403 && status !== 404 && status !== 429;
39070
+ }
38011
39071
  var FORWARD_BUDGET_MS = 1500;
38012
39072
  var DECISION_PATH_BUDGET_MS = 800;
38013
39073
  var BREAKER_FAILURE_THRESHOLD = 3;
@@ -38035,7 +39095,7 @@ function parseBreakerState(raw, nowMs) {
38035
39095
  }
38036
39096
  function createForwardPolicy(deps) {
38037
39097
  const now = deps.now ?? (() => Date.now());
38038
- const file2 = join22(deps.dir, STATE_FILENAME);
39098
+ const file2 = join24(deps.dir, STATE_FILENAME);
38039
39099
  let state = null;
38040
39100
  let loading = null;
38041
39101
  async function readState() {
@@ -38060,7 +39120,7 @@ function createForwardPolicy(deps) {
38060
39120
  state = next;
38061
39121
  try {
38062
39122
  await ensureDataDir(deps.dir);
38063
- const tmp = `${file2}.${randomUUID15()}.tmp`;
39123
+ const tmp = `${file2}.${randomUUID16()}.tmp`;
38064
39124
  await writeFile(tmp, JSON.stringify(next), { encoding: "utf8", mode: DATA_FILE_MODE });
38065
39125
  await rename(tmp, file2);
38066
39126
  } catch {
@@ -38075,6 +39135,20 @@ function createForwardPolicy(deps) {
38075
39135
  } catch {
38076
39136
  current = { ...CLOSED };
38077
39137
  }
39138
+ const restoreOpenedAtMs = (openedAtMs) => persist({
39139
+ consecutiveFailures: current.consecutiveFailures,
39140
+ openedAtMs,
39141
+ lastFailure: current.lastFailure
39142
+ });
39143
+ const recordFailure = (cause) => {
39144
+ const failures = current.consecutiveFailures + 1;
39145
+ const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
39146
+ return persist({
39147
+ consecutiveFailures: failures,
39148
+ openedAtMs: shouldOpen ? now() : null,
39149
+ lastFailure: cause
39150
+ });
39151
+ };
38078
39152
  const at = now();
38079
39153
  if (current.openedAtMs !== null) {
38080
39154
  if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
@@ -38093,15 +39167,20 @@ function createForwardPolicy(deps) {
38093
39167
  }
38094
39168
  return { ok: true, value };
38095
39169
  } catch (err) {
38096
- if (isInvalidRequest(err)) return { ok: false, reason: "invalid-request" };
39170
+ if (isInvalidRequest(err)) {
39171
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(current.openedAtMs);
39172
+ return { ok: false, reason: "invalid-request" };
39173
+ }
39174
+ if (isRouteAbsent(err)) {
39175
+ if (current.openedAtMs !== null) await restoreOpenedAtMs(null);
39176
+ return { ok: false, reason: "route-absent" };
39177
+ }
39178
+ if (isServerRejection(err)) {
39179
+ await recordFailure("unreachable");
39180
+ return { ok: false, reason: "rejected" };
39181
+ }
38097
39182
  const reason = classifyFailure(err);
38098
- const failures = current.consecutiveFailures + 1;
38099
- const shouldOpen = current.openedAtMs !== null || failures >= BREAKER_FAILURE_THRESHOLD;
38100
- await persist({
38101
- consecutiveFailures: failures,
38102
- openedAtMs: shouldOpen ? now() : null,
38103
- lastFailure: reason
38104
- });
39183
+ await recordFailure(reason);
38105
39184
  return { ok: false, reason };
38106
39185
  }
38107
39186
  }
@@ -38109,13 +39188,11 @@ function createForwardPolicy(deps) {
38109
39188
  }
38110
39189
 
38111
39190
  // ../../packages/plugin-runtime/src/attached/gateway.ts
38112
- var ACTION_STRENGTH = {
38113
- allow: 0,
38114
- log: 1,
38115
- warn: 2,
38116
- redact: 3,
38117
- block: 4
38118
- };
39191
+ function strongerOf(a, b) {
39192
+ if (a === null) return b;
39193
+ if (b === null) return a;
39194
+ return strongerAction(a, b);
39195
+ }
38119
39196
  function ruleCategoryMap(wireRules, localRules) {
38120
39197
  const map2 = /* @__PURE__ */ new Map();
38121
39198
  for (const rule of wireRules ?? []) map2.set(rule.id, rule.category);
@@ -38125,11 +39202,6 @@ function ruleCategoryMap(wireRules, localRules) {
38125
39202
  }
38126
39203
  return map2;
38127
39204
  }
38128
- function strongerOf(a, b) {
38129
- if (a === null) return b;
38130
- if (b === null) return a;
38131
- return ACTION_STRENGTH[a] >= ACTION_STRENGTH[b] ? a : b;
38132
- }
38133
39205
  function policyKey(policy) {
38134
39206
  return "ruleId" in policy.target ? `rule:${policy.target.ruleId}` : `category:${policy.target.category}`;
38135
39207
  }
@@ -38148,7 +39220,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38148
39220
  const floor = floorFor(policy, categoryByRuleId);
38149
39221
  remoteCategoryAction.set(
38150
39222
  policy.target.category,
38151
- floor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[floor] ? floor : policy.action
39223
+ floor !== null && !isActionAtLeast(policy.action, floor) ? floor : policy.action
38152
39224
  );
38153
39225
  }
38154
39226
  for (const policy of localPolicies) {
@@ -38165,7 +39237,7 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38165
39237
  }
38166
39238
  merged.set(
38167
39239
  key,
38168
- remoteFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[remoteFloor] ? { ...policy, action: remoteFloor } : policy
39240
+ remoteFloor !== null && !isActionAtLeast(policy.action, remoteFloor) ? { ...policy, action: remoteFloor } : policy
38169
39241
  );
38170
39242
  }
38171
39243
  const localCategoryAction = /* @__PURE__ */ new Map();
@@ -38185,13 +39257,13 @@ function mergeRaiseOnly(localPolicies, remotePolicies, categoryByRuleId) {
38185
39257
  if (category !== void 0) localFloor = localCategoryAction.get(category) ?? null;
38186
39258
  }
38187
39259
  const effectiveFloor = strongerOf(floor, localFloor);
38188
- const clamped = effectiveFloor !== null && ACTION_STRENGTH[policy.action] < ACTION_STRENGTH[effectiveFloor] ? { ...policy, action: effectiveFloor } : policy;
39260
+ const clamped = effectiveFloor !== null && !isActionAtLeast(policy.action, effectiveFloor) ? { ...policy, action: effectiveFloor } : policy;
38189
39261
  const existing = merged.get(key);
38190
39262
  if (existing === void 0) {
38191
39263
  merged.set(key, clamped);
38192
39264
  continue;
38193
39265
  }
38194
- if (ACTION_STRENGTH[clamped.action] > ACTION_STRENGTH[existing.action]) {
39266
+ if (actionRank(clamped.action) > actionRank(existing.action)) {
38195
39267
  merged.set(key, clamped);
38196
39268
  }
38197
39269
  }
@@ -38224,6 +39296,8 @@ var AttachedDataGateway = class {
38224
39296
  );
38225
39297
  if (forwarded.ok && forwarded.value.accepted + forwarded.value.duplicates > 0) {
38226
39298
  this.deps.local.markCaptureDelivered(record2.event, Date.now());
39299
+ } else {
39300
+ this.deps.local.markCaptureOwed(record2.event);
38227
39301
  }
38228
39302
  }
38229
39303
  async ensureInventory(ctx) {
@@ -38260,9 +39334,10 @@ var AttachedDataGateway = class {
38260
39334
  // a retried tool_call, exactly this path — can never stomp a populated row.
38261
39335
  async recordAuditEvent(event) {
38262
39336
  await this.deps.local.recordAuditEvent(event);
38263
- await this.deps.forward.run(
39337
+ const forwarded = await this.deps.forward.run(
38264
39338
  () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
38265
39339
  );
39340
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
38266
39341
  }
38267
39342
  // Attached `llm_call` is written locally by the inner gateway, then routed to
38268
39343
  // the control plane through the existing `recordAuditEvent` ingest (no dedicated
@@ -38271,44 +39346,170 @@ var AttachedDataGateway = class {
38271
39346
  // which would write the event to the local store a second time.
38272
39347
  async recordLlmCall(input2) {
38273
39348
  await this.deps.local.recordLlmCall(input2);
38274
- await this.deps.forward.run(
38275
- () => this.deps.client.recordAuditEvent(
38276
- reKeyForForward(llmAuditEvent(input2), this.remoteInventory)
38277
- )
39349
+ const event = llmAuditEvent(input2);
39350
+ const forwarded = await this.deps.forward.run(
39351
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
38278
39352
  );
39353
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([event], Date.now());
38279
39354
  }
38280
39355
  /**
38281
- * Forward one batch, item by item, under ONE aggregate deadline.
39356
+ * Forward one batch in CHUNKS of AUDIT_EVENT_BATCH_MAX, under ONE aggregate deadline.
39357
+ *
39358
+ * This used to send one HTTP request per event, which is what made the batch
39359
+ * budget bite: at 200ms round-trip a 3s budget admitted ~15 events and threw
39360
+ * away everything after them. The same rows now cross 50 at a time over
39361
+ * `POST /v1/audit-events/batch` — the route the attach-time drain has always
39362
+ * used — so the same budget admits ~750. The wire cap is the server's own
39363
+ * constant, sized against server cost, and the client REFUSES a longer array
39364
+ * client-side, so the chunking here is not a convention.
39365
+ *
39366
+ * Still serial, and still for the original reason: firing N requests at once
39367
+ * would trade a latency problem for a burst the plane's per-key rate limiting
39368
+ * answers with the refusals the breaker then counts. Fewer, fuller requests is
39369
+ * the fix; more concurrent ones is not.
39370
+ *
39371
+ * When the deadline passes the remainder is dropped rather than sent: the
39372
+ * local write has already succeeded, so every caller has a correct result to
39373
+ * return. What is dropped is COUNTED, everywhere it can happen — this path
39374
+ * returns BEFORE `ForwardPolicy.run` is reached, so without the tally in
39375
+ * `forward-drops.ts` a slow-but-answering plane produces no failures, keeps
39376
+ * the breaker closed, renders a healthy block, and discards the tail of every
39377
+ * batch indefinitely. The SAME tally also covers a single that fails inside
39378
+ * the per-item retry below — the breaker opening mid-retry is a failure the
39379
+ * breaker's own state DOES capture, but the events still in this chunk once
39380
+ * that happens are neither delivered nor otherwise counted anywhere, which is
39381
+ * the same invisibility with a different cause.
38282
39382
  *
38283
- * Per-item budgets bound each request and nothing bounded their sum see
38284
- * BATCH_FORWARD_BUDGET_MS. When the deadline passes the remainder is dropped
38285
- * rather than sent: the local write has already succeeded, so every caller
38286
- * has a correct result to return, and a drop is the outcome this path is
38287
- * built to accept (G8) where a blown hook timeout is not.
39383
+ * `ok` ALONE IS NOT DELIVERY, the same rule `recordCapture` states for the
39384
+ * single-event ack and at fifty times the blast radius here:
39385
+ * `AuditEventBatchAck.accepted` is an aggregate count the wire contract does
39386
+ * not tie to the chunk's own length, so a 2xx answering `{accepted: 30}` for
39387
+ * fifty events is well-formed. Trusting `ok` alone would stamp all fifty as
39388
+ * delivered and never re-offer the twenty the plane did not take. So success
39389
+ * is checked against `chunk.length`; anything short of it falls into the same
39390
+ * per-item pass as a refused chunk, which is the only way to recover the
39391
+ * rows that did not land, since the ack carries no per-row verdict to
39392
+ * resend by.
38288
39393
  *
38289
- * Serial rather than concurrent on purpose. Firing N requests at once would
38290
- * trade a latency problem for a burst the plane's own per-key rate limiting
38291
- * would answer with the refusals the breaker then counts.
39394
+ * That fallback ASSUMES a re-send of an already-landed row is a harmless
39395
+ * no-op rather than a second cost an assumption this file cannot verify.
39396
+ * `AuditEventBatchAck` carries only `accepted`, unlike its sibling
39397
+ * `IngestAck` (`accepted` + `duplicates`, with `accepted + duplicates ==`
39398
+ * the batch size as the invariant `recordCapture` reads), so whether a
39399
+ * duplicate counts toward THIS route's `accepted` is not expressed
39400
+ * anywhere in this repo. If it follows its sibling's convention and does
39401
+ * NOT, a chunk containing even one already-delivered row — the ordinary
39402
+ * consequence of a lost stamp, which this file already treats as cheap —
39403
+ * answers short forever and enters the per-item pass on every pass it is
39404
+ * offered again. The cost of that is bounded rather than silent: the
39405
+ * pass converges (every row lands and stamps), so it is one wasted round
39406
+ * of singles rather than a stall, and it errs toward an extra resend
39407
+ * rather than toward the lost row the alternative risks.
38292
39408
  *
38293
- * WHAT IS DROPPED IS COUNTED. Every other forward failure ends in
38294
- * `ForwardPolicy.run`'s catch and moves the breaker's file, which is what
38295
- * lets status call the forward unhealthy; this path returns BEFORE `run` is
38296
- * reached, so without the tally in `forward-drops.ts` a slow-but-answering
38297
- * plane produces no failures, keeps the breaker closed, renders a healthy
38298
- * block, and discards the tail of every batch indefinitely.
39409
+ * BATCH-ATOMIC SETTLEMENT is otherwise the rule: the receiver wraps a chunk in
39410
+ * one transaction, so a full 2xx settles every event in it and a non-2xx
39411
+ * settles none which is why the whole chunk is stamped together on a FULL
39412
+ * accept and none of it otherwise. THREE reasons do not deserve whole-chunk
39413
+ * treatment, alongside a short accept, and all are re-sent one event at a
39414
+ * time:
39415
+ *
39416
+ * `invalid-request` a chunk the client refused to send at all. One malformed
39417
+ * event would otherwise cost the 49 good ones beside it —
39418
+ * a new way to lose data introduced by the very change
39419
+ * meant to stop losing it.
39420
+ * `route-absent` a deployment that predates the batch route. The
39421
+ * single-event route is the one it serves, and re-sending
39422
+ * here rather than inside the client is what gives each
39423
+ * request its own budget instead of 50 inside one.
39424
+ * `rejected` the deployment's SERVER-side twin of `invalid-request` —
39425
+ * a 4xx body refusal from schema drift on the other side
39426
+ * of the wire. Settlement is batch-atomic on this reason
39427
+ * exactly as on the others, so leaving it out would cost
39428
+ * the whole chunk for one event the DEPLOYMENT considers
39429
+ * malformed, where the per-item form cost only that one.
39430
+ *
39431
+ * Every other reason (breaker-open, a refusal, a timeout) applies to the whole
39432
+ * chunk, and re-sending it item by item would just spend the budget failing 50
39433
+ * more times — for those, the blast radius stays exactly what it was before
39434
+ * batching.
38299
39435
  */
38300
39436
  async forwardBatch(inputs, toEvent) {
38301
39437
  const deadline = Date.now() + BATCH_FORWARD_BUDGET_MS;
38302
- for (let i = 0; i < inputs.length; i += 1) {
38303
- const now = Date.now();
38304
- if (now >= deadline) {
38305
- recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
38306
- return;
39438
+ const delivered = [];
39439
+ try {
39440
+ for (let i = 0; i < inputs.length; i += AUDIT_EVENT_BATCH_MAX) {
39441
+ const now = Date.now();
39442
+ if (now >= deadline) {
39443
+ recordForwardDrops(this.deps.dataDir, inputs.length - i, now);
39444
+ return;
39445
+ }
39446
+ const chunk = inputs.slice(i, i + AUDIT_EVENT_BATCH_MAX).map((input2) => toEvent(input2));
39447
+ const forwarded = await this.deps.forward.run(
39448
+ () => this.deps.client.recordAuditEvents(
39449
+ chunk.map((event) => reKeyForForward(event, this.remoteInventory))
39450
+ )
39451
+ );
39452
+ if (forwarded.ok) {
39453
+ if (forwarded.value.accepted === chunk.length) {
39454
+ delivered.push(...chunk);
39455
+ continue;
39456
+ }
39457
+ } else if (
39458
+ // THREE reasons are worth a second pass, one at a time, and they are
39459
+ // the three settled BEFORE the control plane refused anything, or
39460
+ // (for `rejected`) refused the BODY rather than the connection.
39461
+ //
39462
+ // `invalid-request` — the CLIENT refused the body before any request
39463
+ // went out: a defect in one event, not an outage. Re-sending singly
39464
+ // isolates the bad one instead of charging its 49 neighbours for it.
39465
+ //
39466
+ // `route-absent` — the deployment predates the batch route and serves
39467
+ // only the single-event one. The retry IS the compatibility path, and
39468
+ // it has to live HERE rather than inside the client: each single gets
39469
+ // its own FORWARD_BUDGET_MS through `run`, whereas the client's own
39470
+ // fallback would spend 50 sequential round trips inside the ONE
39471
+ // budget wrapping this call — turning a working older deployment into
39472
+ // a timeout, three of those into an open breaker, and every row into
39473
+ // a silent drop while the status surface called an answering
39474
+ // deployment down.
39475
+ //
39476
+ // `rejected` — the deployment's own 4xx refusal of the body, the
39477
+ // server-side twin of `invalid-request`: isolating it the same way
39478
+ // costs one event instead of the whole chunk for a defect the
39479
+ // deployment considers local to one row.
39480
+ //
39481
+ // Every other reason (breaker-open, a refusal, a timeout) applies to
39482
+ // the whole chunk; re-sending it item by item would just spend the
39483
+ // budget failing 50 more times.
39484
+ forwarded.reason !== "invalid-request" && forwarded.reason !== "route-absent" && forwarded.reason !== "rejected"
39485
+ ) {
39486
+ continue;
39487
+ }
39488
+ for (const [j, event] of chunk.entries()) {
39489
+ const at = Date.now();
39490
+ if (at >= deadline) {
39491
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
39492
+ return;
39493
+ }
39494
+ const single = await this.deps.forward.run(
39495
+ () => this.deps.client.recordAuditEvent(reKeyForForward(event, this.remoteInventory))
39496
+ );
39497
+ if (single.ok) {
39498
+ delivered.push(event);
39499
+ continue;
39500
+ }
39501
+ if (single.reason === "breaker-open") {
39502
+ recordForwardDrops(this.deps.dataDir, inputs.length - i - j, at);
39503
+ return;
39504
+ }
39505
+ recordForwardDrops(this.deps.dataDir, 1, at);
39506
+ }
39507
+ }
39508
+ } finally {
39509
+ try {
39510
+ this.deps.local.markAuditEventsDelivered(delivered, Date.now());
39511
+ } catch {
38307
39512
  }
38308
- const input2 = inputs[i];
38309
- await this.deps.forward.run(
38310
- () => this.deps.client.recordAuditEvent(reKeyForForward(toEvent(input2), this.remoteInventory))
38311
- );
38312
39513
  }
38313
39514
  }
38314
39515
  // Delegated as a BATCH rather than looped over recordLlmCall: the inner
@@ -38351,9 +39552,10 @@ var AttachedDataGateway = class {
38351
39552
  // local store.
38352
39553
  async recordConfigScan(record2) {
38353
39554
  await this.deps.local.recordConfigScan(record2);
38354
- await this.deps.forward.run(
39555
+ const forwarded = await this.deps.forward.run(
38355
39556
  () => this.deps.client.recordAuditEvent(reKeyForForward(record2.scanEvent, this.remoteInventory))
38356
39557
  );
39558
+ if (forwarded.ok) this.deps.local.markAuditEventsDelivered([record2.scanEvent], Date.now());
38357
39559
  }
38358
39560
  async recordBlockedDetection(entry) {
38359
39561
  return this.deps.local.recordBlockedDetection(entry);
@@ -38487,6 +39689,18 @@ var AttachedDataGateway = class {
38487
39689
  // exactly what it did, leaving the whole control inert on every device
38488
39690
  // while every test around it stayed green.
38489
39691
  prohibitedModels: cached2.prohibitedModels
39692
+ // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
39693
+ // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
39694
+ // it emits, so an 'authored' policy arriving from the control plane
39695
+ // keeps that marker even where the clamp rebuilds it with a stronger
39696
+ // action. The device reads it in exactly one direction — the rules such a
39697
+ // policy targets are not locally re-assignable — so it sits on the
39698
+ // `prohibitedModels` side of the line for the same reason that field
39699
+ // does: it can only ever ADD a refusal, never relax one, and an unsigned
39700
+ // cache therefore has no relaxation to grant by carrying it. Dropping it
39701
+ // would be the silent failure rather than the safe one — the action would
39702
+ // still be enforced while the local override the organization authored
39703
+ // away quietly came back.
38490
39704
  // `rulesComplete` is a STANDALONE-ONLY signal (the user's local installed
38491
39705
  // snapshot) and is taken from the LOCAL bundle only — never from the wire
38492
39706
  // or the on-disk cache. Honoring a cached one would hand the control plane, or
@@ -38526,10 +39740,10 @@ var AttachedDataGateway = class {
38526
39740
  //
38527
39741
  // Implementing these is what actually closes the skipped-local-maintenance
38528
39742
  // gap: the OSS structural guard `hasLocalStoreMaintenance()` is satisfied by
38529
- // any object carrying all five, so the composite qualifies and SessionStart
39743
+ // any object carrying them all, so the composite qualifies and SessionStart
38530
39744
  // runs maintenance on the device's real store.
38531
39745
  //
38532
- // ⚠ Three of the six are SYNCHRONOUS and must stay that way. `handle-session-start`
39746
+ // ⚠ Several of them are SYNCHRONOUS and must stay that way. `handle-session-start`
38533
39747
  // calls `capWarnEraEnforcement` without `await` and uses `staleBinaryNotice`'s
38534
39748
  // return value directly; declaring them `async` here would hand those call
38535
39749
  // sites a Promise and silently break both.
@@ -38552,9 +39766,15 @@ var AttachedDataGateway = class {
38552
39766
  // Delegated like the rest, and SYNCHRONOUS for the reason the note above
38553
39767
  // gives: `recordCapture` calls it after the forward has already settled, on a
38554
39768
  // path that has nothing left to await.
39769
+ markCaptureOwed(event) {
39770
+ this.deps.local.markCaptureOwed(event);
39771
+ }
38555
39772
  markCaptureDelivered(event, atMs) {
38556
39773
  this.deps.local.markCaptureDelivered(event, atMs);
38557
39774
  }
39775
+ markAuditEventsDelivered(events, atMs) {
39776
+ this.deps.local.markAuditEventsDelivered(events, atMs);
39777
+ }
38558
39778
  };
38559
39779
  function reKeyForForward(event, remote) {
38560
39780
  if (remote === null) {
@@ -38597,281 +39817,17 @@ function toolAuditEvent(input2) {
38597
39817
  }
38598
39818
 
38599
39819
  // ../../packages/plugin-runtime/src/attached/history-state.ts
38600
- import { readFileSync as readFileSync15 } from "fs";
38601
- import { join as join23 } from "path";
39820
+ import { readFileSync as readFileSync17 } from "fs";
39821
+ import { join as join25 } from "path";
38602
39822
 
38603
39823
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
38604
39824
  import { createHash as createHash6 } from "crypto";
38605
39825
  import { hostname as hostname5 } from "os";
38606
39826
 
38607
- // ../../packages/remote/src/http.ts
38608
- import { request as httpRequest } from "http";
38609
- import { request as httpsRequest } from "https";
38610
- var DEFAULT_TIMEOUT_MS = 1e4;
38611
- var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
38612
- var RemoteRequestError = class extends Error {
38613
- constructor(status) {
38614
- super(`control-plane request failed with status ${String(status)}`);
38615
- this.status = status;
38616
- this.name = "RemoteRequestError";
38617
- }
38618
- status;
38619
- };
38620
- var RemoteRequestInvalid = class extends Error {
38621
- constructor(route, cause) {
38622
- super(`refusing to send a malformed body to ${route}`);
38623
- this.cause = cause;
38624
- this.name = "RemoteRequestInvalid";
38625
- }
38626
- cause;
38627
- };
38628
- var RemoteResponseInvalid = class extends Error {
38629
- constructor(route, detail) {
38630
- super(`control plane answered ${route} with ${detail}`);
38631
- this.name = "RemoteResponseInvalid";
38632
- }
38633
- };
38634
- var RemoteTransportError = class extends Error {
38635
- /**
38636
- * The status the peer sent, when headers arrived and only the BODY was
38637
- * refused.
38638
- *
38639
- * Undefined for the ordinary case this class was written for — no answer at
38640
- * all. It exists because two paths reject after a status has already been
38641
- * delivered: an oversized body and an aborted response. Discarding it there
38642
- * reported a deployment answering 401 with a verbose body as a network
38643
- * outage, which sends the reader to look at their network instead of their
38644
- * credential.
38645
- */
38646
- constructor(reason, status) {
38647
- super(`control-plane request did not complete: ${reason}`);
38648
- this.status = status;
38649
- this.name = "RemoteTransportError";
38650
- }
38651
- status;
38652
- };
38653
- async function send(options) {
38654
- const url2 = new URL(options.url);
38655
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
38656
- const send_ = url2.protocol === "http:" ? httpRequest : httpsRequest;
38657
- const requestOptions = {
38658
- method: options.method,
38659
- headers: {
38660
- // CALLER HEADERS FIRST, so this module's own are not overridable. Spread
38661
- // last they win, and two of the values below are ones no caller may
38662
- // replace: `x-api-key` is the credential, and `content-length` is the
38663
- // byte count that stops a multi-byte body being truncated by the
38664
- // receiver. `SendOptions.headers` is a free-form record on an exported
38665
- // function, so "no caller does that today" is not the guarantee to rely
38666
- // on. The one header any caller actually passes — `if-none-match` on the
38667
- // conditional GET — is untouched by this order.
38668
- ...options.headers,
38669
- // The credential. One header, matching what the deployment authenticates
38670
- // on; a second copy in an `Authorization` header would be one more place
38671
- // it can be logged by an intermediary for no gain.
38672
- //
38673
- // Spread conditionally rather than assigned as `undefined`: Node's header
38674
- // handling and `content-length` bookkeeping treat a present-but-undefined
38675
- // key differently from an absent one, and "the header is not there" is
38676
- // the property the attach flow needs.
38677
- ...options.apiKey === void 0 ? {} : { "x-api-key": options.apiKey },
38678
- accept: "application/json",
38679
- ...options.body === void 0 ? {} : {
38680
- "content-type": "application/json",
38681
- // Byte length, not string length: a multi-byte body sent with a
38682
- // character count is truncated by the receiver.
38683
- "content-length": String(Buffer.byteLength(options.body))
38684
- }
38685
- }
38686
- };
38687
- return new Promise((resolve2, reject) => {
38688
- let settled = false;
38689
- const fail = (reason, status) => {
38690
- if (settled) return;
38691
- settled = true;
38692
- reject(new RemoteTransportError(reason, status));
38693
- };
38694
- const req = send_(url2, requestOptions, (res) => {
38695
- const chunks = [];
38696
- let size = 0;
38697
- res.on("data", (chunk) => {
38698
- size += chunk.length;
38699
- if (size > MAX_RESPONSE_BYTES) {
38700
- fail(`response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`, res.statusCode);
38701
- res.destroy();
38702
- req.destroy();
38703
- return;
38704
- }
38705
- chunks.push(chunk);
38706
- });
38707
- res.on("aborted", () => {
38708
- fail("the response was aborted", res.statusCode);
38709
- });
38710
- res.on("end", () => {
38711
- if (settled) return;
38712
- settled = true;
38713
- resolve2({
38714
- status: res.statusCode ?? 0,
38715
- headers: res.headers,
38716
- body: Buffer.concat(chunks).toString("utf8")
38717
- });
38718
- });
38719
- });
38720
- const deadline = setTimeout(() => {
38721
- fail(`no response within ${String(timeoutMs)}ms`);
38722
- req.destroy();
38723
- }, timeoutMs);
38724
- deadline.unref();
38725
- req.on("upgrade", (_res, socket) => {
38726
- fail("the deployment answered with a protocol upgrade");
38727
- socket.destroy();
38728
- });
38729
- req.on("close", () => {
38730
- fail("the connection closed before a response was read");
38731
- clearTimeout(deadline);
38732
- });
38733
- req.on("error", (err) => {
38734
- fail(err.message);
38735
- });
38736
- if (options.body !== void 0) req.write(options.body);
38737
- req.end();
38738
- });
38739
- }
38740
-
38741
- // ../../packages/remote/src/client.ts
38742
- var ROUTES = {
38743
- events: "/v1/events",
38744
- auditEvents: "/v1/audit-events",
38745
- auditEventsBatch: "/v1/audit-events/batch",
38746
- inventory: "/v1/inventory",
38747
- storePosture: "/v1/store-posture",
38748
- policyBundle: "/v1/policy-bundle",
38749
- whoami: "/v1/plugin/whoami",
38750
- shares: "/v1/shares"
38751
- };
38752
- function headerValue(response, name) {
38753
- const raw = response.headers[name];
38754
- if (raw === void 0) return void 0;
38755
- return Array.isArray(raw) ? raw[0] : raw;
38756
- }
38757
- function okBody(response) {
38758
- if (response.status < 200 || response.status >= 300) {
38759
- throw new RemoteRequestError(response.status);
38760
- }
38761
- return response.body;
38762
- }
38763
- function parsed(schema, body, route) {
38764
- let json2;
38765
- try {
38766
- json2 = JSON.parse(body);
38767
- } catch {
38768
- throw new RemoteResponseInvalid(route, "a body that is not JSON");
38769
- }
38770
- const result = schema.safeParse(json2);
38771
- if (!result.success) {
38772
- throw new RemoteResponseInvalid(route, "a body this client cannot read");
38773
- }
38774
- return result.data;
38775
- }
38776
- function withoutTrailingSlashes(endpoint) {
38777
- let end = endpoint.length;
38778
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
38779
- return endpoint.slice(0, end);
38780
- }
38781
- var SLASH = "/".charCodeAt(0);
38782
- function createRemoteClient(options) {
38783
- const base = withoutTrailingSlashes(options.endpoint);
38784
- const url2 = (route) => `${base}${route}`;
38785
- const common = { apiKey: options.apiKey, timeoutMs: options.timeoutMs };
38786
- const sendOne = async (event) => {
38787
- const validated = RecordAuditEventRequest.safeParse(event);
38788
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.auditEvents, validated.error);
38789
- const response = await send({
38790
- ...common,
38791
- method: "POST",
38792
- url: url2(ROUTES.auditEvents),
38793
- body: JSON.stringify(validated.data)
38794
- });
38795
- okBody(response);
38796
- };
38797
- return {
38798
- async ingestEvents(batch) {
38799
- const response = await send({
38800
- ...common,
38801
- method: "POST",
38802
- url: url2(ROUTES.events),
38803
- body: JSON.stringify(batch)
38804
- });
38805
- return parsed(IngestAck, okBody(response), ROUTES.events);
38806
- },
38807
- async ingestInventory(context) {
38808
- const response = await send({
38809
- ...common,
38810
- method: "POST",
38811
- url: url2(ROUTES.inventory),
38812
- body: JSON.stringify(context)
38813
- });
38814
- return parsed(ResolvedInventory, okBody(response), ROUTES.inventory);
38815
- },
38816
- async recordAuditEvent(event) {
38817
- await sendOne(event);
38818
- },
38819
- async recordAuditEvents(events) {
38820
- const validated = RecordAuditEventBatch.safeParse({ events });
38821
- if (!validated.success) {
38822
- throw new RemoteRequestInvalid(ROUTES.auditEventsBatch, validated.error);
38823
- }
38824
- const response = await send({
38825
- ...common,
38826
- method: "POST",
38827
- url: url2(ROUTES.auditEventsBatch),
38828
- body: JSON.stringify(validated.data)
38829
- });
38830
- if (response.status === 404) {
38831
- for (const event of validated.data.events) await sendOne(event);
38832
- return { accepted: validated.data.events.length };
38833
- }
38834
- return parsed(AuditEventBatchAck, okBody(response), ROUTES.auditEventsBatch);
38835
- },
38836
- async reportStorePosture(snapshot) {
38837
- const response = await send({
38838
- ...common,
38839
- method: "POST",
38840
- url: url2(ROUTES.storePosture),
38841
- body: JSON.stringify(snapshot)
38842
- });
38843
- okBody(response);
38844
- },
38845
- async getPolicyBundle(etag) {
38846
- const response = await send({
38847
- ...common,
38848
- method: "GET",
38849
- url: url2(ROUTES.policyBundle),
38850
- ...etag === void 0 ? {} : { headers: { "if-none-match": etag } }
38851
- });
38852
- if (response.status === 304) {
38853
- return { changed: false, etag: headerValue(response, "etag") ?? etag };
38854
- }
38855
- const bundle = parsed(PolicyBundle, okBody(response), ROUTES.policyBundle);
38856
- return { changed: true, bundle, etag: headerValue(response, "etag") };
38857
- },
38858
- async whoami() {
38859
- const response = await send({ ...common, method: "GET", url: url2(ROUTES.whoami) });
38860
- return parsed(PluginWhoami, okBody(response), ROUTES.whoami);
38861
- },
38862
- async recordProjectEgress(request) {
38863
- const validated = EgressIngestRequest.safeParse(request);
38864
- if (!validated.success) throw new RemoteRequestInvalid(ROUTES.shares, validated.error);
38865
- const response = await send({
38866
- ...common,
38867
- method: "POST",
38868
- url: url2(ROUTES.shares),
38869
- body: JSON.stringify(validated.data)
38870
- });
38871
- okBody(response);
38872
- }
38873
- };
38874
- }
39827
+ // ../../packages/plugin-runtime/src/attached/capture-rebuild.ts
39828
+ var CORRELATION_ID = EventMetadata.shape.correlationId;
39829
+ var TRACE_ID = EventMetadata.shape.traceId;
39830
+ var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
38875
39831
 
38876
39832
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
38877
39833
  import { spawn } from "child_process";
@@ -38879,7 +39835,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
38879
39835
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
38880
39836
 
38881
39837
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
38882
- import { readFileSync as readFileSync16 } from "fs";
39838
+ import { readFileSync as readFileSync18 } from "fs";
38883
39839
  function createPluginBlock(build, policyStore) {
38884
39840
  return async () => {
38885
39841
  const cached2 = await policyStore.read();
@@ -38896,9 +39852,9 @@ function createPluginBlock(build, policyStore) {
38896
39852
  }
38897
39853
 
38898
39854
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38899
- import { randomUUID as randomUUID16 } from "crypto";
39855
+ import { randomUUID as randomUUID17 } from "crypto";
38900
39856
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
38901
- import { join as join24 } from "path";
39857
+ import { join as join26 } from "path";
38902
39858
 
38903
39859
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
38904
39860
  import { rename as rename2 } from "fs/promises";
@@ -38922,7 +39878,7 @@ async function publishByRename(tmp, file2, move = rename2) {
38922
39878
 
38923
39879
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
38924
39880
  function createPolicyStore(dir = dataDir()) {
38925
- const file2 = join24(dir, "policy-cache.json");
39881
+ const file2 = join26(dir, "policy-cache.json");
38926
39882
  async function read() {
38927
39883
  try {
38928
39884
  const raw = await readFile2(file2, "utf8");
@@ -38931,22 +39887,32 @@ function createPolicyStore(dir = dataDir()) {
38931
39887
  const record2 = parsed2;
38932
39888
  const bundle = PolicyBundle.parse(record2.bundle);
38933
39889
  const fetchedAtMs = typeof record2.fetchedAtMs === "number" ? record2.fetchedAtMs : 0;
38934
- const etag = typeof record2.etag === "string" ? record2.etag : void 0;
39890
+ const stored = typeof record2.etag === "string" ? record2.etag : void 0;
39891
+ const replayable = record2.shapeId === POLICY_BUNDLE_SHAPE_ID || knowsMoreThanThisBuild(record2.shapeId);
39892
+ const etag = replayable ? stored : void 0;
38935
39893
  return { bundle, fetchedAtMs, ...etag === void 0 ? {} : { etag } };
38936
39894
  } catch {
38937
39895
  return null;
38938
39896
  }
38939
39897
  }
38940
- async function write(bundle, etag) {
38941
- await ensureDataDir(dir);
38942
- const stored = {
38943
- bundle,
38944
- fetchedAtMs: Date.now(),
38945
- ...etag === void 0 ? {} : { etag }
38946
- };
38947
- const tmp = `${file2}.${randomUUID16()}.tmp`;
39898
+ function knowsMoreThanThisBuild(shapeId) {
39899
+ if (typeof shapeId !== "string" || shapeId === "") return false;
39900
+ const theirs = new Set(shapeId.split(","));
39901
+ const ours = new Set(POLICY_BUNDLE_SHAPE_ID.split(","));
39902
+ return theirs.size > ours.size && [...ours].every((key) => theirs.has(key));
39903
+ }
39904
+ async function priorRecord() {
39905
+ try {
39906
+ const parsed2 = JSON.parse(await readFile2(file2, "utf8"));
39907
+ return typeof parsed2 === "object" && parsed2 !== null ? parsed2 : null;
39908
+ } catch {
39909
+ return null;
39910
+ }
39911
+ }
39912
+ async function publishRecord(record2) {
39913
+ const tmp = `${file2}.${randomUUID17()}.tmp`;
38948
39914
  try {
38949
- await writeFile2(tmp, JSON.stringify(stored), {
39915
+ await writeFile2(tmp, JSON.stringify(record2), {
38950
39916
  encoding: "utf8",
38951
39917
  mode: DATA_FILE_MODE,
38952
39918
  flag: "wx"
@@ -38957,6 +39923,27 @@ function createPolicyStore(dir = dataDir()) {
38957
39923
  throw err;
38958
39924
  }
38959
39925
  }
39926
+ async function write(bundle, etag) {
39927
+ await ensureDataDir(dir);
39928
+ const prior = await priorRecord();
39929
+ const priorVersion = prior?.bundle?.version;
39930
+ if (prior !== null && knowsMoreThanThisBuild(prior.shapeId) && priorVersion === bundle.version) {
39931
+ await publishRecord({
39932
+ ...prior,
39933
+ fetchedAtMs: Date.now()
39934
+ });
39935
+ return;
39936
+ }
39937
+ await publishRecord({
39938
+ bundle,
39939
+ fetchedAtMs: Date.now(),
39940
+ // Stamped on EVERY write, the 304 arm's included: that arm hands back the
39941
+ // bundle it already holds, and the point of the stamp is to describe the
39942
+ // build that last narrowed those bytes, which is this one.
39943
+ shapeId: POLICY_BUNDLE_SHAPE_ID,
39944
+ ...etag === void 0 ? {} : { etag }
39945
+ });
39946
+ }
38960
39947
  return { read, write, file: file2 };
38961
39948
  }
38962
39949
 
@@ -39009,7 +39996,7 @@ function createPostureReporter(deps) {
39009
39996
  }
39010
39997
 
39011
39998
  // ../../packages/plugin-runtime/src/attached/posture-snapshot.ts
39012
- import { statSync as statSync9 } from "fs";
39999
+ import { statSync as statSync10 } from "fs";
39013
40000
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
39014
40001
 
39015
40002
  // ../../packages/plugin-runtime/src/attached/action-counts.ts
@@ -39040,7 +40027,7 @@ function emptyReadout(readError = false) {
39040
40027
  }
39041
40028
  function readStorePosture(dbPath2) {
39042
40029
  try {
39043
- statSync9(dbPath2);
40030
+ statSync10(dbPath2);
39044
40031
  } catch (err) {
39045
40032
  const code = err.code;
39046
40033
  if (code === "ENOENT" || code === "ENOTDIR") return emptyReadout();
@@ -39120,16 +40107,16 @@ function readStorePosture(dbPath2) {
39120
40107
  }
39121
40108
 
39122
40109
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
39123
- import { randomUUID as randomUUID17 } from "crypto";
40110
+ import { randomUUID as randomUUID18 } from "crypto";
39124
40111
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
39125
- import { join as join25 } from "path";
40112
+ import { join as join27 } from "path";
39126
40113
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39127
40114
  function createPostureStore(dir = settingsDir(), legacyDir) {
39128
- const file2 = join25(dir, "posture-state.json");
39129
- const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
40115
+ const file2 = join27(dir, "posture-state.json");
40116
+ const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
39130
40117
  async function persist(state) {
39131
40118
  await ensureDataDir(dir);
39132
- const tmp = `${file2}.${randomUUID17()}.tmp`;
40119
+ const tmp = `${file2}.${randomUUID18()}.tmp`;
39133
40120
  try {
39134
40121
  await writeFile3(tmp, JSON.stringify(state), { encoding: "utf8", mode: DATA_FILE_MODE });
39135
40122
  await publishByRename(tmp, file2);
@@ -39171,7 +40158,7 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
39171
40158
  }
39172
40159
  return legacy;
39173
40160
  }
39174
- const fresh = { deviceId: randomUUID17(), lastAttemptedAtMs: 0 };
40161
+ const fresh = { deviceId: randomUUID18(), lastAttemptedAtMs: 0 };
39175
40162
  try {
39176
40163
  await ensureDataDir(dir);
39177
40164
  if (createOwnerOnlyFileSync(file2, JSON.stringify(fresh))) return fresh;
@@ -39194,8 +40181,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
39194
40181
  }
39195
40182
 
39196
40183
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
39197
- import { readFileSync as readFileSync17 } from "fs";
39198
- import { join as join26 } from "path";
40184
+ import { readFileSync as readFileSync19 } from "fs";
40185
+ import { join as join28 } from "path";
39199
40186
 
39200
40187
  // ../../packages/plugin-runtime/src/attached/status.ts
39201
40188
  var REFUSAL_LINES = {
@@ -39220,7 +40207,7 @@ var SYNC_THROTTLE_MS = 15 * 60 * 1e3;
39220
40207
  import { hostname as hostname6 } from "os";
39221
40208
 
39222
40209
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
39223
- import { randomUUID as randomUUID18 } from "crypto";
40210
+ import { randomUUID as randomUUID19 } from "crypto";
39224
40211
 
39225
40212
  // ../../packages/plugin-runtime/src/recorder.ts
39226
40213
  var PLUGIN_RECORDER_BINARY = "plugin";
@@ -39428,7 +40415,7 @@ var StandaloneDataGateway = class {
39428
40415
  const customKeywords = [...new Set(policies.flatMap((p) => p.customKeywords ?? []))];
39429
40416
  const installed = this.installedScanRules();
39430
40417
  const rulePolicies = installed ? [...installed.ruleActions].map(([ruleId, action]) => ({
39431
- id: randomUUID18(),
40418
+ id: randomUUID19(),
39432
40419
  scope: "global",
39433
40420
  target: { ruleId },
39434
40421
  action,
@@ -39513,9 +40500,21 @@ var StandaloneDataGateway = class {
39513
40500
  // for the whole of it, so a member that threw would make that answer a lie
39514
40501
  // the moment a composite delegated to it. A store-level no-op is the honest
39515
40502
  // shape — a standalone machine has nothing delivered to record.
40503
+ markCaptureOwed(event) {
40504
+ this.db.markCaptureOwed(event);
40505
+ }
39516
40506
  markCaptureDelivered(event, atMs) {
39517
40507
  this.db.markCaptureDelivered(event, atMs);
39518
40508
  }
40509
+ // Implemented, not stubbed, for the same reason its sibling above is: the
40510
+ // attached gateway is a DECORATOR over an instance of this class
40511
+ // (`attached/factory.ts` builds one and passes it as `deps.local`), so every
40512
+ // stamp the live forward makes lands here with a non-empty array. This is the
40513
+ // production write path for that feature, not a shape-satisfying no-op — a
40514
+ // machine that is merely standalone simply never calls it.
40515
+ markAuditEventsDelivered(events, atMs) {
40516
+ this.db.markAuditEventsDelivered(events, atMs);
40517
+ }
39519
40518
  staleBinaryNotice(currentVersion) {
39520
40519
  try {
39521
40520
  const newest = this.db.installedPacks.newestRecordedBinary();
@@ -39650,7 +40649,7 @@ function resolveDataGateway(config2, meta4, gatewayFactory = defaultGatewayFacto
39650
40649
  }
39651
40650
 
39652
40651
  // ../../packages/plugin-runtime/src/handle-session-start.ts
39653
- import { randomUUID as randomUUID19 } from "crypto";
40652
+ import { randomUUID as randomUUID20 } from "crypto";
39654
40653
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
39655
40654
 
39656
40655
  // src/hooks/store-health.ts
@@ -39674,12 +40673,12 @@ function claimStoreUnavailableWarning(dataDir2, sessionId) {
39674
40673
  return true;
39675
40674
  }
39676
40675
  function markerDirs(dataDir2) {
39677
- return [dataDir2, dirname5(dataDir2)];
40676
+ return [dataDir2, dirname7(dataDir2)];
39678
40677
  }
39679
40678
  function alreadyClaimed(dirs, marker, sessionId) {
39680
40679
  return dirs.some((dir) => {
39681
40680
  try {
39682
- return readFileSync18(join27(dir, marker), "utf8") === sessionId;
40681
+ return readFileSync20(join29(dir, marker), "utf8") === sessionId;
39683
40682
  } catch {
39684
40683
  return false;
39685
40684
  }
@@ -39689,7 +40688,7 @@ function recordClaim(dirs, marker, sessionId) {
39689
40688
  for (const dir of dirs) {
39690
40689
  try {
39691
40690
  mkdirSync6(dir, { recursive: true, mode: DATA_DIR_MODE });
39692
- writeFileSync9(join27(dir, marker), sessionId, { mode: DATA_FILE_MODE });
40691
+ writeFileSync9(join29(dir, marker), sessionId, { mode: DATA_FILE_MODE });
39693
40692
  return;
39694
40693
  } catch {
39695
40694
  }
@@ -39714,7 +40713,7 @@ function formatMode(mode) {
39714
40713
  }
39715
40714
  function warnIfStoreRedirected(config2, sessionId, write = (message) => void process.stderr.write(message)) {
39716
40715
  try {
39717
- const paths = symlinkedStorePaths(dirname5(config2.dataDir));
40716
+ const paths = symlinkedStorePaths(dirname7(config2.dataDir));
39718
40717
  if (paths.length === 0) return;
39719
40718
  if (!sessionId) {
39720
40719
  write(storeRedirectedMessage(paths));
@@ -39736,10 +40735,22 @@ async function main() {
39736
40735
  const rawToolInput = input2.tool_input;
39737
40736
  if (typeof rawToolInput !== "object" || rawToolInput === null) return;
39738
40737
  const toolInput = rawToolInput;
40738
+ const sessionId = getString(input2, "session_id");
40739
+ let configMemo;
40740
+ const loadConfigOnce = () => configMemo ??= loadConfig();
40741
+ if (await handleSubagentSpawn(
40742
+ () => openGatewayOrNull(loadConfigOnce()),
40743
+ toolName,
40744
+ toolInput,
40745
+ sessionId,
40746
+ getString(input2, "cwd"),
40747
+ emit
40748
+ )) {
40749
+ return;
40750
+ }
39739
40751
  const fields = scannableInputFields(toolName, toolInput);
39740
40752
  if (fields.length === 0) return;
39741
- const config2 = loadConfig();
39742
- const sessionId = getString(input2, "session_id");
40753
+ const config2 = loadConfigOnce();
39743
40754
  warnIfStoreRedirected(config2, sessionId);
39744
40755
  const consented = isVaultConsentValid(config2.settings.vaultConsent);
39745
40756
  const vaultGlue = consented ? createVaultGlue() : null;