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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -492,12 +492,13 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync7 } from "fs";
496
- import { join as join12 } from "path";
495
+ import { existsSync as existsSync8 } from "fs";
496
+ import { join as join14 } 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
 
502
503
  // ../../packages/persistence/src/control-plane-credential.ts
503
504
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
@@ -596,6 +597,30 @@ var SQLITE_MIGRATIONS = [
596
597
  {
597
598
  tag: "0022_audit_inspection_ms",
598
599
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
600
+ },
601
+ {
602
+ tag: "0023_secret_vault_user_authorized",
603
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
604
+ },
605
+ {
606
+ tag: "0024_finding_resolution_key_created_index",
607
+ 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`);"
608
+ },
609
+ {
610
+ tag: "0025_audit_capture_attribute_columns",
611
+ 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;"
612
+ },
613
+ {
614
+ tag: "0026_audit_llm_call_usage_columns",
615
+ 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;"
616
+ },
617
+ {
618
+ tag: "0027_audit_llm_usage_index",
619
+ 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;"
620
+ },
621
+ {
622
+ tag: "0028_activity_session_probe_indexes",
623
+ 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"
599
624
  }
600
625
  ];
601
626
 
@@ -20716,13 +20741,11 @@ var FindingGroup = external_exports.object({
20716
20741
  latestDetectedAt: external_exports.iso.datetime(),
20717
20742
  instances: external_exports.array(FindingInstance),
20718
20743
  // Derived from instances' statuses with open-dominates precedence (see
20719
- // buildFindingGroups). Undefined only when no instance carries a status.
20744
+ // foldGroupStatus). Undefined only when no instance carries a status.
20720
20745
  status: FindingStatus.optional(),
20721
- // The distinct people across the WHOLE group, not just the `instances`
20722
- // preview — from the store's whole-group aggregate when it supplies one,
20723
- // else folded from the rows (see buildFindingGroups). Undefined when no
20724
- // instance carries a user, or when the store supplied whole-group folds
20725
- // without one.
20746
+ // The distinct people across the WHOLE group, not just the instances
20747
+ // carried here. Undefined when no instance carries a user, or when the
20748
+ // store supplied whole-group folds without one.
20726
20749
  users: external_exports.array(FindingUser).optional()
20727
20750
  }).meta({ id: "FindingGroup" });
20728
20751
  var FindingStats = external_exports.object({
@@ -20751,21 +20774,31 @@ var FindingFacets = external_exports.object({
20751
20774
  // counted under no value.
20752
20775
  status: external_exports.array(FindingFacetItem),
20753
20776
  // Host tool (attributes.tool_name). Present only on the instance-level
20754
- // reads, which can filter by it; the grouped read omits the dimension
20777
+ // reads, which can filter by it; the type-level read omits the dimension
20755
20778
  // because a group spans tools.
20756
20779
  tool: external_exports.array(FindingFacetItem).optional()
20757
20780
  }).meta({ id: "FindingFacets" });
20758
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20759
- var ListGroupedFindingsQuery = external_exports.object({
20781
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20782
+ id: "FindingTypeSummary"
20783
+ });
20784
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20785
+ var MAX_FINDING_TYPES_LIMIT = 100;
20786
+ var ListFindingTypesQuery = external_exports.object({
20760
20787
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20761
- // FindingAction.
20788
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20789
+ // firing version carries, and this list pages types.
20790
+ //
20791
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20792
+ // definition versions at different severities, so a type kept by this filter
20793
+ // can hold findings that individually do not match — see totals.findings on
20794
+ // ListFindingTypesResponse, which counts them all.
20762
20795
  severity: external_exports.array(Severity).optional(),
20763
20796
  subtype: external_exports.array(external_exports.string()).optional(),
20764
20797
  provider: external_exports.array(FindingProvider).optional(),
20765
20798
  action: external_exports.array(FindingAction).optional(),
20766
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20767
- // individual instances' — so a filtered group's Status column always reads
20768
- // one of the requested values.
20799
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20800
+ // individual findings' — so a filtered row's status always reads one of the
20801
+ // requested values.
20769
20802
  status: external_exports.array(FindingStatus).optional(),
20770
20803
  q: external_exports.string().optional(),
20771
20804
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20775,23 +20808,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20775
20808
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20776
20809
  // means all time — this list has no default window.
20777
20810
  from: external_exports.iso.datetime().optional(),
20778
- // A group or instance id that must appear in the page even when the cursor
20779
- // has already advanced past its sort position. This is what keeps the
20780
- // Findings page's one-shot ?finding= deep link resolving once the list
20781
- // paginates: the target group is appended out of sort order rather than
20782
- // scanning forward for it. Never affects totals, facets or the cursor.
20811
+ // A RULE id that must appear in the page even when the cursor has already
20812
+ // advanced past its sort position. This is what keeps the selected type
20813
+ // visible in the list once it paginates: the target is appended out of sort
20814
+ // order rather than scanned forward for. Never affects totals, facets or the
20815
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20816
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20817
+ // and so is not bounded by what any page happens to hold.
20783
20818
  includeId: external_exports.string().optional(),
20784
- groupBy: external_exports.literal("type").optional(),
20785
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20819
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20786
20820
  cursor: external_exports.string().optional()
20787
20821
  });
20788
- var ListGroupedFindingsResponse = external_exports.object({
20822
+ var ListFindingTypesResponse = external_exports.object({
20789
20823
  totals: external_exports.object({
20824
+ // Findings belonging to the matching TYPES — not findings that each match
20825
+ // the filters. The filters here select types, so a type that survives
20826
+ // contributes its whole instanceCount.
20827
+ //
20828
+ // `status` is the one exception, narrowed per finding via
20829
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20830
+ // this can exceed what the instance read reports for the same filters: a
20831
+ // rule whose severity moved between versions is kept on its newest and
20832
+ // still counts its older findings. Narrowing the other three needs
20833
+ // per-dimension counts the aggregate does not carry today.
20790
20834
  findings: external_exports.number().int().nonnegative(),
20791
- groups: external_exports.number().int().nonnegative()
20835
+ // Counts TYPES, which is the unit this read pages. The instance read's
20836
+ // own totals count findings; the two deliberately answer different
20837
+ // questions and are never summed.
20838
+ types: external_exports.number().int().nonnegative()
20792
20839
  }),
20793
20840
  facets: FindingFacets,
20794
- items: external_exports.array(FindingGroup),
20841
+ items: external_exports.array(FindingTypeSummary),
20795
20842
  nextCursor: external_exports.string().nullable(),
20796
20843
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20797
20844
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20799,7 +20846,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20799
20846
  // every firing, so the two numbers legitimately differ — this map lets a
20800
20847
  // session-scoped view show both.
20801
20848
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20802
- }).meta({ id: "ListGroupedFindingsResponse" });
20849
+ }).meta({ id: "ListFindingTypesResponse" });
20803
20850
  var ApplyFindingActionRequest = external_exports.object({
20804
20851
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20805
20852
  // it, so it is excluded from the request contract. The mapping helper
@@ -20829,12 +20876,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20829
20876
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20830
20877
  var ListFindingInstancesQuery = external_exports.object({
20831
20878
  severity: external_exports.array(Severity).optional(),
20832
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20879
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20880
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20833
20881
  subtype: external_exports.array(external_exports.string()).optional(),
20834
20882
  provider: external_exports.array(FindingProvider).optional(),
20835
20883
  action: external_exports.array(FindingAction).optional(),
20836
20884
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20837
- // the grouped query's group-level fold.
20885
+ // the types query's type-level fold.
20838
20886
  status: external_exports.array(FindingStatus).optional(),
20839
20887
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20840
20888
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20851,37 +20899,47 @@ var ListFindingInstancesQuery = external_exports.object({
20851
20899
  });
20852
20900
  var ListFindingInstancesResponse = external_exports.object({
20853
20901
  // Instances matching the filters across the whole scope, not just this
20854
- // page — cursor-independent, like the grouped list's totals.
20902
+ // page — cursor-independent, like the types list's totals.
20855
20903
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20856
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20904
+ // Counts in INSTANCES here, where the types response counts types. Each
20857
20905
  // dimension still excludes its own filter.
20858
20906
  facets: FindingFacets,
20859
20907
  items: external_exports.array(FindingInstanceDetail),
20860
20908
  nextCursor: external_exports.string().nullable()
20861
20909
  }).meta({ id: "ListFindingInstancesResponse" });
20862
- var FindingLocationFile = external_exports.object({
20863
- // Empty when the instances carried no file path (a prompt or a tool call
20864
- // with no file attribution).
20865
- file: external_exports.string(),
20866
- instanceCount: external_exports.number().int().nonnegative(),
20867
- maxSeverity: Severity,
20868
- latestDetectedAt: external_exports.iso.datetime(),
20869
- // Folded from the instances' derived statuses with the same
20870
- // open-dominates precedence a group uses.
20871
- status: FindingStatus.optional(),
20872
- // Distinct rules seen at this location, capped — the row shows them as
20873
- // chips, and the count is what conveys scale.
20874
- ruleIds: external_exports.array(external_exports.string())
20875
- }).meta({ id: "FindingLocationFile" });
20876
- var FindingLocationRepo = external_exports.object({
20910
+ var FindingLocationSummary = external_exports.object({
20911
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20912
+ // because a location's identity is two values and a URL param carries one:
20913
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20914
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20915
+ // client's page dedupe — never decoded, and never a sort key.
20916
+ id: external_exports.string(),
20877
20917
  /** Empty when the instances carried no repo attribute. */
20878
20918
  repo: external_exports.string(),
20919
+ // Empty when the instances carried no file path (a prompt, or a tool call
20920
+ // with no file attribution). Both halves empty is a real location — usually
20921
+ // the largest one in a store — and is selectable like any other.
20922
+ file: external_exports.string(),
20879
20923
  instanceCount: external_exports.number().int().nonnegative(),
20924
+ // The WORST severity present, not the first row's. It is this list's primary
20925
+ // sort key, so it is also what explains why a row is where it is, and it is
20926
+ // how a reader decides what to open without opening everything.
20880
20927
  maxSeverity: Severity,
20881
20928
  latestDetectedAt: external_exports.iso.datetime(),
20929
+ // Folded from the instances' derived statuses with the same open-dominates
20930
+ // precedence a group uses, so it answers "is anything left to do here" and
20931
+ // not much more: a location holding 1 open among 40 resolved reads like one
20932
+ // holding 40 open. That loss is accepted — the panel beside this list
20933
+ // carries each finding's own status, and instanceCount sits next to the
20934
+ // badge.
20882
20935
  status: FindingStatus.optional(),
20883
- files: external_exports.array(FindingLocationFile)
20884
- }).meta({ id: "FindingLocationRepo" });
20936
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20937
+ // tally rather than a sample and a row can say how many there are. Bounded
20938
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20939
+ ruleIds: external_exports.array(external_exports.string())
20940
+ }).meta({ id: "FindingLocationSummary" });
20941
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20942
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20885
20943
  var ListFindingLocationsQuery = external_exports.object({
20886
20944
  severity: external_exports.array(Severity).optional(),
20887
20945
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20894,18 +20952,42 @@ var ListFindingLocationsQuery = external_exports.object({
20894
20952
  q: external_exports.string().optional(),
20895
20953
  sessionId: external_exports.string().optional(),
20896
20954
  from: external_exports.iso.datetime().optional(),
20897
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
20955
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
20956
+ // even when the cursor has already advanced past its sort position — the
20957
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
20958
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
20959
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
20960
+ // into the thousands, a selection sitting off page 0 is the ordinary case
20961
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
20962
+ includeId: external_exports.string().optional(),
20963
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
20964
+ cursor: external_exports.string().optional()
20898
20965
  });
20899
20966
  var ListFindingLocationsResponse = external_exports.object({
20900
20967
  totals: external_exports.object({
20968
+ // Findings matching the filters across the whole scope. Unlike the types
20969
+ // read's same-named field this needs no caveat: the filters here narrow
20970
+ // per finding, so this is the sum of every row's instanceCount.
20901
20971
  findings: external_exports.number().int().nonnegative(),
20902
- repos: external_exports.number().int().nonnegative(),
20903
- files: external_exports.number().int().nonnegative()
20972
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
20973
+ // states. The facets beside it count FINDINGS (see below); a surface
20974
+ // showing both says which is which.
20975
+ locations: external_exports.number().int().nonnegative()
20904
20976
  }),
20905
- /** Sorted by max severity, then most recent. */
20906
- items: external_exports.array(FindingLocationRepo),
20907
- /** Whether `limit` truncated the repo list. */
20908
- hasMore: external_exports.boolean()
20977
+ // Counts in FINDINGS, where the types response counts types, each dimension
20978
+ // still excluding its own filter. Deliberately not locations: counting those
20979
+ // needs a set of location keys per dimension per value — memory tracking the
20980
+ // store times the vocabulary, in a read whose scan promises flat memory —
20981
+ // and the cheap per-location version is not an approximation but WRONG. A
20982
+ // location holding {claudecode, block} and {codex, warn} would survive
20983
+ // provider=claudecode AND action=warn, under which no single finding
20984
+ // matches, so the facet would contradict the instanceCount this whole view
20985
+ // rests on. Findings also keep the toolbar in the same unit as the page
20986
+ // tally and the panel it sits above.
20987
+ facets: FindingFacets,
20988
+ /** Sorted by max severity, then most recent, then (repo, file). */
20989
+ items: external_exports.array(FindingLocationSummary),
20990
+ nextCursor: external_exports.string().nullable()
20909
20991
  }).meta({ id: "ListFindingLocationsResponse" });
20910
20992
 
20911
20993
  // ../../packages/schema/src/zod/meta.ts
@@ -22073,6 +22155,14 @@ var ControlPlaneErrorBody = external_exports.object({
22073
22155
  message: external_exports.string().optional()
22074
22156
  }).optional()
22075
22157
  });
22158
+ var RemoteFailureKind = external_exports.enum([
22159
+ "unauthorized",
22160
+ "forbidden",
22161
+ "route-absent",
22162
+ "invalid-request",
22163
+ "rejected",
22164
+ "unreachable"
22165
+ ]);
22076
22166
  var AttachDeviceRequest = external_exports.object({
22077
22167
  // This machine's own continuity id, so re-attaching ROTATES the credential
22078
22168
  // on one machine record instead of producing a second one. Client-minted
@@ -22134,6 +22224,26 @@ var AttachTokenResponse = external_exports.union([
22134
22224
  AttachTokenExpired,
22135
22225
  external_exports.object({ status: printable(64) })
22136
22226
  ]);
22227
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22228
+ var DeviceCommand = external_exports.object({
22229
+ id: printable(128).min(1),
22230
+ kind: DeviceCommandKind,
22231
+ issuedAt: printable(64).min(1),
22232
+ expiresAt: printable(64).min(1)
22233
+ }).strict();
22234
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22235
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22236
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22237
+ external_exports.object({
22238
+ outcome: external_exports.literal("reported"),
22239
+ projectsScanned: external_exports.number().int().nonnegative()
22240
+ }).strict(),
22241
+ external_exports.object({
22242
+ outcome: external_exports.literal("failed"),
22243
+ reason: DeviceCommandFailureReason,
22244
+ projectsScanned: external_exports.number().int().nonnegative()
22245
+ }).strict()
22246
+ ]);
22137
22247
 
22138
22248
  // ../../packages/schema/src/zod/registry.ts
22139
22249
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22300,7 +22410,7 @@ var PackManifest = external_exports.object({
22300
22410
  }).meta({ id: "PackManifest" });
22301
22411
 
22302
22412
  // ../../packages/schema/src/zod/detection.ts
22303
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22413
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22304
22414
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22305
22415
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22306
22416
  var DetectionCounts = external_exports.object({
@@ -22437,14 +22547,17 @@ function optional2(key, parsed, raw) {
22437
22547
  function isStringArray(value) {
22438
22548
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22439
22549
  }
22550
+ var ORIGIN_VALUES = { library: true, custom: true };
22551
+ function resolveOrigin(origin) {
22552
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22553
+ }
22440
22554
  function summaryToDetectionListItem(s) {
22441
22555
  return {
22442
22556
  id: `${s.namespace}/${s.packId}`,
22443
22557
  name: s.name,
22444
22558
  version: s.version,
22445
22559
  enabled: s.enabled,
22446
- origin: "library",
22447
- // v1: every installed pack is library origin
22560
+ origin: resolveOrigin(s.origin),
22448
22561
  namespace: s.namespace,
22449
22562
  packId: s.packId,
22450
22563
  ruleCount: s.ruleCount,
@@ -22496,7 +22609,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22496
22609
  name: row.name,
22497
22610
  version: row.version,
22498
22611
  enabled: row.enabled,
22499
- origin: "library",
22612
+ origin: resolveOrigin(row.origin),
22500
22613
  namespace: row.namespace,
22501
22614
  packId: row.packId,
22502
22615
  ruleCount: row.rules.length,
@@ -22516,16 +22629,20 @@ function splitDetectionId(id) {
22516
22629
  }
22517
22630
  function buildDetectionsList(summaries, query) {
22518
22631
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22632
+ const originOf = (s) => resolveOrigin(s.origin);
22519
22633
  const counts = {
22520
22634
  all: summaries.length,
22521
- library: summaries.length,
22522
- // all origin=library in v1
22523
- custom: 0,
22635
+ library: summaries.filter((s) => originOf(s) === "library").length,
22636
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22637
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22638
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22639
+ // place, and that state does not exist — editing a library pack forks it. See
22640
+ // OriginEnum.
22524
22641
  customized: 0,
22525
22642
  updates: withUpdate.length
22526
22643
  };
22527
22644
  const filter = query.filter;
22528
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22645
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22529
22646
  if (query.q) {
22530
22647
  const q = query.q.toLowerCase();
22531
22648
  filtered = filtered.filter(
@@ -22605,8 +22722,9 @@ var Event = external_exports.object({
22605
22722
  metadata: EventMetadata.optional()
22606
22723
  }).meta({ id: "Event" });
22607
22724
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22725
+ var INGEST_BATCH_MAX = 100;
22608
22726
  var IngestBatch = external_exports.object({
22609
- events: external_exports.array(IngestEvent).min(1).max(100),
22727
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22610
22728
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22611
22729
  // additionally rejects any event whose contentHash the store has already
22612
22730
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -22738,139 +22856,62 @@ function deriveFindingStatus(row) {
22738
22856
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22739
22857
  return "open";
22740
22858
  }
22741
- function distinctUsers(instances) {
22742
- const seen = /* @__PURE__ */ new Set();
22743
- const users = [];
22744
- for (const i of instances) {
22745
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22746
- seen.add(i.user.id);
22747
- users.push(i.user);
22748
- }
22749
- return users;
22750
- }
22751
22859
  function sortUsers(users) {
22752
22860
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22753
22861
  }
22754
- function buildFindingGroups(rows, opts = {}) {
22755
- const overrides = opts.overrides;
22862
+ function buildFindingTypes(aggregates, opts = {}) {
22756
22863
  const packNames = opts.packNames;
22757
- const aggregates = opts.aggregates;
22758
- const byRuleId = /* @__PURE__ */ new Map();
22759
- for (const row of rows) {
22760
- const existing = byRuleId.get(row.ruleId);
22761
- if (existing) existing.push(row);
22762
- else byRuleId.set(row.ruleId, [row]);
22763
- }
22764
- const groups = [];
22765
- for (const [ruleId, ruleRows] of byRuleId) {
22766
- const instances = ruleRows.map((r) => {
22767
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22768
- return {
22769
- id: r.id,
22770
- provider: toApiProvider(r.sourceTool),
22771
- repo: r.repo,
22772
- file: r.file,
22773
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22774
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22775
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22776
- ...r.user === void 0 ? {} : { user: r.user },
22777
- action: toApiAction(effectiveDbAction),
22778
- detectedAt: r.occurredAt,
22779
- confidence: r.confidence,
22780
- status: r.status
22781
- };
22782
- });
22783
- const agg = aggregates?.get(ruleId);
22784
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22785
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22786
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22787
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22788
- );
22789
- const seenProviders = /* @__PURE__ */ new Set();
22790
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22791
- if (seenProviders.has(p)) return false;
22792
- seenProviders.add(p);
22793
- return true;
22794
- });
22795
- const actionSet = new Set(
22796
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22797
- );
22864
+ const types = [];
22865
+ for (const [ruleId, agg] of aggregates) {
22866
+ const users = sortUsers(agg.users ?? []);
22867
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22868
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22798
22869
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22799
- const severity = ruleRows[0]?.severity ?? "low";
22800
- const detection = {
22801
- id: ruleId,
22802
- name: packNames?.get(ruleId) ?? null
22803
- };
22804
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22805
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22806
- const match = {
22807
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22808
- contextPrefix: ""
22809
- // empty (pending privacy review)
22810
- };
22811
- const status = foldGroupStatus(
22812
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22813
- );
22814
- const group = {
22870
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22871
+ const type = {
22815
22872
  id: ruleId,
22816
22873
  category: apiCategory,
22817
22874
  subtype: ruleId,
22818
22875
  // human label comes with pack metadata later
22819
- severity,
22820
- match,
22821
- detection,
22822
- policy,
22823
- instanceCount: agg?.instanceCount ?? instances.length,
22876
+ severity: agg.severity ?? "low",
22877
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22878
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22879
+ instanceCount: agg.instanceCount,
22824
22880
  providers,
22825
22881
  aggregateAction,
22826
- latestDetectedAt,
22827
- instances,
22828
- status,
22882
+ latestDetectedAt: agg.latestDetectedAt,
22883
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22829
22884
  ...users.length > 0 ? { users } : {}
22830
22885
  };
22831
- if (agg) {
22832
- actionsCache.set(group, [...actionSet]);
22833
- if (agg.searchText !== void 0) {
22834
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22835
- }
22886
+ actionsCache.set(type, [...actionSet]);
22887
+ if (agg.searchText !== void 0) {
22888
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22836
22889
  }
22837
- groups.push(group);
22890
+ types.push(type);
22838
22891
  }
22839
- return groups;
22892
+ return types;
22840
22893
  }
22841
22894
  var haystackCache = /* @__PURE__ */ new WeakMap();
22842
- function buildHaystack(g, extra) {
22895
+ function buildHaystack(t, extra) {
22843
22896
  return [
22844
- g.subtype,
22845
- g.category,
22846
- g.match.maskedValue,
22847
- g.policy.name,
22848
- g.id,
22849
- ...g.instances.map((i) => i.repo),
22850
- ...g.instances.map((i) => i.file),
22851
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22852
- ...g.instances.map((i) => i.id),
22853
- // The people: the whole group's list when the store folded one, plus the
22854
- // preview's own — the two overlap, and a haystack does not mind.
22855
- ...(g.users ?? []).map((u) => u.name),
22856
- ...g.instances.map((i) => i.user?.name ?? ""),
22897
+ t.subtype,
22898
+ t.category,
22899
+ t.policy.name,
22900
+ t.id,
22901
+ ...(t.users ?? []).map((u) => u.name),
22857
22902
  ...extra === void 0 ? [] : [extra]
22858
22903
  ].join(" ").toLowerCase();
22859
22904
  }
22860
- function groupHaystack(g) {
22861
- const cached2 = haystackCache.get(g);
22905
+ function typeHaystack(t) {
22906
+ const cached2 = haystackCache.get(t);
22862
22907
  if (cached2 !== void 0) return cached2;
22863
- const haystack = buildHaystack(g);
22864
- haystackCache.set(g, haystack);
22908
+ const haystack = buildHaystack(t);
22909
+ haystackCache.set(t, haystack);
22865
22910
  return haystack;
22866
22911
  }
22867
22912
  var actionsCache = /* @__PURE__ */ new WeakMap();
22868
- function groupActions(g) {
22869
- const cached2 = actionsCache.get(g);
22870
- if (cached2 !== void 0) return cached2;
22871
- const actions = [...new Set(g.instances.map((i) => i.action))];
22872
- actionsCache.set(g, actions);
22873
- return actions;
22913
+ function typeActions(t) {
22914
+ return actionsCache.get(t) ?? [];
22874
22915
  }
22875
22916
  function countInstancesByStatus(statusInputs, statuses) {
22876
22917
  const statusSet = new Set(statuses);
@@ -22881,8 +22922,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22881
22922
  }
22882
22923
  return sum;
22883
22924
  }
22884
- function applyFindingFilters(groups, opts) {
22885
- let filtered = groups;
22925
+ function applyFindingFilters(types, opts) {
22926
+ let filtered = types;
22886
22927
  if (opts.severity && opts.severity.length > 0) {
22887
22928
  const sevSet = new Set(opts.severity);
22888
22929
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22893,7 +22934,7 @@ function applyFindingFilters(groups, opts) {
22893
22934
  }
22894
22935
  if (opts.actions && opts.actions.length > 0) {
22895
22936
  const actionSet = new Set(opts.actions);
22896
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
22937
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22897
22938
  }
22898
22939
  if (opts.subtype && opts.subtype.length > 0) {
22899
22940
  const subtypeSet = new Set(opts.subtype);
@@ -22905,7 +22946,7 @@ function applyFindingFilters(groups, opts) {
22905
22946
  }
22906
22947
  if (opts.q) {
22907
22948
  const q = opts.q.toLowerCase();
22908
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
22949
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22909
22950
  }
22910
22951
  return filtered;
22911
22952
  }
@@ -22920,11 +22961,11 @@ function compareFindingGroupOrder(a, b) {
22920
22961
  if (recencyDiff !== 0) return recencyDiff;
22921
22962
  return a.id.localeCompare(b.id);
22922
22963
  }
22923
- function sortFindingGroups(groups) {
22924
- return [...groups].sort(compareFindingGroupOrder);
22964
+ function sortFindingTypes(types) {
22965
+ return [...types].sort(compareFindingGroupOrder);
22925
22966
  }
22926
- function computeFindingFacets(allGroups, opts) {
22927
- const forSeverity = applyFindingFilters(allGroups, {
22967
+ function computeFindingFacets(allTypes, opts) {
22968
+ const forSeverity = applyFindingFilters(allTypes, {
22928
22969
  providers: opts.providers,
22929
22970
  actions: opts.actions,
22930
22971
  statuses: opts.statuses,
@@ -22935,7 +22976,7 @@ function computeFindingFacets(allGroups, opts) {
22935
22976
  for (const g of forSeverity) {
22936
22977
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22937
22978
  }
22938
- const forProvider = applyFindingFilters(allGroups, {
22979
+ const forProvider = applyFindingFilters(allTypes, {
22939
22980
  actions: opts.actions,
22940
22981
  statuses: opts.statuses,
22941
22982
  q: opts.q,
@@ -22946,7 +22987,7 @@ function computeFindingFacets(allGroups, opts) {
22946
22987
  for (const g of forProvider) {
22947
22988
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
22948
22989
  }
22949
- const forAction = applyFindingFilters(allGroups, {
22990
+ const forAction = applyFindingFilters(allTypes, {
22950
22991
  providers: opts.providers,
22951
22992
  statuses: opts.statuses,
22952
22993
  q: opts.q,
@@ -22955,9 +22996,9 @@ function computeFindingFacets(allGroups, opts) {
22955
22996
  });
22956
22997
  const actionMap = /* @__PURE__ */ new Map();
22957
22998
  for (const g of forAction) {
22958
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
22999
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
22959
23000
  }
22960
- const forSubtype = applyFindingFilters(allGroups, {
23001
+ const forSubtype = applyFindingFilters(allTypes, {
22961
23002
  providers: opts.providers,
22962
23003
  actions: opts.actions,
22963
23004
  statuses: opts.statuses,
@@ -22966,7 +23007,7 @@ function computeFindingFacets(allGroups, opts) {
22966
23007
  });
22967
23008
  const subtypeMap = /* @__PURE__ */ new Map();
22968
23009
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
22969
- const forStatus = applyFindingFilters(allGroups, {
23010
+ const forStatus = applyFindingFilters(allTypes, {
22970
23011
  providers: opts.providers,
22971
23012
  actions: opts.actions,
22972
23013
  q: opts.q,
@@ -23014,10 +23055,20 @@ function matchesDimension(row, opts, dimension) {
23014
23055
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23015
23056
  case "tools":
23016
23057
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23058
+ // An EMPTY value is a real filter here, not an absent one. The location
23059
+ // list buckets a finding whose event recorded no repo — or no file — under
23060
+ // the empty string, and selecting that bucket has to narrow the panel to
23061
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23062
+ // row omits the key, which every call site already does.
23063
+ //
23064
+ // Reading '' as unset is what this replaced, and it failed in the one place
23065
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23066
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23067
+ // — a row reading 3 findings beside a panel listing every finding there is.
23017
23068
  case "repo":
23018
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23069
+ return opts.repo === void 0 || row.repo === opts.repo;
23019
23070
  case "file":
23020
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23071
+ return opts.file === void 0 || row.file === opts.file;
23021
23072
  case "q":
23022
23073
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23023
23074
  }
@@ -23131,6 +23182,23 @@ function addToLocation(acc, row) {
23131
23182
  acc.statuses.push(row.status);
23132
23183
  acc.ruleIds.add(row.ruleId);
23133
23184
  }
23185
+ function compareLocationOrder(a, b) {
23186
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23187
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23188
+ if (rankA !== rankB) return rankA - rankB;
23189
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23190
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23191
+ }
23192
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23193
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23194
+ return 0;
23195
+ }
23196
+ function encodeLocationId(repo, file2) {
23197
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23198
+ }
23199
+ function encodePart(value) {
23200
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23201
+ }
23134
23202
 
23135
23203
  // ../../packages/schema/src/zod/installed-pack.ts
23136
23204
  var InstalledPack = external_exports.object({
@@ -23162,6 +23230,252 @@ var PatchInstalledPackRequest = external_exports.object({
23162
23230
  message: "At least one field must be provided"
23163
23231
  }).meta({ id: "PatchInstalledPackRequest" });
23164
23232
 
23233
+ // ../../packages/schema/src/zod/policy.ts
23234
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23235
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23236
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23237
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23238
+ var Policy = external_exports.object({
23239
+ id: external_exports.guid(),
23240
+ scope: PolicyScope,
23241
+ target: PolicyTarget,
23242
+ action: ActionTaken,
23243
+ enabled: external_exports.boolean().default(true),
23244
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23245
+ // Display name — optional so older policy rows without name still parse.
23246
+ // Added for the findings API (policy.name column migration).
23247
+ name: external_exports.string().optional(),
23248
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23249
+ // which row this is. A producer that collapses several rows onto one target
23250
+ // must carry the marker onto whichever row survives, or the collapse decides
23251
+ // the answer; a survivor may therefore be a built-in expansion still marked
23252
+ // 'authored' because an authored sibling targeted the same thing.
23253
+ // Optional so an older producer — and an older on-disk cache — still parses;
23254
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23255
+ //
23256
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23257
+ // built-in archetype catalog entry a policy is, which every catalog surface
23258
+ // reads and which a caller may state. This one is a statement the PRODUCER
23259
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23260
+ // — the CRUD routes neither accept nor set it.
23261
+ //
23262
+ // A device consumes this in exactly one direction: an 'authored' policy
23263
+ // arriving from a control plane marks the rules it targets as not
23264
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23265
+ // which is what makes it safe to honour from an unsigned cache — the same
23266
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23267
+ provenance: PolicyProvenance.optional()
23268
+ }).meta({ id: "Policy" });
23269
+ var PolicyBundle = external_exports.object({
23270
+ version: external_exports.string(),
23271
+ policies: external_exports.array(Policy),
23272
+ // Rules from the installed marketplace packs (snapshotted by the
23273
+ // control plane). The plugin registers these in addition to its bundled
23274
+ // packs. Optional so older backends — and older on-disk caches — that omit
23275
+ // the field still parse; consumers read `bundle.rules ?? []`.
23276
+ rules: external_exports.array(Rule).optional(),
23277
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23278
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23279
+ // after reading the user's installed snapshot (installed_packs, enabled
23280
+ // packs only), which is how detection updates stay manual: new bundled
23281
+ // rules run only after the user applies the pack update. Absent/false keeps
23282
+ // the historical composition (bundled packs + rules) — older caches.
23283
+ rulesComplete: external_exports.boolean().optional(),
23284
+ // Active detection exceptions, evaluation subset only (see
23285
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23286
+ // on-disk caches — that omit the field still parse; consumers read
23287
+ // `bundle.exceptions ?? []`.
23288
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23289
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23290
+ // A second axis over the same `redact` action, carried beside the policies
23291
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23292
+ // widening Policy itself would change a persisted shape to express something
23293
+ // only the in-memory bundle needs. Optional so an older producer — or an
23294
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23295
+ // pre-existing one-way behaviour, which is the safe direction to default.
23296
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23297
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23298
+ // from a versioned installed pack. Optional so older backends — and older
23299
+ // on-disk caches — that omit the field still parse; consumers fall back to
23300
+ // the rule's own spec version. NOT the bundle version above — see
23301
+ // installedRuleset's ruleVersions for the source of truth.
23302
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23303
+ // Model ids (the raw `model` string a harness reports, e.g.
23304
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23305
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23306
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23307
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23308
+ // unenforced behaviour that predates this field and the safe direction to
23309
+ // default.
23310
+ //
23311
+ // Ids, not display names: the governance decision is keyed on the exact
23312
+ // string the harness reports (`model_status_override.versionId` in the
23313
+ // control plane), so no name resolution stands between the decision and the
23314
+ // comparison.
23315
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23316
+ customKeywords: external_exports.array(external_exports.string()),
23317
+ fetchedAt: external_exports.iso.datetime()
23318
+ }).meta({ id: "PolicyBundle" });
23319
+ var POLICY_BUNDLE_SHAPE_ID = [
23320
+ ...Object.keys(PolicyBundle.shape),
23321
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23322
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23323
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23324
+ ].sort().join(",");
23325
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23326
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23327
+ var CATEGORY_PEAK_SEVERITY = {
23328
+ secret: "critical",
23329
+ financial: "critical",
23330
+ // core-financial/credit-card
23331
+ code_flaw: "critical",
23332
+ pii: "high",
23333
+ phi: "high",
23334
+ custom: "high",
23335
+ // user-defined; conservative
23336
+ code_context: "low",
23337
+ config: "low"
23338
+ // observe-only; floors to monitor regardless
23339
+ };
23340
+ function severityFloorPolicy(category) {
23341
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23342
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23343
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23344
+ }
23345
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23346
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23347
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23348
+ id: "RedactFallback"
23349
+ });
23350
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23351
+ var BUILTIN_POLICY_SPECS = {
23352
+ monitor: {
23353
+ name: "Monitor",
23354
+ action: "log",
23355
+ reversible: false,
23356
+ description: "Log every match for audit. The request is allowed through untouched."
23357
+ },
23358
+ warn: {
23359
+ name: "Warn",
23360
+ action: "warn",
23361
+ reversible: false,
23362
+ description: "Allow the request, but warn the user inline before it is sent."
23363
+ },
23364
+ redact: {
23365
+ name: "Redact",
23366
+ action: "redact",
23367
+ reversible: false,
23368
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23369
+ },
23370
+ vault: {
23371
+ name: "Redact & Vault",
23372
+ action: "redact",
23373
+ reversible: true,
23374
+ 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."
23375
+ },
23376
+ block: {
23377
+ name: "Block",
23378
+ action: "block",
23379
+ reversible: false,
23380
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23381
+ }
23382
+ };
23383
+ function builtinPolicyToAction(id) {
23384
+ return BUILTIN_POLICY_SPECS[id].action;
23385
+ }
23386
+ var PALETTE_WEAKEST_FIRST = [
23387
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23388
+ ];
23389
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23390
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23391
+ );
23392
+ var ACTION_STRENGTH_ORDER = [
23393
+ ...BELOW_PALETTE,
23394
+ ...PALETTE_WEAKEST_FIRST
23395
+ ];
23396
+ function actionRank(action) {
23397
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23398
+ }
23399
+ function isActionAtLeast(action, floor) {
23400
+ return actionRank(action) >= actionRank(floor);
23401
+ }
23402
+ function strongerAction(a, b) {
23403
+ return actionRank(a) >= actionRank(b) ? a : b;
23404
+ }
23405
+ function weakestBuiltinAtLeast(floor) {
23406
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23407
+ }
23408
+ var PackPolicyFloor = external_exports.object({
23409
+ /**
23410
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23411
+ * rather than a raw ActionTaken because that is the vocabulary the user
23412
+ * picks from — a floor a UI cannot name is one it cannot explain.
23413
+ */
23414
+ floor: BuiltinPolicyId,
23415
+ /**
23416
+ * True when the organization AUTHORED a policy governing this pack rather
23417
+ * than stating a minimum: it gave the answer, so the pack is not
23418
+ * re-assignable locally in either direction.
23419
+ */
23420
+ locked: external_exports.boolean()
23421
+ }).describe("PackPolicyFloor");
23422
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23423
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23424
+ );
23425
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23426
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23427
+ );
23428
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23429
+ function builtinPolicyIsReversible(id) {
23430
+ return BUILTIN_POLICY_SPECS[id].reversible;
23431
+ }
23432
+ function policyIdIsReversible(policyId) {
23433
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23434
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23435
+ return builtinPolicyIsReversible(id);
23436
+ }
23437
+ var DEFAULT_ACTIONS = Object.fromEntries(
23438
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23439
+ );
23440
+ var BUILTIN_POLICIES = Object.fromEntries(
23441
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23442
+ );
23443
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23444
+ function policyIdToAction(policyId) {
23445
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23446
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23447
+ return BUILTIN_POLICIES[id].action;
23448
+ }
23449
+ var UsedByItem = external_exports.object({
23450
+ id: external_exports.string(),
23451
+ name: external_exports.string(),
23452
+ ruleCount: external_exports.number().int().nonnegative(),
23453
+ enabled: external_exports.boolean()
23454
+ }).meta({ id: "UsedByItem" });
23455
+ var PolicyListItem = external_exports.object({
23456
+ id: external_exports.string(),
23457
+ kind: PolicyKind,
23458
+ name: external_exports.string(),
23459
+ enabled: external_exports.boolean(),
23460
+ usedByCount: external_exports.number().int().nonnegative()
23461
+ }).meta({ id: "PolicyListItem" });
23462
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23463
+ var PolicyDetail = external_exports.object({
23464
+ specVersion: external_exports.literal(1),
23465
+ id: external_exports.string(),
23466
+ kind: PolicyKind,
23467
+ name: external_exports.string(),
23468
+ enabled: external_exports.boolean(),
23469
+ description: external_exports.string(),
23470
+ usedBy: external_exports.array(UsedByItem)
23471
+ }).meta({ id: "PolicyDetail" });
23472
+ var PolicyStatsResponse = external_exports.object({
23473
+ policies: external_exports.number().int().nonnegative(),
23474
+ builtin: external_exports.number().int().nonnegative(),
23475
+ custom: external_exports.number().int().nonnegative(),
23476
+ detectionsGoverned: external_exports.number().int().nonnegative()
23477
+ }).meta({ id: "PolicyStatsResponse" });
23478
+
23165
23479
  // ../../packages/schema/src/zod/vault.ts
23166
23480
  var POINTER_FORMAT_VERSION = 2;
23167
23481
  var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
@@ -23202,6 +23516,14 @@ var VaultEntry = external_exports.object({
23202
23516
  // How many times this value has been detected on this machine — the reuse
23203
23517
  // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23204
23518
  occurrenceCount: external_exports.number().int().nonnegative(),
23519
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23520
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23521
+ // one row however many paths vault it, so this is what tells a policy sweep
23522
+ // that the row carries somebody's own instruction and not just an assignment
23523
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23524
+ // vaulting of the same value must never clear it — what the user said about
23525
+ // the value does not expire.
23526
+ userAuthorized: external_exports.boolean(),
23205
23527
  firstSeen: external_exports.string(),
23206
23528
  lastSeen: external_exports.string()
23207
23529
  });
@@ -23328,7 +23650,7 @@ function isVaultConsentValid(consent) {
23328
23650
  }
23329
23651
 
23330
23652
  // ../../packages/schema/src/zod/local.ts
23331
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23653
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23332
23654
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23333
23655
  var RunMode = external_exports.enum(["standalone", "attached"]);
23334
23656
  var ControlPlaneConnection = external_exports.object({
@@ -23370,6 +23692,19 @@ var WorkspaceSettings = external_exports.object({
23370
23692
  vaultKeyCustody: VaultKeyCustody.default("file"),
23371
23693
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23372
23694
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23695
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23696
+ // place. Not a handling policy: the policy has already resolved to redact,
23697
+ // and this only says what happens when the host offers no channel to carry it
23698
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23699
+ // Claude Code decline to mask a field that EXECUTES because masking would
23700
+ // change what runs. Per FIELD rather than per host, so a host that can
23701
+ // rewrite some inputs keeps true redaction on those.
23702
+ //
23703
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23704
+ // an attached machine's merge is `strongerAction` over the one action ladder
23705
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23706
+ // word and stays out of the stored value.
23707
+ redactFallback: RedactFallback.default("warn"),
23373
23708
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23374
23709
  onboardedAt: external_exports.iso.datetime().optional(),
23375
23710
  // Records that the user consented to sending findings to the model API for
@@ -23377,15 +23712,20 @@ var WorkspaceSettings = external_exports.object({
23377
23712
  // Absent until granted; a stale payloadVersion means the consent no longer
23378
23713
  // covers the current payload and must be re-granted.
23379
23714
  modelJudgeConsent: ModelJudgeConsent.optional(),
23380
- // Records that the user consented to sending the activity already recorded on
23381
- // this machine to the deployment it is attached to, along with the payload
23382
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23383
- // a different endpoint or an older payload no longer counts.
23715
+ // Records that the user consented to the DEFERRED send — the outbox — along
23716
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23717
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23718
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23719
+ // both widenings. Absent until granted, and a grant for a different endpoint
23720
+ // or an older payload no longer counts.
23384
23721
  historySyncConsent: HistorySyncConsent.optional()
23385
23722
  });
23386
23723
  function defaultWorkspaceSettings() {
23387
23724
  return WorkspaceSettings.parse({});
23388
23725
  }
23726
+ function isAttached(settings) {
23727
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23728
+ }
23389
23729
  function toInventoryRow(input2, id, now) {
23390
23730
  return {
23391
23731
  id,
@@ -23505,8 +23845,12 @@ var ManagedSettingKey = external_exports.enum([
23505
23845
  "vaultKeyCustody",
23506
23846
  "vaultInlineReveal",
23507
23847
  "modelJudgeConsent",
23508
- "dataSharesInPlace"
23848
+ "dataSharesInPlace",
23849
+ "redactFallback"
23509
23850
  ]).meta({ id: "ManagedSettingKey" });
23851
+ function isManagedSettingKey(value) {
23852
+ return ManagedSettingKey.safeParse(value).success;
23853
+ }
23510
23854
  var ManagedSettingsValues = external_exports.object({
23511
23855
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23512
23856
  controlPlane: external_exports.object({
@@ -23518,7 +23862,8 @@ var ManagedSettingsValues = external_exports.object({
23518
23862
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23519
23863
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23520
23864
  modelJudgeConsent: external_exports.boolean().optional(),
23521
- dataSharesInPlace: external_exports.boolean().optional()
23865
+ dataSharesInPlace: external_exports.boolean().optional(),
23866
+ redactFallback: RedactFallback.optional()
23522
23867
  }).meta({ id: "ManagedSettingsValues" });
23523
23868
  var ManagedSettings = external_exports.object({
23524
23869
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23530,188 +23875,28 @@ var ManagedSettings = external_exports.object({
23530
23875
  // Which of those the user may not change. A key here with no matching value
23531
23876
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23532
23877
  // the user may still override. The two are separable on purpose.
23533
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23534
- }).meta({ id: "ManagedSettings" });
23535
-
23536
- // ../../packages/schema/src/zod/policy.ts
23537
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23538
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23539
- var Policy = external_exports.object({
23540
- id: external_exports.guid(),
23541
- scope: PolicyScope,
23542
- target: PolicyTarget,
23543
- action: ActionTaken,
23544
- enabled: external_exports.boolean().default(true),
23545
- customKeywords: external_exports.array(external_exports.string()).optional(),
23546
- // Display name — optional so older policy rows without name still parse.
23547
- // Added for the findings API (policy.name column migration).
23548
- name: external_exports.string().optional()
23549
- }).meta({ id: "Policy" });
23550
- var PolicyBundle = external_exports.object({
23551
- version: external_exports.string(),
23552
- policies: external_exports.array(Policy),
23553
- // Rules from the installed marketplace packs (snapshotted by the
23554
- // control plane). The plugin registers these in addition to its bundled
23555
- // packs. Optional so older backends — and older on-disk caches — that omit
23556
- // the field still parse; consumers read `bundle.rules ?? []`.
23557
- rules: external_exports.array(Rule).optional(),
23558
- // When true, `rules` IS the complete effective ruleset and the runtime must
23559
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23560
- // after reading the user's installed snapshot (installed_packs, enabled
23561
- // packs only), which is how detection updates stay manual: new bundled
23562
- // rules run only after the user applies the pack update. Absent/false keeps
23563
- // the historical composition (bundled packs + rules) — older caches.
23564
- rulesComplete: external_exports.boolean().optional(),
23565
- // Active detection exceptions, evaluation subset only (see
23566
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23567
- // on-disk caches — that omit the field still parse; consumers read
23568
- // `bundle.exceptions ?? []`.
23569
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23570
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23571
- // A second axis over the same `redact` action, carried beside the policies
23572
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23573
- // widening Policy itself would change a persisted shape to express something
23574
- // only the in-memory bundle needs. Optional so an older producer — or an
23575
- // older on-disk cache — still parses; consumers read `?? []` and get the
23576
- // pre-existing one-way behaviour, which is the safe direction to default.
23577
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23578
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23579
- // from a versioned installed pack. Optional so older backends — and older
23580
- // on-disk caches — that omit the field still parse; consumers fall back to
23581
- // the rule's own spec version. NOT the bundle version above — see
23582
- // installedRuleset's ruleVersions for the source of truth.
23583
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23584
- // Model ids (the raw `model` string a harness reports, e.g.
23585
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23586
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23587
- // one (UserPromptSubmit). Optional so an older backend — and an older
23588
- // on-disk cache — still parses; consumers read `?? []`, which is the
23589
- // unenforced behaviour that predates this field and the safe direction to
23590
- // default.
23591
23878
  //
23592
- // Ids, not display names: the governance decision is keyed on the exact
23593
- // string the harness reports (`model_status_override.versionId` in the
23594
- // control plane), so no name resolution stands between the decision and the
23595
- // comparison.
23596
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23597
- customKeywords: external_exports.array(external_exports.string()),
23598
- fetchedAt: external_exports.iso.datetime()
23599
- }).meta({ id: "PolicyBundle" });
23600
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23601
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23602
- var CATEGORY_PEAK_SEVERITY = {
23603
- secret: "critical",
23604
- financial: "critical",
23605
- // core-financial/credit-card
23606
- code_flaw: "critical",
23607
- pii: "high",
23608
- phi: "high",
23609
- custom: "high",
23610
- // user-defined; conservative
23611
- code_context: "low",
23612
- config: "low"
23613
- // observe-only; floors to monitor regardless
23614
- };
23615
- function severityFloorPolicy(category) {
23616
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23617
- const peak = CATEGORY_PEAK_SEVERITY[category];
23618
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23619
- }
23620
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23621
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23622
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23623
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23624
- var BUILTIN_POLICY_SPECS = {
23625
- monitor: {
23626
- name: "Monitor",
23627
- action: "log",
23628
- reversible: false,
23629
- description: "Log every match for audit. The request is allowed through untouched."
23630
- },
23631
- warn: {
23632
- name: "Warn",
23633
- action: "warn",
23634
- reversible: false,
23635
- description: "Allow the request, but warn the user inline before it is sent."
23636
- },
23637
- redact: {
23638
- name: "Redact",
23639
- action: "redact",
23640
- reversible: false,
23641
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23642
- },
23643
- vault: {
23644
- name: "Redact & Vault",
23645
- action: "redact",
23646
- reversible: true,
23647
- 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."
23648
- },
23649
- block: {
23650
- name: "Block",
23651
- action: "block",
23652
- reversible: false,
23653
- description: "Refuse the request entirely whenever any rule in this detection matches."
23879
+ // Parsed as NAMES rather than as the enum, and split below: a name this
23880
+ // build does not know is dropped from the locked set and reported, never a
23881
+ // reason to refuse the file. The same shape reaches an older build whenever
23882
+ // an administrator locks a key a newer build added, and refusing it there
23883
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
23884
+ // the fleets most likely to carry a version skew. A name outside the enum
23885
+ // is still never HONOURED: the lockable set stays explicit above.
23886
+ lockedFields: external_exports.array(external_exports.string()).default([])
23887
+ }).transform(({ lockedFields, ...rest }) => {
23888
+ const known = [];
23889
+ const unknown2 = [];
23890
+ for (const name of lockedFields) {
23891
+ if (isManagedSettingKey(name)) known.push(name);
23892
+ else unknown2.push(name);
23654
23893
  }
23655
- };
23656
- function builtinPolicyToAction(id) {
23657
- return BUILTIN_POLICY_SPECS[id].action;
23658
- }
23659
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23660
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23661
- );
23662
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23663
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23664
- );
23665
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23666
- function builtinPolicyIsReversible(id) {
23667
- return BUILTIN_POLICY_SPECS[id].reversible;
23668
- }
23669
- function policyIdIsReversible(policyId) {
23670
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23671
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23672
- return builtinPolicyIsReversible(id);
23673
- }
23674
- var DEFAULT_ACTIONS = Object.fromEntries(
23675
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23676
- );
23677
- var BUILTIN_POLICIES = Object.fromEntries(
23678
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23679
- );
23680
- var DEFAULT_PACK_POLICY_ID = "monitor";
23681
- function policyIdToAction(policyId) {
23682
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23683
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23684
- return BUILTIN_POLICIES[id].action;
23685
- }
23686
- var UsedByItem = external_exports.object({
23687
- id: external_exports.string(),
23688
- name: external_exports.string(),
23689
- ruleCount: external_exports.number().int().nonnegative(),
23690
- enabled: external_exports.boolean()
23691
- }).meta({ id: "UsedByItem" });
23692
- var PolicyListItem = external_exports.object({
23693
- id: external_exports.string(),
23694
- kind: PolicyKind,
23695
- name: external_exports.string(),
23696
- enabled: external_exports.boolean(),
23697
- usedByCount: external_exports.number().int().nonnegative()
23698
- }).meta({ id: "PolicyListItem" });
23699
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23700
- var PolicyDetail = external_exports.object({
23701
- specVersion: external_exports.literal(1),
23702
- id: external_exports.string(),
23703
- kind: PolicyKind,
23704
- name: external_exports.string(),
23705
- enabled: external_exports.boolean(),
23706
- description: external_exports.string(),
23707
- usedBy: external_exports.array(UsedByItem)
23708
- }).meta({ id: "PolicyDetail" });
23709
- var PolicyStatsResponse = external_exports.object({
23710
- policies: external_exports.number().int().nonnegative(),
23711
- builtin: external_exports.number().int().nonnegative(),
23712
- custom: external_exports.number().int().nonnegative(),
23713
- detectionsGoverned: external_exports.number().int().nonnegative()
23714
- }).meta({ id: "PolicyStatsResponse" });
23894
+ return {
23895
+ ...rest,
23896
+ lockedFields: known,
23897
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
23898
+ };
23899
+ }).meta({ id: "ManagedSettings" });
23715
23900
 
23716
23901
  // ../../packages/schema/src/zod/project-files.ts
23717
23902
  var ProjectFileInput = external_exports.object({
@@ -23834,7 +24019,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23834
24019
  timestamp: external_exports.iso.date(),
23835
24020
  critical: external_exports.number().int().nonnegative(),
23836
24021
  high: external_exports.number().int().nonnegative(),
23837
- medium: external_exports.number().int().nonnegative()
24022
+ medium: external_exports.number().int().nonnegative(),
24023
+ // Optional and additive, so a producer written against the earlier
24024
+ // three-series contract keeps validating. A consumer plotting it resolves the
24025
+ // absent case itself — the chart point requires a number.
24026
+ low: external_exports.number().int().nonnegative().optional()
23838
24027
  }).meta({ id: "FindingsTimeseriesPoint" });
23839
24028
  var FindingsTimeseriesResponse = external_exports.object({
23840
24029
  range: TimeRange,
@@ -23860,6 +24049,10 @@ var ResolvedFeedItem = external_exports.object({
23860
24049
  findingKey: external_exports.string(),
23861
24050
  ruleId: external_exports.string(),
23862
24051
  severity: Severity,
24052
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24053
+ // identifies the file: a bare path matches the same name in every repo.
24054
+ // Optional and additive; empty when the event carried no repo.
24055
+ repo: external_exports.string().optional(),
23863
24056
  path: external_exports.string(),
23864
24057
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
23865
24058
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -23958,10 +24151,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23958
24151
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23959
24152
 
23960
24153
  // ../../packages/schema/src/zod/settings-action.ts
24154
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24155
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23961
24156
  var SaveSettingsInput = external_exports.object({
23962
24157
  historicalAccess: external_exports.string(),
23963
- modelJudgeConsent: external_exports.boolean(),
23964
- historySyncConsent: external_exports.boolean(),
24158
+ modelJudgeConsent: ModelJudgeConsentChoice,
24159
+ historySyncConsent: HistorySyncConsentChoice,
23965
24160
  vaultConsent: external_exports.string(),
23966
24161
  vaultInlineReveal: external_exports.string()
23967
24162
  });
@@ -24111,9 +24306,9 @@ function deriveReviewReasons(trust, transports) {
24111
24306
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24112
24307
  return reasons;
24113
24308
  }
24114
- function buildReviewInfo(trust, transports) {
24309
+ function buildReviewInfo(trust, transports, decided) {
24115
24310
  const reasons = deriveReviewReasons(trust, transports);
24116
- return { needsReview: reasons.length > 0, reasons };
24311
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24117
24312
  }
24118
24313
  function distinctTransports(transports) {
24119
24314
  return Array.from(new Set(transports));
@@ -24261,8 +24456,8 @@ function publishByLink(tmp, file2, data) {
24261
24456
  }
24262
24457
 
24263
24458
  // ../../packages/persistence/src/database.ts
24264
- import { randomUUID as randomUUID10 } from "crypto";
24265
- import { join as join4, sep } from "path";
24459
+ import { randomUUID as randomUUID11 } from "crypto";
24460
+ import { dirname as dirname2, join as join7, sep } from "path";
24266
24461
  import { DatabaseSync } from "node:sqlite";
24267
24462
 
24268
24463
  // ../../packages/persistence/src/ids.ts
@@ -24506,6 +24701,10 @@ function allRows(stmt, params) {
24506
24701
  if (Array.isArray(params)) return stmt.all(...params);
24507
24702
  return stmt.all(params);
24508
24703
  }
24704
+ function* iterateRows(stmt, params) {
24705
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24706
+ for (const row of rows) yield row;
24707
+ }
24509
24708
  function getRow(stmt, params) {
24510
24709
  if (params === void 0) return stmt.get();
24511
24710
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24974,10 +25173,17 @@ function ensureSyncedAtColumn(db, table) {
24974
25173
  if (!columns.includes("sync_claimed_at")) {
24975
25174
  db.exec(`ALTER TABLE ${table} ADD COLUMN sync_claimed_at integer`);
24976
25175
  }
25176
+ if (!columns.includes("outbox_owed")) {
25177
+ db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25178
+ }
24977
25179
  db.exec(
24978
25180
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24979
25181
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24980
25182
  );
25183
+ db.exec(
25184
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25185
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25186
+ );
24981
25187
  db.exec(
24982
25188
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24983
25189
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25082,7 +25288,6 @@ function decodeKeysetCursor(cursor) {
25082
25288
  // ../../packages/persistence/src/repositories/activity.ts
25083
25289
  var DAY_MS = 864e5;
25084
25290
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25085
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25086
25291
  function defaultTimeZone() {
25087
25292
  try {
25088
25293
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25137,6 +25342,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25137
25342
  error: "error",
25138
25343
  active: "active"
25139
25344
  };
25345
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25140
25346
  function safeParseStringArray(raw) {
25141
25347
  if (!raw) return [];
25142
25348
  const parsed = safeJson(raw, null);
@@ -25210,6 +25416,37 @@ var TIMELINE_COLUMNS = `
25210
25416
  json_extract(attributes, '$.targetId') AS target_id,
25211
25417
  json_extract(attributes, '$.internal') AS internal,
25212
25418
  json_extract(attributes, '$.flagged') AS flagged`;
25419
+ var LLM_USAGE_SELECT = `
25420
+ SELECT root_session_id AS sessionId,
25421
+ provider,
25422
+ model,
25423
+ service_tier AS serviceTier,
25424
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25425
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25426
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25427
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25428
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25429
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25430
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25431
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25432
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25433
+ function usageLeaves(rows) {
25434
+ return rows.map((row) => {
25435
+ const attributes = {
25436
+ input_tokens: row.inputTokens,
25437
+ output_tokens: row.outputTokens,
25438
+ cache_creation_input_tokens: row.cacheCreationTokens,
25439
+ cache_read_input_tokens: row.cacheReadTokens,
25440
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25441
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25442
+ web_search_requests: row.webSearchRequests
25443
+ };
25444
+ if (row.provider !== null) attributes.provider = row.provider;
25445
+ if (row.model !== null) attributes.model = row.model;
25446
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25447
+ return { sessionId: row.sessionId, attributes };
25448
+ });
25449
+ }
25213
25450
  var SESSION_ROOT = `event_type = 'session'`;
25214
25451
  var HAS_ACTIVITY = `EXISTS (
25215
25452
  SELECT 1 FROM audit_events c
@@ -25235,16 +25472,17 @@ var SqliteActivityRepository = class {
25235
25472
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25236
25473
  const liveNow = countScalar(
25237
25474
  this.db,
25238
- `SELECT count(*) AS n FROM audit_events s
25475
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25239
25476
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25240
- AND max(
25241
- s.started_at,
25242
- coalesce(
25243
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25244
- s.started_at
25245
- )
25246
- ) >= ?`,
25247
- [liveThreshold]
25477
+ AND s.id IN (
25478
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25479
+ UNION
25480
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25481
+ WHERE started_at >= ?
25482
+ UNION
25483
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25484
+ WHERE ended_at >= ?)`,
25485
+ [liveThreshold, liveThreshold, liveThreshold]
25248
25486
  );
25249
25487
  const toolCallsToday = countScalar(
25250
25488
  this.db,
@@ -25297,7 +25535,8 @@ var SqliteActivityRepository = class {
25297
25535
  SELECT 1 FROM audit_events d
25298
25536
  WHERE d.root_session_id = audit_events.id
25299
25537
  AND (d.content LIKE ? ESCAPE '\\'
25300
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25538
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25539
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25301
25540
  );
25302
25541
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25303
25542
  }
@@ -25374,7 +25613,7 @@ var SqliteActivityRepository = class {
25374
25613
  this.db.prepare(
25375
25614
  `SELECT ${TIMELINE_COLUMNS}
25376
25615
  FROM audit_events
25377
- WHERE id = ? OR root_session_id = ?
25616
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25378
25617
  ORDER BY started_at ASC, id ASC`
25379
25618
  ),
25380
25619
  [sessionId, sessionId]
@@ -25387,14 +25626,14 @@ var SqliteActivityRepository = class {
25387
25626
  coalesce(sum(output_tokens), 0) AS output,
25388
25627
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25389
25628
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25390
- FROM audit_events
25629
+ FROM audit_events INDEXED BY idx_audit_session_type
25391
25630
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25392
25631
  ),
25393
25632
  [sessionId]
25394
25633
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25395
25634
  const primaryModel = getRow(
25396
25635
  this.db.prepare(
25397
- `SELECT model, provider FROM audit_events
25636
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25398
25637
  WHERE root_session_id = ? AND event_type = 'llm_call'
25399
25638
  ORDER BY started_at ASC, id ASC
25400
25639
  LIMIT 1`
@@ -25405,7 +25644,7 @@ var SqliteActivityRepository = class {
25405
25644
  this.db.prepare(
25406
25645
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25407
25646
  count(*) AS n
25408
- FROM audit_events
25647
+ FROM audit_events INDEXED BY idx_audit_session
25409
25648
  WHERE root_session_id = ? AND event_type = 'tool_call'
25410
25649
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25411
25650
  ),
@@ -25413,7 +25652,7 @@ var SqliteActivityRepository = class {
25413
25652
  );
25414
25653
  const modelRows = allRows(
25415
25654
  this.db.prepare(
25416
- `SELECT DISTINCT model FROM audit_events
25655
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25417
25656
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25418
25657
  ORDER BY model`
25419
25658
  ),
@@ -25422,7 +25661,7 @@ var SqliteActivityRepository = class {
25422
25661
  const derivedModels = modelRows.map((r) => r.model);
25423
25662
  const commits = countScalar(
25424
25663
  this.db,
25425
- `SELECT count(*) AS n FROM audit_events
25664
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25426
25665
  WHERE root_session_id = ? AND event_type = 'commit'`,
25427
25666
  [sessionId]
25428
25667
  );
@@ -25458,25 +25697,57 @@ var SqliteActivityRepository = class {
25458
25697
  return Promise.resolve(session);
25459
25698
  }
25460
25699
  /**
25461
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25462
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25463
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25464
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25465
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25466
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25700
+ * Cross-session token report — every `llm_call` in the store (or in a
25701
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25702
+ * session, with USD cost DERIVED at read time via the shared
25703
+ * `defaultCostModel` (never stored). The caller collapses these onto
25704
+ * per-model rows with `aggregateTokenUsage`.
25705
+ *
25706
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25707
+ * the members the rollup sums — and priced once per group, which is exact
25708
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25709
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25710
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25711
+ * the index stores the values once, at write, and answers the same window in
25712
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25713
+ * planner prefers the general event-type index and fetches every row to
25714
+ * recompute the columns it could have read. The index is one every open
25715
+ * store carries, since opening runs the migrations, so the hard requirement
25716
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25717
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25718
+ * per call, no bag parsed.
25467
25719
  */
25468
25720
  tokenReports(fromMs) {
25469
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25470
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25721
+ const rows = allRows(
25722
+ this.db.prepare(
25723
+ `${LLM_USAGE_SELECT}
25724
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25725
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25726
+ ${LLM_USAGE_GROUP}`
25727
+ ),
25728
+ fromMs === void 0 ? void 0 : [fromMs]
25729
+ );
25730
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25471
25731
  }
25472
25732
  /**
25473
- * One session's token report — its `llm_call` leaves grouped per (provider,
25474
- * model) with derived cost, or `null` when the session made no `llm_call`s
25475
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25476
- * breakdown + estimated cost.
25733
+ * One session's token report — its `llm_call`s grouped per (provider,
25734
+ * model, tier) with derived cost, or `null` when the session made no
25735
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25736
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25737
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25738
+ * it replaces walked every `llm_call` in the store to find one session's.
25477
25739
  */
25478
25740
  tokenReportForSession(sessionId) {
25479
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25741
+ const rows = allRows(
25742
+ this.db.prepare(
25743
+ `${LLM_USAGE_SELECT}
25744
+ FROM audit_events
25745
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25746
+ ${LLM_USAGE_GROUP}`
25747
+ ),
25748
+ [sessionId]
25749
+ );
25750
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25480
25751
  return Promise.resolve(reports[0] ?? null);
25481
25752
  }
25482
25753
  /**
@@ -25500,42 +25771,6 @@ var SqliteActivityRepository = class {
25500
25771
  for (const row of rows) seen.add(toHarness(row.harness));
25501
25772
  return Promise.resolve([...seen]);
25502
25773
  }
25503
- /**
25504
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25505
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25506
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25507
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25508
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25509
- */
25510
- readLlmCallLeaves(opts = {}) {
25511
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25512
- const params = [];
25513
- if (opts.sessionId !== void 0) {
25514
- conditions.push("root_session_id = ?");
25515
- params.push(opts.sessionId);
25516
- }
25517
- if (opts.fromMs !== void 0) {
25518
- conditions.push("started_at >= ?");
25519
- params.push(opts.fromMs);
25520
- }
25521
- const rows = allRows(
25522
- this.db.prepare(
25523
- `SELECT root_session_id AS sessionId, attributes
25524
- FROM audit_events
25525
- WHERE ${conditions.join(" AND ")}`
25526
- ),
25527
- params
25528
- );
25529
- return mapRowsTolerant(
25530
- rows.filter(
25531
- (row) => row.sessionId !== null
25532
- ),
25533
- (row) => ({
25534
- sessionId: row.sessionId,
25535
- attributes: JSON.parse(row.attributes)
25536
- })
25537
- );
25538
- }
25539
25774
  /**
25540
25775
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25541
25776
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25550,20 +25785,23 @@ var SqliteActivityRepository = class {
25550
25785
  const inClause = placeholders(sessionIds.length);
25551
25786
  const lastActivityRows = allRows(
25552
25787
  this.db.prepare(
25553
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25554
- WHERE root_session_id IN (${inClause})
25555
- GROUP BY root_session_id`
25788
+ `SELECT ids.value AS id,
25789
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25790
+ (SELECT max(ended_at) FROM audit_events e
25791
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25792
+ FROM json_each(?) AS ids`
25556
25793
  ),
25557
- sessionIds
25794
+ [JSON.stringify(sessionIds)]
25558
25795
  );
25559
25796
  for (const row of lastActivityRows) {
25560
- if (row.id === null) continue;
25561
25797
  const entry = result.get(row.id);
25562
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25798
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25799
+ if (entry && last > 0) entry.lastActivityMs = last;
25563
25800
  }
25564
25801
  const turnsRows = allRows(
25565
25802
  this.db.prepare(
25566
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25803
+ `SELECT root_session_id AS id, count(*) AS n
25804
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25567
25805
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25568
25806
  GROUP BY root_session_id`
25569
25807
  ),
@@ -25578,7 +25816,7 @@ var SqliteActivityRepository = class {
25578
25816
  this.db.prepare(
25579
25817
  `SELECT root_session_id AS id,
25580
25818
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25581
- FROM audit_events
25819
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25582
25820
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25583
25821
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25584
25822
  GROUP BY root_session_id`
@@ -25608,7 +25846,7 @@ var SqliteActivityRepository = class {
25608
25846
  this.db.prepare(
25609
25847
  `SELECT root_session_id AS id,
25610
25848
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25611
- FROM audit_events
25849
+ FROM audit_events INDEXED BY idx_audit_session_share
25612
25850
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25613
25851
  GROUP BY root_session_id`
25614
25852
  ),
@@ -26636,24 +26874,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26636
26874
  )`;
26637
26875
 
26638
26876
  // ../../packages/persistence/src/repositories/findings.ts
26639
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26640
- var SCAN_BATCH_ROWS = 1e3;
26641
- var DEFAULT_LOCATIONS_LIMIT = 100;
26642
- var LOCATION_RULE_IDS_CAP = 20;
26643
- function compareLocationOrder(a, b) {
26644
- return compareFindingGroupOrder(
26645
- {
26646
- severity: a.maxSeverity,
26647
- latestDetectedAt: a.latestDetectedAt,
26648
- id: ""
26649
- },
26650
- {
26651
- severity: b.maxSeverity,
26652
- latestDetectedAt: b.latestDetectedAt,
26653
- id: ""
26654
- }
26655
- );
26656
- }
26657
26877
  var CONCAT_SEP = ",";
26658
26878
  var TUPLE_SEP = "|";
26659
26879
  function splitConcat(value) {
@@ -26666,6 +26886,25 @@ function deriveInstanceStatus(row) {
26666
26886
  latestResolutionStatus: row.latest_status
26667
26887
  });
26668
26888
  }
26889
+ function toFlatFindingRow(r) {
26890
+ return {
26891
+ id: r.id,
26892
+ ruleId: r.rule_id,
26893
+ category: r.category,
26894
+ severity: r.severity,
26895
+ maskedMatch: r.masked_match,
26896
+ actionTaken: r.action_taken,
26897
+ confidence: r.confidence,
26898
+ occurredAt: epochMillisToIso(r.occurred_at),
26899
+ sourceTool: r.source_tool,
26900
+ repo: r.repo ?? "",
26901
+ file: r.file ?? "",
26902
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26903
+ eventId: r.event_id,
26904
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26905
+ status: deriveInstanceStatus(r)
26906
+ };
26907
+ }
26669
26908
  function encodeGroupCursor(group) {
26670
26909
  const payload = {
26671
26910
  sev: group.severity,
@@ -26686,13 +26925,48 @@ function decodeGroupCursor(cursor) {
26686
26925
  return null;
26687
26926
  }
26688
26927
  function firstAfter(sorted, cursor) {
26689
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
26928
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26690
26929
  return index === -1 ? sorted.length : index;
26691
26930
  }
26692
26931
  function findDeepLinked(sorted, page, id) {
26693
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26694
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
26932
+ if (page.some((t) => t.id === id)) return void 0;
26933
+ return sorted.find((t) => t.id === id);
26695
26934
  }
26935
+ function encodeLocationCursor(location) {
26936
+ const payload = {
26937
+ sev: location.maxSeverity,
26938
+ t: location.latestDetectedAt,
26939
+ r: location.repo,
26940
+ f: location.file
26941
+ };
26942
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
26943
+ }
26944
+ function decodeLocationCursor(cursor) {
26945
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
26946
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
26947
+ return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
26948
+ }
26949
+ return null;
26950
+ }
26951
+ function firstLocationAfter(sorted, cursor) {
26952
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
26953
+ return index === -1 ? sorted.length : index;
26954
+ }
26955
+ function findDeepLinkedLocation(sorted, page, id) {
26956
+ if (page.some((l) => l.id === id)) return void 0;
26957
+ return sorted.find((l) => l.id === id);
26958
+ }
26959
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
26960
+ d.severity AS severity, f.masked_match AS masked_match,
26961
+ f.action_taken AS action_taken, f.confidence AS confidence,
26962
+ e.started_at AS occurred_at,
26963
+ e.source_tool AS source_tool,
26964
+ e.repo AS repo,
26965
+ e.file_path AS file,
26966
+ e.tool_name AS tool_name,
26967
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
26968
+ e.event_type AS kind, f.finding_key AS finding_key,
26969
+ ${latestResolutionStatusSql("f")} AS latest_status`;
26696
26970
  var DAY_MS3 = 864e5;
26697
26971
  var SqliteFindingsRepository = class {
26698
26972
  constructor(db) {
@@ -26741,7 +27015,7 @@ var SqliteFindingsRepository = class {
26741
27015
  this.db.prepare(
26742
27016
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26743
27017
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26744
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27018
+ e.source_tool AS source_tool,
26745
27019
  e.event_type AS kind
26746
27020
  FROM audit_events e
26747
27021
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26813,30 +27087,26 @@ var SqliteFindingsRepository = class {
26813
27087
  );
26814
27088
  }
26815
27089
  /**
26816
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
26817
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
26818
- * attributes bag, rule_id/category/severity from the definition), scoped to
26819
- * the four capture kinds (audit_events also holds structural/reconciler/scan
26820
- * rows this list must never surface), groups by ruleId, computes
26821
- * per-filter-excluded facets, applies the requested filters, and sorts by
26822
- * severity then recency. Filtering
26823
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
26824
- * reflect the full filtered set; `items` is the requested
26825
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
26826
- * filter, `totals.findings` counts only instances whose derived status was
26827
- * requested, and each item's instance preview is narrowed the same way.
27090
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27091
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27092
+ * list must never surface), with per-filter-excluded facets, the requested
27093
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27094
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27095
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27096
+ * Under a `status` filter, `totals.findings` counts only findings whose
27097
+ * derived status was requested.
27098
+ *
27099
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27100
+ * folding EVERY finding into the numbers a type row and the filters need
27101
+ * (count, severity, category, providers, actions, statuses, latest, search
27102
+ * text). The findings OF a type come from listFindingInstances scoped to
27103
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
26828
27104
  *
26829
- * Two reads, neither of which materializes a row per finding:
26830
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
26831
- * the group and the filters need (count, providers, actions, statuses,
26832
- * latest, search text);
26833
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
26834
- * populate `instances` for the table's expanded rows.
26835
27105
  * The aggregates carry raw DB values and are translated by the same
26836
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
26837
- * rule is ever restated in SQL.
27106
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27107
+ * status rule is ever restated in SQL.
26838
27108
  */
26839
- listGroupedFindings(query) {
27109
+ listFindingTypes(query) {
26840
27110
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
26841
27111
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
26842
27112
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -26849,57 +27119,7 @@ var SqliteFindingsRepository = class {
26849
27119
  predicate,
26850
27120
  params: sessionParams
26851
27121
  });
26852
- const rows = allRows(
26853
- this.db.prepare(
26854
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26855
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26856
- kind, finding_key, latest_status
26857
- FROM (
26858
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26859
- d.severity AS severity, f.masked_match AS masked_match,
26860
- f.action_taken AS action_taken, f.confidence AS confidence,
26861
- e.started_at AS occurred_at,
26862
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26863
- json_extract(e.attributes, '$.repo') AS repo,
26864
- json_extract(e.attributes, '$.file_path') AS file,
26865
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26866
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26867
- e.event_type AS kind, f.finding_key AS finding_key,
26868
- latest.status AS latest_status,
26869
- ROW_NUMBER() OVER (
26870
- PARTITION BY d.rule_id
26871
- ORDER BY e.started_at DESC, f.id DESC
26872
- ) AS rn
26873
- FROM inspection_findings f
26874
- JOIN audit_events e ON e.id = f.audit_event_id
26875
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26876
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26877
- ON latest.finding_key = f.finding_key
26878
- ${predicate}
26879
- )
26880
- WHERE rn <= :cap
26881
- ORDER BY occurred_at DESC, id DESC`
26882
- ),
26883
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26884
- );
26885
- const groupable = rows.map((r) => ({
26886
- id: r.id,
26887
- ruleId: r.rule_id,
26888
- category: r.category,
26889
- severity: r.severity,
26890
- maskedMatch: r.masked_match,
26891
- actionTaken: r.action_taken,
26892
- confidence: r.confidence,
26893
- occurredAt: epochMillisToIso(r.occurred_at),
26894
- sourceTool: r.source_tool,
26895
- repo: r.repo ?? "",
26896
- file: r.file ?? "",
26897
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26898
- eventId: r.event_id,
26899
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26900
- status: deriveInstanceStatus(r)
26901
- }));
26902
- const allGroups = buildFindingGroups(groupable, { aggregates });
27122
+ const allTypes = buildFindingTypes(aggregates);
26903
27123
  const filterOpts = {
26904
27124
  severity: query.severity,
26905
27125
  providers: query.provider,
@@ -26908,30 +27128,25 @@ var SqliteFindingsRepository = class {
26908
27128
  subtype: query.subtype,
26909
27129
  q: query.q
26910
27130
  };
26911
- const facets = computeFindingFacets(allGroups, filterOpts);
26912
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27131
+ const facets = computeFindingFacets(allTypes, filterOpts);
27132
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
26913
27133
  const statusFilter = query.status ?? [];
26914
27134
  const totals = {
26915
- findings: sorted.reduce((acc, g) => {
26916
- if (statusFilter.length === 0) return acc + g.instanceCount;
26917
- const agg = aggregates.get(g.id);
26918
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27135
+ findings: sorted.reduce((acc, t) => {
27136
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27137
+ const agg = aggregates.get(t.id);
27138
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
26919
27139
  }, 0),
26920
- groups: sorted.length
27140
+ types: sorted.length
26921
27141
  };
26922
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27142
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
26923
27143
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
26924
27144
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
26925
27145
  const page = sorted.slice(start, start + limit);
26926
27146
  const lastOnPage = page.at(-1);
26927
27147
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
26928
27148
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
26929
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
26930
- const narrow = (g) => statusSet ? {
26931
- ...g,
26932
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
26933
- } : g;
26934
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27149
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
26935
27150
  return Promise.resolve({
26936
27151
  totals,
26937
27152
  facets,
@@ -26942,7 +27157,7 @@ var SqliteFindingsRepository = class {
26942
27157
  }
26943
27158
  /**
26944
27159
  * One row per rule_id, folding EVERY instance of the group into the values
26945
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27160
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
26946
27161
  * distinct rule_ids (the installed packs' rules), not by the store's size.
26947
27162
  *
26948
27163
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -26984,8 +27199,10 @@ var SqliteFindingsRepository = class {
26984
27199
  *
26985
27200
  * The scan runs from the top of the scope on every request, not from the
26986
27201
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26987
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26988
- * while the counting runs, and only the page itself is retained.
27202
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27203
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27204
+ * counting runs — a generator streaming the index order, not a sequence of
27205
+ * fetched batches; only the page itself is retained.
26989
27206
  */
26990
27207
  listFindingInstances(query) {
26991
27208
  const opts = {
@@ -27001,6 +27218,10 @@ var SqliteFindingsRepository = class {
27001
27218
  };
27002
27219
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
27003
27220
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27221
+ const isPastCursor = cursor === null ? () => true : (row) => {
27222
+ const rowMs = isoToEpochMillis(row.occurredAt);
27223
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27224
+ };
27004
27225
  const accumulator = createInstanceFacetAccumulator(opts);
27005
27226
  const items = [];
27006
27227
  let total = 0;
@@ -27013,6 +27234,7 @@ var SqliteFindingsRepository = class {
27013
27234
  accumulator.add(row);
27014
27235
  if (!matchesInstanceFilters(row, opts)) continue;
27015
27236
  total += 1;
27237
+ if (!isPastCursor(row)) continue;
27016
27238
  if (items.length < limit) {
27017
27239
  items.push(toInstanceDetail(row));
27018
27240
  last = row;
@@ -27021,15 +27243,6 @@ var SqliteFindingsRepository = class {
27021
27243
  }
27022
27244
  }
27023
27245
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
27024
- if (cursor !== null) {
27025
- const resumed = this.pageAfter(cursor, opts, limit, query);
27026
- return Promise.resolve({
27027
- totals: { findings: total },
27028
- facets: accumulator.facets(),
27029
- items: resumed.items,
27030
- nextCursor: resumed.nextCursor
27031
- });
27032
- }
27033
27246
  return Promise.resolve({
27034
27247
  totals: { findings: total },
27035
27248
  facets: accumulator.facets(),
@@ -27038,42 +27251,25 @@ var SqliteFindingsRepository = class {
27038
27251
  });
27039
27252
  }
27040
27253
  /**
27041
- * The page of matching rows strictly after `cursor`. Separate from the
27042
- * counting pass because that one starts at the top of the scope by design;
27043
- * this one narrows the scan with the same keyset predicate the activity list
27044
- * uses, so a later page costs less than the first rather than more.
27045
- */
27046
- pageAfter(cursor, opts, limit, query) {
27047
- const items = [];
27048
- let last;
27049
- let hasMore = false;
27050
- for (const row of this.scanFindingRows({
27051
- sessionId: query.sessionId,
27052
- from: query.from,
27053
- after: cursor
27054
- })) {
27055
- if (!matchesInstanceFilters(row, opts)) continue;
27056
- if (items.length < limit) {
27057
- items.push(toInstanceDetail(row));
27058
- last = row;
27059
- } else {
27060
- hasMore = true;
27061
- break;
27062
- }
27063
- }
27064
- return {
27065
- items,
27066
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27067
- };
27068
- }
27069
- /**
27070
- * The same findings folded by location: repository, then file within it.
27254
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27071
27255
  *
27072
27256
  * The grouping keys come from the capturing event's attributes, which is what
27073
- * the local store relates a finding to — there is no finding↔asset row to
27074
- * group by instead. A repo or file the event did not record folds into the
27075
- * empty-string bucket, which the view renders but does not link, since no
27076
- * filter can name it.
27257
+ * the local store relates a finding to; there is no finding↔asset row to group
27258
+ * by instead. A repo or file the event did not record folds into the
27259
+ * empty-string bucket, which is a real location like any other: it is listed,
27260
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27261
+ *
27262
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27263
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27264
+ * list was rebuilt to remove — and two-level pagination inside an
27265
+ * expand/collapse table is what pushed that view to master/detail in the first
27266
+ * place.
27267
+ *
27268
+ * Every filter narrows the FINDINGS and the locations fall out of what
27269
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27270
+ * reports for the same filters scoped to that pair. The view depends on it:
27271
+ * one toolbar sits over both panels precisely because a location owns none of
27272
+ * its fields.
27077
27273
  */
27078
27274
  listFindingLocations(query) {
27079
27275
  const opts = {
@@ -27085,13 +27281,16 @@ var SqliteFindingsRepository = class {
27085
27281
  tools: query.tool,
27086
27282
  q: query.q
27087
27283
  };
27088
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27284
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27285
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27089
27286
  const byRepo = /* @__PURE__ */ new Map();
27287
+ const accumulator = createInstanceFacetAccumulator(opts);
27090
27288
  let total = 0;
27091
27289
  for (const row of this.scanFindingRows({
27092
27290
  sessionId: query.sessionId,
27093
27291
  from: query.from
27094
27292
  })) {
27293
+ accumulator.add(row);
27095
27294
  if (!matchesInstanceFilters(row, opts)) continue;
27096
27295
  total += 1;
27097
27296
  let files = byRepo.get(row.repo);
@@ -27106,67 +27305,112 @@ var SqliteFindingsRepository = class {
27106
27305
  }
27107
27306
  addToLocation(acc, row);
27108
27307
  }
27109
- let fileCount = 0;
27110
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27111
- fileCount += files.size;
27112
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27113
- file: file2,
27114
- instanceCount: acc.instanceCount,
27115
- maxSeverity: acc.maxSeverity,
27116
- latestDetectedAt: acc.latestDetectedAt,
27117
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27118
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27119
- })).sort(compareLocationOrder);
27120
- const rollup = fileRows.reduce(
27121
- (a, f) => ({
27122
- instanceCount: a.instanceCount + f.instanceCount,
27123
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27124
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27125
- }),
27126
- {
27127
- instanceCount: 0,
27128
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27129
- latestDetectedAt: ""
27130
- }
27131
- );
27132
- const statuses = fileRows.map((f) => f.status);
27133
- const folded = foldGroupStatus(statuses);
27134
- return {
27135
- repo,
27136
- instanceCount: rollup.instanceCount,
27137
- maxSeverity: rollup.maxSeverity,
27138
- latestDetectedAt: rollup.latestDetectedAt,
27139
- ...folded === void 0 ? {} : { status: folded },
27140
- files: fileRows
27141
- };
27142
- });
27143
- repos.sort(compareLocationOrder);
27308
+ const sorted = [];
27309
+ for (const [repo, files] of byRepo) {
27310
+ for (const [file2, acc] of files) {
27311
+ const status = foldGroupStatus(acc.statuses);
27312
+ sorted.push({
27313
+ id: encodeLocationId(repo, file2),
27314
+ repo,
27315
+ file: file2,
27316
+ instanceCount: acc.instanceCount,
27317
+ maxSeverity: acc.maxSeverity,
27318
+ latestDetectedAt: acc.latestDetectedAt,
27319
+ ...status === void 0 ? {} : { status },
27320
+ ruleIds: [...acc.ruleIds]
27321
+ });
27322
+ }
27323
+ }
27324
+ sorted.sort(compareLocationOrder);
27325
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27326
+ const page = sorted.slice(start, start + limit);
27327
+ const lastOnPage = page.at(-1);
27328
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27329
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27144
27330
  return Promise.resolve({
27145
- totals: { findings: total, repos: repos.length, files: fileCount },
27146
- items: repos.slice(0, limit),
27147
- hasMore: repos.length > limit
27331
+ totals: { findings: total, locations: sorted.length },
27332
+ facets: accumulator.facets(),
27333
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27334
+ nextCursor
27148
27335
  });
27149
27336
  }
27150
27337
  /**
27151
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27338
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27152
27339
  *
27153
27340
  * A generator so a caller streams the scope without it ever being an array:
27154
27341
  * the flat list counts and facets the whole filtered scope, which on a large
27155
- * store is far more rows than any page. Each batch advances the same keyset
27156
- * predicate the page read uses, so the scan is a sequence of bounded reads
27157
- * rather than one unbounded result set.
27342
+ * store is far more rows than any page. The rows come off ONE statement,
27343
+ * iterated rather than materialized, in the index order `findingScanSql`
27344
+ * arranges — so the scan is a single pass with a block sort of the id
27345
+ * tie-break only, never a sort of the scope, where a sequence of
27346
+ * keyset-bounded batches re-sorted everything below the cursor on every
27347
+ * batch and cost the square of the scope.
27158
27348
  *
27159
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27160
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27161
- * makes it a point lookup per row, and the derived table would re-materialize
27162
- * a window over the whole resolution table once per batch.
27163
- *
27164
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27165
- * would be missing from its own facet, which is computed by excluding that
27166
- * dimension — see listFindingInstances.
27349
+ * `sessionId` and `from` carry ONLY what no facet counts — a filter
27350
+ * dimension narrowed here would be missing from its own facet, which is
27351
+ * computed by excluding that dimension (see listFindingInstances). There is
27352
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27353
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27354
+ * narrower statement, since the counting pass already visits every row a
27355
+ * page-2+ request would otherwise re-seek for.
27167
27356
  */
27168
27357
  *scanFindingRows(scope) {
27169
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27358
+ const { sql, params } = this.findingScanSql(scope);
27359
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27360
+ yield toFlatFindingRow(r);
27361
+ }
27362
+ }
27363
+ /**
27364
+ * One finding by its own id, or null when no such row exists.
27365
+ *
27366
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27367
+ * the store — and, unlike anything derived from a list page, it resolves a
27368
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27369
+ * deep link needs: the id it carries may name a finding thousands of rows
27370
+ * older than anything a first page holds.
27371
+ *
27372
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27373
+ * RESOLVES an id; whether that row would survive the list's current filters is
27374
+ * a different question, and hiding the target because a filter excludes it is
27375
+ * worse than showing it.
27376
+ *
27377
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27378
+ * type should the list select?" and "what does the drawer show?".
27379
+ */
27380
+ findingInstance(id) {
27381
+ const row = this.db.prepare(
27382
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27383
+ FROM inspection_findings f
27384
+ JOIN audit_events e ON e.id = f.audit_event_id
27385
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27386
+ WHERE f.id = ?`
27387
+ ).get(id);
27388
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27389
+ }
27390
+ /**
27391
+ * The one statement both instance-level scans run: every finding in scope,
27392
+ * joined to its event and definition, newest first.
27393
+ *
27394
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27395
+ * the same two `recentFindings` documents at length, for the same reason:
27396
+ *
27397
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27398
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27399
+ * yields `started_at` order per event type, not across the four, so
27400
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27401
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27402
+ * `idx_audit_session` for a session scope, which is also `started_at`
27403
+ * ordered within the session — and the order falls out of the index.
27404
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27405
+ * JOINs the planner drives from the findings and sorts everything.
27406
+ *
27407
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27408
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27409
+ * index probe per keyed row, and a derived table over the whole resolution
27410
+ * table would be materialized before the first row streamed.
27411
+ */
27412
+ findingScanSql(scope) {
27413
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27170
27414
  const params = [];
27171
27415
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27172
27416
  conditions.push("e.root_session_id = ?");
@@ -27176,64 +27420,40 @@ var SqliteFindingsRepository = class {
27176
27420
  conditions.push("e.started_at >= ?");
27177
27421
  params.push(isoToEpochMillis(scope.from));
27178
27422
  }
27179
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27180
- d.severity AS severity, f.masked_match AS masked_match,
27181
- f.action_taken AS action_taken, f.confidence AS confidence,
27182
- e.started_at AS occurred_at,
27183
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27184
- json_extract(e.attributes, '$.repo') AS repo,
27185
- json_extract(e.attributes, '$.file_path') AS file,
27186
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27187
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27188
- e.event_type AS kind, f.finding_key AS finding_key,
27189
- ${latestResolutionStatusSql("f")} AS latest_status
27190
- FROM inspection_findings f
27191
- JOIN audit_events e ON e.id = f.audit_event_id
27192
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27423
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27424
+ FROM audit_events e
27425
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27426
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27193
27427
  WHERE ${conditions.join(" AND ")}
27194
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27195
- ORDER BY e.started_at DESC, f.id DESC
27196
- LIMIT ?`;
27197
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27198
- for (; ; ) {
27199
- const rows = allRows(this.db.prepare(sql), [
27200
- ...params,
27201
- after.startedAtMs,
27202
- after.startedAtMs,
27203
- after.id,
27204
- SCAN_BATCH_ROWS
27205
- ]);
27206
- for (const r of rows) {
27207
- yield {
27208
- id: r.id,
27209
- ruleId: r.rule_id,
27210
- category: r.category,
27211
- severity: r.severity,
27212
- maskedMatch: r.masked_match,
27213
- actionTaken: r.action_taken,
27214
- confidence: r.confidence,
27215
- occurredAt: epochMillisToIso(r.occurred_at),
27216
- sourceTool: r.source_tool,
27217
- repo: r.repo ?? "",
27218
- file: r.file ?? "",
27219
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27220
- eventId: r.event_id,
27221
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27222
- status: deriveInstanceStatus(r)
27223
- };
27224
- }
27225
- if (rows.length < SCAN_BATCH_ROWS) return;
27226
- const lastRow = rows[rows.length - 1];
27227
- if (lastRow === void 0) return;
27228
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27229
- }
27428
+ ORDER BY e.started_at DESC, f.id DESC`;
27429
+ return { sql, params };
27230
27430
  }
27231
27431
  groupAggregates(withSearchText, scope) {
27232
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27233
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27234
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27432
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27433
+ group_concat(DISTINCT e.file_path) AS files,
27434
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27235
27435
  const rows = this.db.prepare(
27236
27436
  `SELECT rule_id,
27437
+ -- BARE columns beside max(latest_at), which is deliberate and
27438
+ -- is SQLite's documented behaviour: with a single min()/max()
27439
+ -- in an aggregate query, every bare column takes its value from
27440
+ -- the row that produced the extremum. So these are the severity
27441
+ -- and category of the definition whose finding is NEWEST, which
27442
+ -- is what the row-based build they replaced read off its first
27443
+ -- (newest-first) row.
27444
+ --
27445
+ -- min() is WRONG here and was the defect: inspection_definitions
27446
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27447
+ -- mints a new row), so a rule whose severity moved between
27448
+ -- versions has several, and min() picks the ALPHABETICALLY
27449
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27450
+ -- That is arbitrary in direction, and it feeds the badge, the
27451
+ -- filter, the facet counts and the primary sort key.
27452
+ --
27453
+ -- Adding a second min()/max() aggregate here would make these
27454
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27455
+ severity,
27456
+ category,
27237
27457
  sum(tuple_count) AS instance_count,
27238
27458
  max(latest_at) AS latest_at,
27239
27459
  group_concat(source_tools) AS source_tools,
@@ -27244,12 +27464,20 @@ var SqliteFindingsRepository = class {
27244
27464
  group_concat(tool_names) AS tool_names
27245
27465
  FROM (
27246
27466
  SELECT d.rule_id AS rule_id,
27467
+ -- Severity and category are columns of the DEFINITION, and
27468
+ -- a rule can have SEVERAL definitions (one per version), so
27469
+ -- these are grouped on below and resolved to the newest
27470
+ -- firing version by the outer query's bare-column select.
27471
+ -- They ride the aggregate because the type build has no rows
27472
+ -- to read them off \u2014 see buildFindingTypes.
27473
+ d.severity AS severity,
27474
+ d.category AS category,
27247
27475
  e.event_type || '${TUPLE_SEP}' ||
27248
27476
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27249
27477
  coalesce(latest.status, '') AS status_tuple,
27250
27478
  count(*) AS tuple_count,
27251
27479
  max(e.started_at) AS latest_at,
27252
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27480
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27253
27481
  group_concat(DISTINCT f.action_taken) AS actions_taken
27254
27482
  ${innerSearchColumns}
27255
27483
  FROM inspection_findings f
@@ -27258,7 +27486,7 @@ var SqliteFindingsRepository = class {
27258
27486
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27259
27487
  ON latest.finding_key = f.finding_key
27260
27488
  ${scope.predicate}
27261
- GROUP BY d.rule_id, status_tuple
27489
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27262
27490
  )
27263
27491
  GROUP BY rule_id`
27264
27492
  ).all(scope.params);
@@ -27267,6 +27495,8 @@ var SqliteFindingsRepository = class {
27267
27495
  r.rule_id,
27268
27496
  {
27269
27497
  instanceCount: r.instance_count,
27498
+ severity: r.severity,
27499
+ category: r.category,
27270
27500
  sourceTools: splitConcat(r.source_tools),
27271
27501
  actionsTaken: splitConcat(r.actions_taken),
27272
27502
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27283,7 +27513,7 @@ var SqliteFindingsRepository = class {
27283
27513
  latestDetectedAt: epochMillisToIso(r.latest_at),
27284
27514
  // Free text only — joined and substring-matched, so group_concat's
27285
27515
  // commas need no unpicking (a repo/path containing one still matches).
27286
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27516
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27287
27517
  // tell "no q this request" from "a group with no repo/file at all"
27288
27518
  // and skip priming a haystack nothing will read.
27289
27519
  ...withSearchText ? {
@@ -27380,6 +27610,8 @@ function isoDay(ms) {
27380
27610
  // ../../packages/persistence/src/repositories/history-sync.ts
27381
27611
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27382
27612
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27613
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27614
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27383
27615
  var SKIPPED = -1;
27384
27616
  var ROW_COLUMNS = `id,
27385
27617
  parent_id AS parentId,
@@ -27419,6 +27651,26 @@ var SqliteHistorySyncRepository = class {
27419
27651
  ORDER BY (event_type = 'session') DESC, started_at
27420
27652
  LIMIT :limit`
27421
27653
  );
27654
+ this.captureRowsStmt = db.prepare(
27655
+ `SELECT ${ROW_COLUMNS}
27656
+ FROM audit_events
27657
+ WHERE synced_at IS NULL
27658
+ AND sync_claimed_at IS NULL
27659
+ AND outbox_owed = 1
27660
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27661
+ AND started_at < :before
27662
+ ORDER BY started_at
27663
+ LIMIT :limit`
27664
+ );
27665
+ this.markOwedStmt = db.prepare(
27666
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27667
+ );
27668
+ this.markCaptureBacklogOwedStmt = db.prepare(
27669
+ `UPDATE audit_events SET outbox_owed = 1
27670
+ WHERE synced_at IS NULL
27671
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27672
+ AND started_at < :before`
27673
+ );
27422
27674
  this.stampStmt = db.prepare(
27423
27675
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27424
27676
  );
@@ -27450,6 +27702,12 @@ var SqliteHistorySyncRepository = class {
27450
27702
  FROM audit_events
27451
27703
  WHERE event_type IN (${TYPE_LIST})`
27452
27704
  );
27705
+ this.captureSkipCountStmt = db.prepare(
27706
+ `SELECT COUNT(*) AS skipped
27707
+ FROM audit_events
27708
+ WHERE synced_at = ${String(SKIPPED)}
27709
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27710
+ );
27453
27711
  this.fingerprintStmt = db.prepare(
27454
27712
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27455
27713
  FROM history_sync WHERE id = 1`
@@ -27459,6 +27717,12 @@ var SqliteHistorySyncRepository = class {
27459
27717
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27460
27718
  WHERE id = 1`
27461
27719
  );
27720
+ this.disownCapturesStmt = db.prepare(
27721
+ `UPDATE audit_events SET outbox_owed = NULL
27722
+ WHERE outbox_owed IS NOT NULL
27723
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27724
+ AND started_at < :attachedAt`
27725
+ );
27462
27726
  this.rearmStmt = db.prepare(
27463
27727
  `UPDATE audit_events SET synced_at = NULL
27464
27728
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27531,6 +27795,11 @@ var SqliteHistorySyncRepository = class {
27531
27795
  closeWindowStmt;
27532
27796
  releaseBoundaryStmt;
27533
27797
  freezeBoundaryStmt;
27798
+ captureRowsStmt;
27799
+ markOwedStmt;
27800
+ markCaptureBacklogOwedStmt;
27801
+ captureSkipCountStmt;
27802
+ disownCapturesStmt;
27534
27803
  partitionStmt;
27535
27804
  claimRowStmt;
27536
27805
  releaseRowStmt;
@@ -27564,6 +27833,51 @@ var SqliteHistorySyncRepository = class {
27564
27833
  pendingRows(sessionId, limit, before) {
27565
27834
  return allRows(this.rowsStmt, { sessionId, limit, before });
27566
27835
  }
27836
+ /**
27837
+ * Captures this machine still owes the deployment, oldest first.
27838
+ *
27839
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27840
+ * by a time window — see captureRowsStmt for why a window could not express
27841
+ * this. `before` is the grace window that leaves a just-recorded capture to
27842
+ * the live path.
27843
+ */
27844
+ pendingCaptureRows(limit, before) {
27845
+ return allRows(this.captureRowsStmt, { limit, before });
27846
+ }
27847
+ /**
27848
+ * Record that a capture is OWED to the deployment.
27849
+ *
27850
+ * Written by the attached forward path when a live send did not confirm
27851
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27852
+ * a fact rather than an inference: the machine was attached, the send did not
27853
+ * land, so the row is owed — which no time window can state, because the same
27854
+ * window that holds the rows a past attachment left owed also holds every
27855
+ * capture recorded while the machine was DETACHED, and those were never
27856
+ * offered to anyone.
27857
+ *
27858
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27859
+ * out of the drain's read.
27860
+ */
27861
+ markCaptureOwed(id) {
27862
+ this.markOwedStmt.run({ id });
27863
+ }
27864
+ /**
27865
+ * Mark every capture already on disk as owed, as of `before`.
27866
+ *
27867
+ * The consent-time backfill, called once from `aka attach` when a human
27868
+ * grants existing-history consent — never from an ongoing drain pass, and
27869
+ * never inferred from a boundary that could later move. `before` is the
27870
+ * caller's own "now" at the moment consent was granted, so what this marks
27871
+ * is exactly the backlog the consent prompt already counted, not whatever a
27872
+ * later re-attach or key rotation might widen it to.
27873
+ *
27874
+ * Returns how many rows matched, for the caller to log or test against. Not a
27875
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
27876
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27877
+ */
27878
+ markCaptureBacklogOwed(before) {
27879
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27880
+ }
27567
27881
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27568
27882
  markSynced(ids, atMs) {
27569
27883
  this.stampAll(ids, atMs);
@@ -27647,10 +27961,12 @@ var SqliteHistorySyncRepository = class {
27647
27961
  this.countsStmt,
27648
27962
  { before }
27649
27963
  );
27964
+ const captures = getRow(this.captureSkipCountStmt);
27650
27965
  return {
27651
27966
  pending: row?.pending ?? 0,
27652
27967
  sent: row?.sent ?? 0,
27653
- skipped: row?.skipped ?? 0
27968
+ skipped: row?.skipped ?? 0,
27969
+ capturesSkipped: captures?.skipped ?? 0
27654
27970
  };
27655
27971
  }
27656
27972
  /**
@@ -27678,20 +27994,54 @@ var SqliteHistorySyncRepository = class {
27678
27994
  *
27679
27995
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
27680
27996
  * machine has just left are undelivered as far as the new one is concerned.
27681
- * All three in one transaction, so a crash between them cannot leave stamps
27682
- * attributed to the wrong deployment, or a boundary that belongs to another.
27997
+ * All four in one transaction, so a crash between them cannot leave stamps
27998
+ * attributed to the wrong deployment, a boundary that belongs to another, or
27999
+ * a disown with no re-mark to follow it.
27683
28000
  *
27684
28001
  * The boundary is written HERE and only here, which is what freezes it: a
27685
28002
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27686
28003
  * unchanged, so this never runs and the backlog does not widen back over rows
27687
28004
  * the live path has since delivered.
28005
+ *
28006
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
28007
+ * granted existing-history consent for the deployment this call is arming —
28008
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
28009
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
28010
+ * apart. Passed only when that grant is valid, since this method has no way
28011
+ * to check consent itself and must not mark a row owed for a machine that
28012
+ * never agreed to it. Applied AFTER the disown above, in the SAME
28013
+ * transaction: what the disown clears is every marker below `backlogBefore`,
28014
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
28015
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
28016
+ * on the cleared side of that bound — and the re-mark in the same
28017
+ * transaction is what puts those rows back. A crash between the two cannot
28018
+ * strand the ledger disowned with nothing re-marked — the transaction either
28019
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
28020
+ * committed re-enters this method on the very next pass. Omit it (the
28021
+ * structural-only tests do) to exercise the disown in isolation.
28022
+ *
28023
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
28024
+ * touching a marker the NEW deployment's OWN live path has already set: B's
28025
+ * live path can mark a capture owed from the moment `aka attach` writes the
28026
+ * descriptor, before the drain's first pass ever reaches this method, and
28027
+ * such a row sits at or after the bound rather than below it. What keeps the
28028
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
28029
+ * bound — disown runs first, re-mark second, both inside the one
28030
+ * transaction above.
27688
28031
  */
27689
- rearmFor(fingerprint, backlogBefore) {
28032
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27690
28033
  this.ensureRowStmt.run();
27691
28034
  withTransaction(
27692
28035
  this.db,
27693
28036
  () => {
28037
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27694
28038
  this.rearmStmt.run();
28039
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28040
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
28041
+ }
28042
+ if (backfillCapturesBefore !== void 0) {
28043
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
28044
+ }
27695
28045
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27696
28046
  },
27697
28047
  "IMMEDIATE"
@@ -27888,7 +28238,256 @@ var SqliteInspectionFindingsRepository = class {
27888
28238
  };
27889
28239
 
27890
28240
  // ../../packages/persistence/src/repositories/installed-packs.ts
27891
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28241
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28242
+
28243
+ // ../../packages/persistence/src/policy-floor.ts
28244
+ import { readFileSync as readFileSync5 } from "fs";
28245
+ import { join as join6 } from "path";
28246
+
28247
+ // ../../packages/persistence/src/local-layout.ts
28248
+ import { renameSync as renameSync3 } from "fs";
28249
+ import { mkdir } from "fs/promises";
28250
+ import { homedir } from "os";
28251
+ import { join as join4 } from "path";
28252
+ function defaultDataDir() {
28253
+ return join4(homedir(), ".aka");
28254
+ }
28255
+ function settingsDir(base = defaultDataDir()) {
28256
+ return join4(base, "settings");
28257
+ }
28258
+ function dataDir(base = defaultDataDir()) {
28259
+ return join4(base, "data");
28260
+ }
28261
+ function dbPath(base = defaultDataDir()) {
28262
+ return join4(dataDir(base), "aka.db");
28263
+ }
28264
+ function keysDir(base = defaultDataDir()) {
28265
+ return join4(base, "keys");
28266
+ }
28267
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28268
+ ensureDataDirSync(dir);
28269
+ }
28270
+ function migrateLegacyLayout(base = defaultDataDir()) {
28271
+ const moves = [
28272
+ { name: "config.json", dest: settingsDir(base) },
28273
+ { name: "policy-cache.json", dest: dataDir(base) }
28274
+ ];
28275
+ for (const { name, dest } of moves) {
28276
+ try {
28277
+ ensureDataDirSync(dest);
28278
+ const moved = join4(dest, name);
28279
+ renameSync3(join4(base, name), moved);
28280
+ tightenFile(moved);
28281
+ } catch {
28282
+ }
28283
+ }
28284
+ }
28285
+
28286
+ // ../../packages/persistence/src/settings.ts
28287
+ import { readFileSync as readFileSync4 } from "fs";
28288
+ import { join as join5 } from "path";
28289
+
28290
+ // ../../packages/persistence/src/file-lock.ts
28291
+ import { randomUUID as randomUUID3 } from "crypto";
28292
+ import {
28293
+ closeSync,
28294
+ existsSync as existsSync2,
28295
+ openSync,
28296
+ readFileSync as readFileSync2,
28297
+ rmSync as rmSync5,
28298
+ statSync as statSync3,
28299
+ writeFileSync as writeFileSync2
28300
+ } from "fs";
28301
+ import { hostname as hostname3 } from "os";
28302
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28303
+
28304
+ // ../../packages/persistence/src/managed-settings.ts
28305
+ import { readFileSync as readFileSync3 } from "fs";
28306
+ import { posix, win32 } from "path";
28307
+ function managedSettingsPaths(platform2 = process.platform) {
28308
+ if (platform2 === "darwin") {
28309
+ return [
28310
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28311
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28312
+ ];
28313
+ }
28314
+ if (platform2 === "win32") {
28315
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28316
+ }
28317
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28318
+ }
28319
+ var testOnlyManagedPaths = null;
28320
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28321
+ for (const path of paths) {
28322
+ let text;
28323
+ try {
28324
+ text = readFileSync3(path, "utf8");
28325
+ } catch {
28326
+ continue;
28327
+ }
28328
+ const record2 = parseJsonObject(text);
28329
+ if (!record2) continue;
28330
+ const parsed = ManagedSettings.safeParse(record2);
28331
+ if (parsed.success) return parsed.data;
28332
+ }
28333
+ return null;
28334
+ }
28335
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28336
+ if (!managed) return settings;
28337
+ const { values } = managed;
28338
+ const merged = { ...settings };
28339
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28340
+ if (values.controlPlane !== void 0) {
28341
+ merged.controlPlane = {
28342
+ ...values.controlPlane,
28343
+ // The administrator pinned WHICH deployment, not WHEN this machine
28344
+ // joined it. Keep the user's own attach time when the endpoint is
28345
+ // unchanged, so a managed machine does not appear to re-attach on every
28346
+ // read; stamp a fresh one when the administrator moved it.
28347
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28348
+ };
28349
+ }
28350
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28351
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28352
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28353
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28354
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28355
+ if (values.vaultConsent !== void 0) {
28356
+ merged.vaultConsent = values.vaultConsent ? (
28357
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28358
+ // at the current version otherwise.
28359
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28360
+ ) : void 0;
28361
+ }
28362
+ if (values.modelJudgeConsent !== void 0) {
28363
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28364
+ acknowledgedAt: now().toISOString(),
28365
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28366
+ } : void 0;
28367
+ }
28368
+ return merged;
28369
+ }
28370
+
28371
+ // ../../packages/persistence/src/settings.ts
28372
+ var SETTINGS_FILENAME = "settings.json";
28373
+ function readWorkspaceSettings(base = defaultDataDir()) {
28374
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28375
+ }
28376
+ function readUserSettings(base) {
28377
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28378
+ if (!record2) return defaultWorkspaceSettings();
28379
+ try {
28380
+ return WorkspaceSettings.parse(record2);
28381
+ } catch {
28382
+ return defaultWorkspaceSettings();
28383
+ }
28384
+ }
28385
+ function readJson(file2) {
28386
+ let text;
28387
+ try {
28388
+ text = readFileSync4(file2, "utf8");
28389
+ } catch {
28390
+ return null;
28391
+ }
28392
+ return parseJsonObject(text) ?? null;
28393
+ }
28394
+
28395
+ // ../../packages/persistence/src/policy-floor.ts
28396
+ function refusalMessage(pack, attempted, floor, refusal) {
28397
+ switch (refusal) {
28398
+ case "lock":
28399
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28400
+ case "disable":
28401
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28402
+ case "floor":
28403
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28404
+ }
28405
+ }
28406
+ var PolicyFloorError = class extends Error {
28407
+ /** `namespace/packId` of the detection whose write was refused. */
28408
+ pack;
28409
+ /**
28410
+ * The archetype the caller asked for, or null when the write named none —
28411
+ * clearing the assignment, or switching the detection off.
28412
+ */
28413
+ attempted;
28414
+ /** The weakest archetype the control plane permits for this pack. */
28415
+ floor;
28416
+ refusal;
28417
+ constructor(pack, attempted, floor, refusal) {
28418
+ super(refusalMessage(pack, attempted, floor, refusal));
28419
+ this.name = "PolicyFloorError";
28420
+ this.pack = pack;
28421
+ this.attempted = attempted;
28422
+ this.floor = floor;
28423
+ this.refusal = refusal;
28424
+ }
28425
+ };
28426
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28427
+ try {
28428
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28429
+ const parsed = JSON.parse(raw);
28430
+ if (typeof parsed !== "object" || parsed === null) return null;
28431
+ return PolicyBundle.parse(parsed.bundle);
28432
+ } catch {
28433
+ return null;
28434
+ }
28435
+ }
28436
+ function indexEnabled(policies) {
28437
+ const byRuleId = /* @__PURE__ */ new Map();
28438
+ const byCategory = /* @__PURE__ */ new Map();
28439
+ for (const policy of policies) {
28440
+ if (!policy.enabled) continue;
28441
+ if ("ruleId" in policy.target) {
28442
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28443
+ } else if (!byCategory.has(policy.target.category)) {
28444
+ byCategory.set(policy.target.category, policy.action);
28445
+ }
28446
+ }
28447
+ return { byRuleId, byCategory };
28448
+ }
28449
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28450
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28451
+ const categories = new Set(rules.map((rule) => rule.category));
28452
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28453
+ return policies.some((policy) => {
28454
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28455
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28456
+ });
28457
+ }
28458
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28459
+ const floors = openControlPlaneFloors(base);
28460
+ return floors === null ? null : floors.floorFor(rules);
28461
+ }
28462
+ function openControlPlaneFloors(base = defaultDataDir()) {
28463
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28464
+ const bundle = readCachedPolicyBundle(base);
28465
+ if (bundle === null) return null;
28466
+ const indexes = indexEnabled(bundle.policies);
28467
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28468
+ }
28469
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28470
+ let action = null;
28471
+ for (const rule of rules) {
28472
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28473
+ if (resolved === void 0) continue;
28474
+ action = action === null ? resolved : strongerAction(action, resolved);
28475
+ }
28476
+ if (action === null) return null;
28477
+ return {
28478
+ floor: weakestBuiltinAtLeast(action),
28479
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28480
+ };
28481
+ }
28482
+ function policyAssignmentRefusal(policyId, floor) {
28483
+ if (floor.locked) return "lock";
28484
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28485
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28486
+ }
28487
+ function packEnablementRefusal(enabled, floor) {
28488
+ if (floor === null || enabled) return null;
28489
+ return "disable";
28490
+ }
27892
28491
 
27893
28492
  // ../../packages/persistence/src/semver.ts
27894
28493
  function parse3(version2) {
@@ -27982,8 +28581,19 @@ function ruleIdsOf(rulesJson) {
27982
28581
  return ids;
27983
28582
  }
27984
28583
  var SqliteInstalledPacksRepository = class {
27985
- constructor(db) {
28584
+ /**
28585
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28586
+ * floor needs both halves of it (settings/ says whether this machine is
28587
+ * attached, data/ holds the cached bundle). It is optional because a caller
28588
+ * holding only a DatabaseSync — every test construction site, and any embedder
28589
+ * that opens the store itself — has no layout to point at, and such a caller
28590
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28591
+ * from `openLocalDatabase`, which is the single construction site that owns a
28592
+ * real `~/.aka`.
28593
+ */
28594
+ constructor(db, baseDir) {
27986
28595
  this.db = db;
28596
+ this.baseDir = baseDir;
27987
28597
  this.insertMissingStmt = db.prepare(
27988
28598
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27989
28599
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -28005,11 +28615,17 @@ var SqliteInstalledPacksRepository = class {
28005
28615
  this.signatureStmt = db.prepare(
28006
28616
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
28007
28617
  );
28618
+ this.packRulesStmt = db.prepare(
28619
+ `SELECT rules_json AS rulesJson FROM installed_packs
28620
+ WHERE namespace = ? AND pack_id = ?`
28621
+ );
28008
28622
  }
28009
28623
  db;
28624
+ baseDir;
28010
28625
  insertMissingStmt;
28011
28626
  upsertAvailableStmt;
28012
28627
  signatureStmt;
28628
+ packRulesStmt;
28013
28629
  /**
28014
28630
  * Record the running binary's detection inventory. Refreshes the
28015
28631
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -28051,7 +28667,7 @@ var SqliteInstalledPacksRepository = class {
28051
28667
  let behind = false;
28052
28668
  for (const row of rows) {
28053
28669
  const params = {
28054
- id: randomUUID3(),
28670
+ id: randomUUID4(),
28055
28671
  namespace: row.namespace,
28056
28672
  packId: row.packId,
28057
28673
  version: row.version,
@@ -28063,7 +28679,7 @@ var SqliteInstalledPacksRepository = class {
28063
28679
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28064
28680
  this.upsertAvailableStmt.run({
28065
28681
  ...params,
28066
- id: randomUUID3(),
28682
+ id: randomUUID4(),
28067
28683
  recordedBy: meta4?.recordedBy ?? null
28068
28684
  });
28069
28685
  } else {
@@ -28309,9 +28925,65 @@ var SqliteInstalledPacksRepository = class {
28309
28925
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28310
28926
  // caller rather than swallowing them. Each returns whether a row matched, so the
28311
28927
  // caller can tell an edit from a no-such-detection.
28928
+ /**
28929
+ * The rules one installed pack owns, reduced to what a floor computation
28930
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
28931
+ * unreadable contributes no rules to a scan either, so it is not a detection
28932
+ * the control plane can be governing, and an empty list correctly imposes no
28933
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
28934
+ * the user can re-enable, and its assignment stays governed meanwhile.
28935
+ */
28936
+ packFloorRules(namespace, packId) {
28937
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
28938
+ if (!row) return [];
28939
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
28940
+ }
28941
+ /**
28942
+ * What the connected control plane imposes on one installed pack, or null on a
28943
+ * machine that is its own authority (standalone, no cached bundle, or a
28944
+ * repository constructed without a layout base).
28945
+ *
28946
+ * Exposed as a READ so a surface can render the constraint — grey out the
28947
+ * choices below the floor, mark a locked detection as locked — rather than
28948
+ * offer the user a picker whose selections it will then be told it may not
28949
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
28950
+ */
28951
+ policyFloor(namespace, packId) {
28952
+ if (this.baseDir === void 0) return null;
28953
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
28954
+ }
28955
+ /**
28956
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
28957
+ * entry only for a pack the control plane actually governs.
28958
+ *
28959
+ * A surface listing every detection asks per pack, and asking through
28960
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
28961
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
28962
+ * answer, repeated for each row, on every render. This reads all of that once.
28963
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
28964
+ * exactly as the single-pack read returns null for them.
28965
+ */
28966
+ policyFloors(packs2) {
28967
+ const floors = /* @__PURE__ */ new Map();
28968
+ if (this.baseDir === void 0) return floors;
28969
+ const source = openControlPlaneFloors(this.baseDir);
28970
+ if (source === null) return floors;
28971
+ for (const pack of packs2) {
28972
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
28973
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
28974
+ }
28975
+ return floors;
28976
+ }
28312
28977
  /**
28313
28978
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28314
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
28979
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
28980
+ *
28981
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
28982
+ * write below, and a detection the organization has authored a policy for is
28983
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
28984
+ * a throw rather than a silently substituted value. This is the one device-local
28985
+ * write path for the assignment, so the check belongs here rather than on any
28986
+ * surface that offers the choice.
28315
28987
  */
28316
28988
  setPolicy(namespace, packId, policyId) {
28317
28989
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28319,14 +28991,38 @@ var SqliteInstalledPacksRepository = class {
28319
28991
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28320
28992
  );
28321
28993
  }
28994
+ const requested = policyId;
28995
+ const floor = this.policyFloor(namespace, packId);
28996
+ if (floor !== null) {
28997
+ const refusal = policyAssignmentRefusal(requested, floor);
28998
+ if (refusal !== null) {
28999
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
29000
+ }
29001
+ }
28322
29002
  const res = this.db.prepare(
28323
29003
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28324
29004
  WHERE namespace = :namespace AND pack_id = :packId`
28325
29005
  ).run({ policyId, now: Date.now(), namespace, packId });
28326
29006
  return Number(res.changes) > 0;
28327
29007
  }
28328
- /** Enable or disable one installed pack. */
29008
+ /**
29009
+ * Enable or disable one installed pack.
29010
+ *
29011
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29012
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29013
+ * merely another point below the floor, and why re-enabling stays open. Like
29014
+ * the assignment above, the check belongs at this write path rather than on a
29015
+ * surface: this is the one device-local writer of the column, and a refusal
29016
+ * that lived in a page would leave the CLI free.
29017
+ */
28329
29018
  setEnabled(namespace, packId, enabled) {
29019
+ const floor = this.policyFloor(namespace, packId);
29020
+ if (floor !== null) {
29021
+ const refusal = packEnablementRefusal(enabled, floor);
29022
+ if (refusal !== null) {
29023
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29024
+ }
29025
+ }
28330
29026
  const res = this.db.prepare(
28331
29027
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28332
29028
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28412,7 +29108,7 @@ var SqliteInventoryRepository = class {
28412
29108
  };
28413
29109
 
28414
29110
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28415
- import { randomUUID as randomUUID4 } from "crypto";
29111
+ import { randomUUID as randomUUID5 } from "crypto";
28416
29112
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28417
29113
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28418
29114
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28901,7 +29597,7 @@ var SqliteInventoryAssetsRepository = class {
28901
29597
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28902
29598
  VALUES (:id, :projectId, :path, :access, :now, :now)
28903
29599
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28904
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29600
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28905
29601
  }
28906
29602
  return true;
28907
29603
  }
@@ -28922,7 +29618,7 @@ var SqliteInventoryAssetsRepository = class {
28922
29618
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28923
29619
  VALUES (:id, :assetId, :trust, :now, :now)
28924
29620
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28925
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29621
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28926
29622
  }
28927
29623
  this.configRowsCache = void 0;
28928
29624
  return "ok";
@@ -29219,7 +29915,7 @@ var SqliteInventoryAssetsRepository = class {
29219
29915
  };
29220
29916
 
29221
29917
  // ../../packages/persistence/src/repositories/policies.ts
29222
- import { randomUUID as randomUUID5 } from "crypto";
29918
+ import { randomUUID as randomUUID6 } from "crypto";
29223
29919
  var SqlitePoliciesRepository = class {
29224
29920
  constructor(db) {
29225
29921
  this.db = db;
@@ -29254,7 +29950,7 @@ var SqlitePoliciesRepository = class {
29254
29950
  failOpenTransaction(this.db, () => {
29255
29951
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29256
29952
  stmt.run({
29257
- id: randomUUID5(),
29953
+ id: randomUUID6(),
29258
29954
  target: JSON.stringify({ category }),
29259
29955
  action,
29260
29956
  now: Date.now()
@@ -29274,7 +29970,7 @@ var SqlitePoliciesRepository = class {
29274
29970
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29275
29971
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29276
29972
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29277
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
29973
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29278
29974
  }
29279
29975
  // Caps every global per-category policy currently set to block/redact down
29280
29976
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29342,7 +30038,7 @@ var SqlitePolicyCatalogRepository = class {
29342
30038
  };
29343
30039
 
29344
30040
  // ../../packages/persistence/src/repositories/project-files.ts
29345
- import { randomUUID as randomUUID6 } from "crypto";
30041
+ import { randomUUID as randomUUID7 } from "crypto";
29346
30042
  var SqliteProjectFilesRepository = class {
29347
30043
  constructor(db) {
29348
30044
  this.db = db;
@@ -29374,7 +30070,7 @@ var SqliteProjectFilesRepository = class {
29374
30070
  const stamp = Math.max(now, maxStamp + 1);
29375
30071
  for (const file2 of scan2.files) {
29376
30072
  this.upsertStmt.run({
29377
- id: randomUUID6(),
30073
+ id: randomUUID7(),
29378
30074
  projectId,
29379
30075
  path: file2.path,
29380
30076
  name: file2.name,
@@ -29388,9 +30084,9 @@ var SqliteProjectFilesRepository = class {
29388
30084
  };
29389
30085
 
29390
30086
  // ../../packages/persistence/src/repositories/resolutions.ts
29391
- import { randomUUID as randomUUID7 } from "crypto";
30087
+ import { randomUUID as randomUUID8 } from "crypto";
29392
30088
  var SqliteResolutionsRepository = class {
29393
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30089
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29394
30090
  this.db = db;
29395
30091
  this.now = now;
29396
30092
  this.newId = newId;
@@ -29603,7 +30299,7 @@ var SqliteScanLedgerRepository = class {
29603
30299
  };
29604
30300
 
29605
30301
  // ../../packages/persistence/src/repositories/secret-vault.ts
29606
- import { randomUUID as randomUUID8 } from "crypto";
30302
+ import { randomUUID as randomUUID9 } from "crypto";
29607
30303
  function pageLimit(requested, fallback) {
29608
30304
  if (requested === void 0) return fallback;
29609
30305
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29649,12 +30345,14 @@ var SELECT_COLUMNS = `
29649
30345
  ciphertext,
29650
30346
  nonce,
29651
30347
  auth_tag AS authTag,
30348
+ user_authorized AS userAuthorized,
29652
30349
  occurrence_count AS occurrenceCount,
29653
30350
  first_seen AS firstSeen,
29654
30351
  last_seen AS lastSeen`;
29655
30352
  function toRow(raw) {
29656
- const { provider, ...rest } = raw;
29657
- return provider === null ? rest : { ...rest, provider };
30353
+ const { provider, userAuthorized, ...rest } = raw;
30354
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30355
+ return provider === null ? row : { ...row, provider };
29658
30356
  }
29659
30357
  var SqliteSecretVaultRepository = class {
29660
30358
  constructor(db) {
@@ -29664,17 +30362,18 @@ var SqliteSecretVaultRepository = class {
29664
30362
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29665
30363
  format_version, category, rule_id, masked_match, provider,
29666
30364
  ciphertext, nonce, auth_tag,
29667
- occurrence_count, first_seen, last_seen
30365
+ user_authorized, occurrence_count, first_seen, last_seen
29668
30366
  ) VALUES (
29669
30367
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29670
30368
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29671
30369
  :ciphertext, :nonce, :authTag,
29672
- 1, :now, :now
30370
+ :userAuthorized, 1, :now, :now
29673
30371
  )`
29674
30372
  );
29675
30373
  this.bumpStmt = db.prepare(
29676
30374
  `UPDATE secret_vault
29677
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30375
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30376
+ user_authorized = max(user_authorized, :userAuthorized)
29678
30377
  WHERE value_fingerprint = :valueFingerprint`
29679
30378
  );
29680
30379
  this.byPointerStmt = db.prepare(
@@ -29694,6 +30393,7 @@ var SqliteSecretVaultRepository = class {
29694
30393
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29695
30394
  WHERE pointer_id = :pointerId`
29696
30395
  );
30396
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29697
30397
  this.derefStmt = db.prepare(
29698
30398
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29699
30399
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29707,6 +30407,7 @@ var SqliteSecretVaultRepository = class {
29707
30407
  listStmt;
29708
30408
  replaceCiphertextStmt;
29709
30409
  refreshFingerprintStmt;
30410
+ deleteByPointerStmt;
29710
30411
  derefStmt;
29711
30412
  /**
29712
30413
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29715,6 +30416,11 @@ var SqliteSecretVaultRepository = class {
29715
30416
  * pointer, category and ciphertext, so the same secret always resolves to one
29716
30417
  * wire token. `minted` is true only when this call created the row.
29717
30418
  *
30419
+ * `userAuthorized` is the one field a repeat call may still change, and only
30420
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30421
+ * the row is shared with every automatic path that vaults the same value. See
30422
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30423
+ *
29718
30424
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29719
30425
  * writers cannot both decide they are minting.
29720
30426
  */
@@ -29741,13 +30447,18 @@ var SqliteSecretVaultRepository = class {
29741
30447
  ciphertext: input2.ciphertext,
29742
30448
  nonce: input2.nonce,
29743
30449
  authTag: input2.authTag,
30450
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29744
30451
  now
29745
30452
  })
29746
30453
  );
29747
30454
  minted = true;
29748
30455
  return;
29749
30456
  }
29750
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30457
+ this.bumpStmt.run({
30458
+ valueFingerprint: input2.valueFingerprint,
30459
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30460
+ now
30461
+ });
29751
30462
  },
29752
30463
  "IMMEDIATE"
29753
30464
  );
@@ -29807,6 +30518,42 @@ var SqliteSecretVaultRepository = class {
29807
30518
  );
29808
30519
  return destroyed;
29809
30520
  }
30521
+ /**
30522
+ * Destroy the named entries and report WHICH ones went — the scoped
30523
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30524
+ * values back where they came from. Ids the store does not hold are absent
30525
+ * from the answer rather than an error, so a set assembled from a stale read
30526
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30527
+ * it.
30528
+ *
30529
+ * The ids come back rather than a count because the caller's next act is to
30530
+ * write a purge row per destroyed entry, and a record of destruction has to
30531
+ * be a record of what was really destroyed: a selection is a claim about a
30532
+ * read that has since gone stale, and auditing from it invents a purge for an
30533
+ * entry still sitting in the vault.
30534
+ *
30535
+ * One transaction over the whole set rather than a statement per id: the
30536
+ * caller hands this the result of a restore pass it has completed, and a
30537
+ * fault partway through must leave the vault as it was found rather than
30538
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30539
+ * stands for, so half a delete is not a state anything can recover from.
30540
+ */
30541
+ deleteByPointerIds(pointerIds) {
30542
+ if (pointerIds.length === 0) return [];
30543
+ const deleted = [];
30544
+ withTransaction(
30545
+ this.db,
30546
+ () => {
30547
+ for (const pointerId of pointerIds) {
30548
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30549
+ deleted.push(pointerId);
30550
+ }
30551
+ }
30552
+ },
30553
+ "IMMEDIATE"
30554
+ );
30555
+ return deleted;
30556
+ }
29810
30557
  /**
29811
30558
  * Record (or re-stamp) one place a pointer has been written. One row per
29812
30559
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29819,7 +30566,7 @@ var SqliteSecretVaultRepository = class {
29819
30566
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29820
30567
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29821
30568
  ).run({
29822
- id: randomUUID8(),
30569
+ id: randomUUID9(),
29823
30570
  pointerId: entry.pointerId,
29824
30571
  location: entry.location,
29825
30572
  kind: entry.kind,
@@ -30050,7 +30797,7 @@ function toUtcDateString(ms) {
30050
30797
  return new Date(ms).toISOString().slice(0, 10);
30051
30798
  }
30052
30799
  function isTimeseriesSeverity(s) {
30053
- return s === "critical" || s === "high" || s === "medium";
30800
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30054
30801
  }
30055
30802
  var SqliteSecurityRepository = class {
30056
30803
  constructor(db, now = () => Date.now()) {
@@ -30179,12 +30926,16 @@ var SqliteSecurityRepository = class {
30179
30926
  const now = this.now();
30180
30927
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30181
30928
  const rows = this.findingsInRange(windowStart, now);
30182
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30183
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30184
- critical: 0,
30185
- high: 0,
30186
- medium: 0
30187
- }));
30929
+ const points = Array.from(
30930
+ { length: numBuckets },
30931
+ (_, i) => ({
30932
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
30933
+ critical: 0,
30934
+ high: 0,
30935
+ medium: 0,
30936
+ low: 0
30937
+ })
30938
+ );
30188
30939
  for (const r of rows) {
30189
30940
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30190
30941
  const bucket = points[idx];
@@ -30332,15 +31083,15 @@ var SqliteSecurityRepository = class {
30332
31083
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30333
31084
  const rows = allRows(
30334
31085
  this.db.prepare(
30335
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31086
+ `SELECT e.repo AS repo, count(*) AS c
30336
31087
  FROM inspection_findings f
30337
31088
  JOIN audit_events e ON e.id = f.audit_event_id
30338
31089
  WHERE e.started_at >= :from AND e.started_at < :to
30339
31090
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30340
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30341
- AND json_extract(e.attributes, '$.repo') != ''
30342
- GROUP BY repo
30343
- ORDER BY c DESC, repo
31091
+ AND e.repo IS NOT NULL
31092
+ AND e.repo != ''
31093
+ GROUP BY e.repo
31094
+ ORDER BY c DESC, e.repo
30344
31095
  LIMIT :limit`
30345
31096
  ),
30346
31097
  { from, to: now, limit }
@@ -30402,7 +31153,8 @@ var SqliteSecurityRepository = class {
30402
31153
  `SELECT f.finding_key AS finding_key,
30403
31154
  d.rule_id AS rule_id,
30404
31155
  d.severity AS severity,
30405
- json_extract(e.attributes, '$.file_path') AS path,
31156
+ e.repo AS repo,
31157
+ e.file_path AS path,
30406
31158
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30407
31159
  latest.resolved_at AS latest_resolved_at
30408
31160
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30421,6 +31173,7 @@ var SqliteSecurityRepository = class {
30421
31173
  const items = rows.map((r) => ({
30422
31174
  findingKey: r.finding_key,
30423
31175
  ruleId: r.rule_id,
31176
+ repo: r.repo ?? "",
30424
31177
  severity: r.severity,
30425
31178
  path: r.path ?? "",
30426
31179
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -30430,13 +31183,66 @@ var SqliteSecurityRepository = class {
30430
31183
  }));
30431
31184
  return Promise.resolve({ items });
30432
31185
  }
31186
+ /**
31187
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31188
+ *
31189
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31190
+ * list: a secret committed three weeks ago and never rotated is still the most
31191
+ * important thing to fix, and any window hides it. It carried a "newest N
31192
+ * findings" cap and then a range; the first meant a different span on every
31193
+ * machine, and the second reported "no recommendations" over live exposure.
31194
+ *
31195
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31196
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31197
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31198
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31199
+ * The two answer different questions and only this one has to match a link.
31200
+ *
31201
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31202
+ * whole-store scope costs a grouped scan rather than a row per finding.
31203
+ */
31204
+ recommendationInputs() {
31205
+ const rows = allRows(
31206
+ this.db.prepare(
31207
+ `SELECT d.rule_id AS rule_id,
31208
+ d.category AS category,
31209
+ d.severity AS severity,
31210
+ COUNT(*) AS count
31211
+ FROM inspection_findings f
31212
+ JOIN audit_events e ON e.id = f.audit_event_id
31213
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31214
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31215
+ ON latest.finding_key = f.finding_key
31216
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31217
+ AND e.event_type = 'code_change'
31218
+ AND (
31219
+ f.finding_key IS NULL
31220
+ OR latest.status IS NULL
31221
+ OR latest.status NOT IN ('resolved', 'dismissed')
31222
+ )
31223
+ GROUP BY d.rule_id, d.category, d.severity`
31224
+ )
31225
+ );
31226
+ return Promise.resolve(
31227
+ rows.map((r) => ({
31228
+ ruleId: r.rule_id,
31229
+ category: r.category,
31230
+ severity: r.severity,
31231
+ count: r.count
31232
+ }))
31233
+ );
31234
+ }
30433
31235
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
30434
31236
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
30435
31237
  // numeric and the JS aggregations bucket/split on ms directly.
30436
31238
  findingsInRange(fromMs, toMs) {
30437
31239
  const rows = allRows(
30438
31240
  this.db.prepare(
30439
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31241
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31242
+ // joined for `severity`, so they are two more columns off a row this read
31243
+ // already fetches. They feed the recommended-actions rollup.
31244
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31245
+ d.rule_id AS rule_id, d.category AS category
30440
31246
  FROM inspection_findings f
30441
31247
  JOIN audit_events e ON e.id = f.audit_event_id
30442
31248
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -30449,13 +31255,15 @@ var SqliteSecurityRepository = class {
30449
31255
  return rows.map((r) => ({
30450
31256
  occurredAt: r.occurred_at,
30451
31257
  severity: r.severity,
30452
- actionTaken: r.action_taken
31258
+ actionTaken: r.action_taken,
31259
+ ruleId: r.rule_id,
31260
+ category: r.category
30453
31261
  }));
30454
31262
  }
30455
31263
  };
30456
31264
 
30457
31265
  // ../../packages/persistence/src/repositories/shares.ts
30458
- import { randomUUID as randomUUID9 } from "crypto";
31266
+ import { randomUUID as randomUUID10 } from "crypto";
30459
31267
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30460
31268
  var IN_CHUNK = 500;
30461
31269
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30543,7 +31351,7 @@ function buildSummary(dest, endpoints) {
30543
31351
  callSiteCount,
30544
31352
  transports: distinctTransports(transports),
30545
31353
  dataClasses: distinctDataClasses(dataClasses),
30546
- review: buildReviewInfo(dest.trust, transports),
31354
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30547
31355
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30548
31356
  endpoints: endpoints.map(toEndpointSummary)
30549
31357
  };
@@ -30570,7 +31378,7 @@ function buildDetail(dest, endpoints, callSites) {
30570
31378
  lastSeen: new Date(lastSeenMs).toISOString(),
30571
31379
  transports: distinctTransports(transports),
30572
31380
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30573
- review: buildReviewInfo(dest.trust, transports),
31381
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30574
31382
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30575
31383
  note: dest.note,
30576
31384
  endpoints: endpoints.map((ep) => ({
@@ -30599,7 +31407,11 @@ var SqliteSharesRepository = class {
30599
31407
  FROM share_destination d
30600
31408
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30601
31409
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30602
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31410
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31411
+ AND NOT EXISTS (
31412
+ SELECT 1 FROM egress_decision_override o
31413
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31414
+ )`
30603
31415
  );
30604
31416
  const kindCounts = countBy(
30605
31417
  this.db,
@@ -30711,7 +31523,7 @@ var SqliteSharesRepository = class {
30711
31523
  (id, destination_id, host, decision, created_at, updated_at)
30712
31524
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30713
31525
  ).run({
30714
- id: randomUUID9(),
31526
+ id: randomUUID10(),
30715
31527
  destinationId,
30716
31528
  host: dest.host,
30717
31529
  decision,
@@ -30860,7 +31672,7 @@ var SqliteSharesRepository = class {
30860
31672
  let destinationId = destIds.get(hit.host);
30861
31673
  if (destinationId === void 0) {
30862
31674
  destStmt.run({
30863
- id: randomUUID9(),
31675
+ id: randomUUID10(),
30864
31676
  kind: hit.kind,
30865
31677
  name: hit.name,
30866
31678
  host: hit.host,
@@ -30876,7 +31688,7 @@ var SqliteSharesRepository = class {
30876
31688
  let endpointId = endpointIds.get(endpointKey);
30877
31689
  if (endpointId === void 0) {
30878
31690
  endpointStmt.run({
30879
- id: randomUUID9(),
31691
+ id: randomUUID10(),
30880
31692
  destinationId,
30881
31693
  method: hit.method,
30882
31694
  transport: hit.transport,
@@ -30889,7 +31701,7 @@ var SqliteSharesRepository = class {
30889
31701
  endpointIds.set(endpointKey, endpointId);
30890
31702
  }
30891
31703
  siteStmt.run({
30892
- id: randomUUID9(),
31704
+ id: randomUUID10(),
30893
31705
  endpointId,
30894
31706
  project: input2.project,
30895
31707
  projectKey: input2.projectKey,
@@ -31254,6 +32066,7 @@ function purgeSampleData(db) {
31254
32066
  }
31255
32067
 
31256
32068
  // ../../packages/persistence/src/database.ts
32069
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31257
32070
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31258
32071
  "aka.persistence.unsafeTestOnlyRawHandle"
31259
32072
  );
@@ -31301,7 +32114,7 @@ function backupLegacyStore(db, file2) {
31301
32114
  discardStore(file2, backup);
31302
32115
  return backup;
31303
32116
  }
31304
- function openAndInitialize(file2) {
32117
+ function openAndInitialize(file2, base) {
31305
32118
  let db = openWithPragmas(file2);
31306
32119
  try {
31307
32120
  if (isForeignSqliteLineage(db)) {
@@ -31314,7 +32127,7 @@ function openAndInitialize(file2) {
31314
32127
  applyMigrations(db, file2);
31315
32128
  tightenPerms(file2);
31316
32129
  const policies = new SqlitePoliciesRepository(db);
31317
- const installedPacks = new SqliteInstalledPacksRepository(db);
32130
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31318
32131
  const repositories = {
31319
32132
  events: new SqliteEventsRepository(db),
31320
32133
  findings: new SqliteFindingsRepository(db),
@@ -31350,7 +32163,7 @@ function openAndInitialize(file2) {
31350
32163
  }
31351
32164
  function openLocalDatabase(dir) {
31352
32165
  ensureDataDirSync(dir);
31353
- const file2 = join4(dir, DB_FILENAME);
32166
+ const file2 = join7(dir, DB_FILENAME);
31354
32167
  reapStalePartials(file2);
31355
32168
  const {
31356
32169
  db,
@@ -31378,7 +32191,13 @@ function openLocalDatabase(dir) {
31378
32191
  inspectionDefinitions,
31379
32192
  inspectionFindings,
31380
32193
  configInventory
31381
- } = openAndInitialize(file2);
32194
+ } = openAndInitialize(
32195
+ file2,
32196
+ // `dir` is always `<base>/data` — every caller resolves it through
32197
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32198
+ // settings/ and data/, and the pack-policy floor needs both halves.
32199
+ dirname2(dir)
32200
+ );
31382
32201
  function captureRowId(event) {
31383
32202
  return captureId(
31384
32203
  event.metadata?.sessionId ?? null,
@@ -31391,6 +32210,21 @@ function openLocalDatabase(dir) {
31391
32210
  historySync.markSynced([captureRowId(event)], atMs);
31392
32211
  });
31393
32212
  }
32213
+ function markCaptureOwed(event) {
32214
+ failOpenTransaction(db, () => {
32215
+ historySync.markCaptureOwed(captureRowId(event));
32216
+ });
32217
+ }
32218
+ function markAuditEventsDelivered(events2, atMs) {
32219
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32220
+ if (stampable.length === 0) return;
32221
+ failOpenTransaction(db, () => {
32222
+ historySync.markSynced(
32223
+ stampable.map((event) => event.id),
32224
+ atMs
32225
+ );
32226
+ });
32227
+ }
31394
32228
  function recordCapture(event, detected) {
31395
32229
  failOpenTransaction(db, () => {
31396
32230
  const sessionId = event.metadata?.sessionId;
@@ -31477,7 +32311,7 @@ function openLocalDatabase(dir) {
31477
32311
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31478
32312
  if (!definitionId) continue;
31479
32313
  inspectionFindings.insertFinding({
31480
- id: randomUUID10(),
32314
+ id: randomUUID11(),
31481
32315
  auditEventId: record2.scanEvent.id,
31482
32316
  inspectionDefinitionId: definitionId,
31483
32317
  span: finding.span,
@@ -31573,6 +32407,8 @@ function openLocalDatabase(dir) {
31573
32407
  inspectionFindings,
31574
32408
  recordCapture,
31575
32409
  markCaptureDelivered,
32410
+ markCaptureOwed,
32411
+ markAuditEventsDelivered,
31576
32412
  ensureInventory,
31577
32413
  recordConfigScan,
31578
32414
  recordProjectFiles,
@@ -31591,6 +32427,9 @@ function openLocalDatabase(dir) {
31591
32427
  };
31592
32428
  }
31593
32429
 
32430
+ // ../../packages/persistence/src/egress-wire.ts
32431
+ import { createHash as createHash3 } from "crypto";
32432
+
31594
32433
  // ../../packages/persistence/src/exception-policy.ts
31595
32434
  var UserGrantPolicyProvider = class {
31596
32435
  #exceptions;
@@ -31611,32 +32450,18 @@ var UserGrantPolicyProvider = class {
31611
32450
  }
31612
32451
  };
31613
32452
 
31614
- // ../../packages/persistence/src/file-lock.ts
31615
- import { randomUUID as randomUUID11 } from "crypto";
31616
- import {
31617
- closeSync,
31618
- existsSync as existsSync2,
31619
- openSync,
31620
- readFileSync as readFileSync2,
31621
- rmSync as rmSync5,
31622
- statSync as statSync3,
31623
- writeFileSync as writeFileSync2
31624
- } from "fs";
31625
- import { hostname as hostname3 } from "os";
31626
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31627
-
31628
32453
  // ../../packages/persistence/src/finding-key.ts
31629
- import { createHash as createHash3 } from "crypto";
32454
+ import { createHash as createHash4 } from "crypto";
31630
32455
 
31631
32456
  // ../../packages/persistence/src/fingerprint.ts
31632
32457
  import { createHmac, randomBytes } from "crypto";
31633
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31634
- import { join as join5 } from "path";
32458
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32459
+ import { join as join8 } from "path";
31635
32460
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31636
32461
  var EXCEPTION_KEY_FILENAME = "exception.key";
31637
32462
  var KEY_MATERIAL_BYTES = 32;
31638
32463
  function keyFilePath(dataDir2) {
31639
- return join5(dataDir2, EXCEPTION_KEY_FILENAME);
32464
+ return join8(dataDir2, EXCEPTION_KEY_FILENAME);
31640
32465
  }
31641
32466
  function parseKeyFile(raw) {
31642
32467
  const parsed = JSON.parse(raw);
@@ -31674,7 +32499,7 @@ var FloorUnreadableError = class extends Error {
31674
32499
  }
31675
32500
  };
31676
32501
  function storedKeyVersionFloor(dataDir2) {
31677
- const file2 = join5(dataDir2, DB_FILENAME);
32502
+ const file2 = join8(dataDir2, DB_FILENAME);
31678
32503
  if (!existsSync3(file2)) return 0;
31679
32504
  let db;
31680
32505
  try {
@@ -31729,7 +32554,7 @@ function occupantMessage(file2, kind) {
31729
32554
  function readFingerprintKey(dataDir2) {
31730
32555
  let raw;
31731
32556
  try {
31732
- raw = readFileSync3(keyFilePath(dataDir2), "utf8");
32557
+ raw = readFileSync6(keyFilePath(dataDir2), "utf8");
31733
32558
  } catch (err) {
31734
32559
  if (err.code === "ENOENT") return null;
31735
32560
  throw err instanceof Error ? err : new Error(String(err));
@@ -31751,144 +32576,18 @@ function fingerprintValue(key, raw) {
31751
32576
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
31752
32577
  }
31753
32578
 
31754
- // ../../packages/persistence/src/history-preview.ts
32579
+ // ../../packages/persistence/src/history-backfill.ts
31755
32580
  import { existsSync as existsSync4 } from "fs";
31756
- import { join as join6 } from "path";
31757
- import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32581
+ import { join as join9 } from "path";
31758
32582
 
31759
- // ../../packages/persistence/src/local-layout.ts
31760
- import { renameSync as renameSync3 } from "fs";
31761
- import { mkdir } from "fs/promises";
31762
- import { homedir } from "os";
31763
- import { join as join7 } from "path";
31764
- function defaultDataDir() {
31765
- return join7(homedir(), ".aka");
31766
- }
31767
- function settingsDir(base = defaultDataDir()) {
31768
- return join7(base, "settings");
31769
- }
31770
- function dataDir(base = defaultDataDir()) {
31771
- return join7(base, "data");
31772
- }
31773
- function dbPath(base = defaultDataDir()) {
31774
- return join7(dataDir(base), "aka.db");
31775
- }
31776
- function keysDir(base = defaultDataDir()) {
31777
- return join7(base, "keys");
31778
- }
31779
- function ensureLayoutDirSync(dir = defaultDataDir()) {
31780
- ensureDataDirSync(dir);
31781
- }
31782
- function migrateLegacyLayout(base = defaultDataDir()) {
31783
- const moves = [
31784
- { name: "config.json", dest: settingsDir(base) },
31785
- { name: "policy-cache.json", dest: dataDir(base) }
31786
- ];
31787
- for (const { name, dest } of moves) {
31788
- try {
31789
- ensureDataDirSync(dest);
31790
- const moved = join7(dest, name);
31791
- renameSync3(join7(base, name), moved);
31792
- tightenFile(moved);
31793
- } catch {
31794
- }
31795
- }
31796
- }
31797
-
31798
- // ../../packages/persistence/src/managed-settings.ts
31799
- import { readFileSync as readFileSync4 } from "fs";
31800
- import { posix, win32 } from "path";
31801
- function managedSettingsPaths(platform2 = process.platform) {
31802
- if (platform2 === "darwin") {
31803
- return [
31804
- posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
31805
- posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
31806
- ];
31807
- }
31808
- if (platform2 === "win32") {
31809
- return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
31810
- }
31811
- return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
31812
- }
31813
- function readManagedSettings(paths = managedSettingsPaths()) {
31814
- for (const path of paths) {
31815
- let text;
31816
- try {
31817
- text = readFileSync4(path, "utf8");
31818
- } catch {
31819
- continue;
31820
- }
31821
- const record2 = parseJsonObject(text);
31822
- if (!record2) continue;
31823
- const parsed = ManagedSettings.safeParse(record2);
31824
- if (parsed.success) return parsed.data;
31825
- }
31826
- return null;
31827
- }
31828
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31829
- if (!managed) return settings;
31830
- const { values } = managed;
31831
- const merged = { ...settings };
31832
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31833
- if (values.controlPlane !== void 0) {
31834
- merged.controlPlane = {
31835
- ...values.controlPlane,
31836
- // The administrator pinned WHICH deployment, not WHEN this machine
31837
- // joined it. Keep the user's own attach time when the endpoint is
31838
- // unchanged, so a managed machine does not appear to re-attach on every
31839
- // read; stamp a fresh one when the administrator moved it.
31840
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31841
- };
31842
- }
31843
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31844
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31845
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31846
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31847
- if (values.vaultConsent !== void 0) {
31848
- merged.vaultConsent = values.vaultConsent ? (
31849
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31850
- // at the current version otherwise.
31851
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31852
- ) : void 0;
31853
- }
31854
- if (values.modelJudgeConsent !== void 0) {
31855
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31856
- acknowledgedAt: now().toISOString(),
31857
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31858
- } : void 0;
31859
- }
31860
- return merged;
31861
- }
31862
-
31863
- // ../../packages/persistence/src/settings.ts
31864
- import { readFileSync as readFileSync5 } from "fs";
31865
- import { join as join8 } from "path";
31866
- var SETTINGS_FILENAME = "settings.json";
31867
- function readWorkspaceSettings(base = defaultDataDir()) {
31868
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31869
- }
31870
- function readUserSettings(base) {
31871
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31872
- if (!record2) return defaultWorkspaceSettings();
31873
- try {
31874
- return WorkspaceSettings.parse(record2);
31875
- } catch {
31876
- return defaultWorkspaceSettings();
31877
- }
31878
- }
31879
- function readJson(file2) {
31880
- let text;
31881
- try {
31882
- text = readFileSync5(file2, "utf8");
31883
- } catch {
31884
- return null;
31885
- }
31886
- return parseJsonObject(text) ?? null;
31887
- }
32583
+ // ../../packages/persistence/src/history-preview.ts
32584
+ import { existsSync as existsSync5 } from "fs";
32585
+ import { join as join10 } from "path";
32586
+ import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31888
32587
 
31889
32588
  // ../../packages/persistence/src/store-symlinks.ts
31890
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31891
- import { dirname as dirname2, join as join9, resolve } from "path";
32589
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32590
+ import { dirname as dirname3, join as join11, resolve } from "path";
31892
32591
 
31893
32592
  // ../../packages/persistence/src/vault/crypto.ts
31894
32593
  import {
@@ -32001,8 +32700,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32001
32700
  // ../../packages/persistence/src/vault/key-provider.ts
32002
32701
  import { execFileSync } from "child_process";
32003
32702
  import { randomBytes as randomBytes2 } from "crypto";
32004
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32005
- import { join as join10 } from "path";
32703
+ import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32704
+ import { join as join12 } from "path";
32006
32705
  var VAULT_OCCUPANT_REASON = {
32007
32706
  symlink: "the path is a symlink; remove it so a keyring can be created",
32008
32707
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32101,7 +32800,7 @@ function claimRotationLock(lock, owner) {
32101
32800
  throw asError(err);
32102
32801
  }
32103
32802
  try {
32104
- writeFileSync3(join10(lock, LOCK_OWNER_FILE), `${owner}
32803
+ writeFileSync3(join12(lock, LOCK_OWNER_FILE), `${owner}
32105
32804
  `, { mode: DATA_FILE_MODE });
32106
32805
  return true;
32107
32806
  } catch (err) {
@@ -32110,7 +32809,7 @@ function claimRotationLock(lock, owner) {
32110
32809
  }
32111
32810
  }
32112
32811
  function acquireRotationLock(keysDir2) {
32113
- const lock = join10(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32812
+ const lock = join12(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32114
32813
  const owner = randomBytes2(16).toString("hex");
32115
32814
  if (claimRotationLock(lock, owner)) return { lock, owner };
32116
32815
  let held;
@@ -32137,7 +32836,7 @@ function acquireRotationLock(keysDir2) {
32137
32836
  }
32138
32837
  function releaseRotationLock(lease) {
32139
32838
  try {
32140
- if (readFileSync6(join10(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32839
+ if (readFileSync7(join12(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32141
32840
  } catch {
32142
32841
  return;
32143
32842
  }
@@ -32158,7 +32857,7 @@ var FileKeyProvider = class {
32158
32857
  this.#keysDir = keysDir2;
32159
32858
  }
32160
32859
  get filePath() {
32161
- return join10(this.#keysDir, VAULT_KEY_FILENAME);
32860
+ return join12(this.#keysDir, VAULT_KEY_FILENAME);
32162
32861
  }
32163
32862
  loadOrCreate() {
32164
32863
  return asAsync(() => {
@@ -32188,7 +32887,7 @@ var FileKeyProvider = class {
32188
32887
  #read() {
32189
32888
  let raw;
32190
32889
  try {
32191
- raw = readFileSync6(this.filePath, "utf8");
32890
+ raw = readFileSync7(this.filePath, "utf8");
32192
32891
  } catch (err) {
32193
32892
  if (err.code === "ENOENT") return null;
32194
32893
  throw err instanceof Error ? err : new Error(String(err));
@@ -32475,7 +33174,14 @@ var SecretVault = class {
32475
33174
  const existing = this.#repo.byValueFingerprint(valueFingerprint);
32476
33175
  const now = this.#now();
32477
33176
  if (existing) {
32478
- this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
33177
+ this.#repo.upsert(
33178
+ {
33179
+ ...existing,
33180
+ provider: existing.provider ?? void 0,
33181
+ userAuthorized: meta4.userAuthorized === true
33182
+ },
33183
+ now
33184
+ );
32479
33185
  return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
32480
33186
  }
32481
33187
  const { material, version: version2 } = await this.#keys.loadOrCreate();
@@ -32497,6 +33203,7 @@ var SecretVault = class {
32497
33203
  ruleId: meta4.ruleId,
32498
33204
  maskedMatch: meta4.maskedMatch,
32499
33205
  provider: meta4.provider,
33206
+ userAuthorized: meta4.userAuthorized === true,
32500
33207
  ciphertext: sealed.ciphertext.toString("base64"),
32501
33208
  nonce: sealed.nonce.toString("base64"),
32502
33209
  authTag: sealed.authTag.toString("base64")
@@ -32815,8 +33522,8 @@ var SecretVault = class {
32815
33522
  };
32816
33523
 
32817
33524
  // ../../packages/persistence/src/warn-era-cap.ts
32818
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32819
- import { join as join11 } from "path";
33525
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
33526
+ import { join as join13 } from "path";
32820
33527
 
32821
33528
  // ../../packages/plugin-sdk/src/provider-env.ts
32822
33529
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32870,8 +33577,8 @@ function resolveProvider() {
32870
33577
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32871
33578
  try {
32872
33579
  ensureLayoutDirSync(base);
32873
- const settingsFile = join12(settingsDir(base), "settings.json");
32874
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
33580
+ const settingsFile = join14(settingsDir(base), "settings.json");
33581
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
32875
33582
  } catch {
32876
33583
  }
32877
33584
  migrateLegacyLayout(base);
@@ -32894,9 +33601,9 @@ function resolveProviderSafe(resolveProviderFn) {
32894
33601
  }
32895
33602
 
32896
33603
  // ../../packages/plugin-sdk/src/config-inventory.ts
32897
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33604
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32898
33605
  import { homedir as homedir2 } from "os";
32899
- import { basename as basename3, join as join14 } from "path";
33606
+ import { basename as basename3, join as join16 } from "path";
32900
33607
 
32901
33608
  // ../../packages/detections/src/egress/registry.ts
32902
33609
  var EXTRACTOR_VERSION = "1";
@@ -35854,24 +36561,20 @@ function registerBundledPacks() {
35854
36561
  }
35855
36562
 
35856
36563
  // ../../packages/plugin-sdk/src/repo.ts
35857
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
35858
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
36564
+ import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36565
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
35859
36566
 
35860
36567
  // ../../packages/plugin-sdk/src/events.ts
35861
- import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
36568
+ import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
35862
36569
 
35863
36570
  // ../../packages/plugin-sdk/src/isolated-scan.ts
35864
- import { existsSync as existsSync9 } from "fs";
36571
+ import { existsSync as existsSync10 } from "fs";
35865
36572
  import { fileURLToPath } from "url";
35866
36573
  import { Worker } from "worker_threads";
35867
36574
 
35868
- // ../../packages/plugin-sdk/src/ignore-layers.ts
35869
- var import_ignore = __toESM(require_ignore(), 1);
35870
- import { readFileSync as readFileSync9 } from "fs";
35871
- import { join as join15 } from "path";
35872
-
35873
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
35874
- import { arch, hostname as hostname4, platform, release } from "os";
36575
+ // ../../packages/plugin-sdk/src/host-floor.ts
36576
+ import { readFileSync as readFileSync11 } from "fs";
36577
+ import { join as join18 } from "path";
35875
36578
 
35876
36579
  // ../../packages/plugin-sdk/src/model-governance.ts
35877
36580
  import {
@@ -35883,20 +36586,46 @@ import {
35883
36586
  readSync,
35884
36587
  writeFileSync as writeFileSync5
35885
36588
  } from "fs";
35886
- import { join as join16 } from "path";
36589
+ import { join as join17 } from "path";
35887
36590
  var TAIL_BYTES = 256 * 1024;
35888
36591
 
36592
+ // ../../packages/plugin-sdk/src/host-floor.ts
36593
+ var HOST_FEATURE = {
36594
+ ModelSwitch: "model-switch",
36595
+ VaultPointerDisplay: "vault-pointer-display"
36596
+ };
36597
+ var HOST_FLOORS = {
36598
+ [HOST_FEATURE.ModelSwitch]: {
36599
+ label: "model-switch protection",
36600
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
36601
+ since: "2.1.251"
36602
+ },
36603
+ [HOST_FEATURE.VaultPointerDisplay]: {
36604
+ label: "vault pointer display",
36605
+ hookEvents: ["MessageDisplay"],
36606
+ since: "2.1.152"
36607
+ }
36608
+ };
36609
+
36610
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
36611
+ var import_ignore = __toESM(require_ignore(), 1);
36612
+ import { readFileSync as readFileSync12 } from "fs";
36613
+ import { join as join19 } from "path";
36614
+
36615
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
36616
+ import { arch, hostname as hostname4, platform, release } from "os";
36617
+
35889
36618
  // ../../packages/plugin-sdk/src/nudge.ts
35890
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
35891
- import { join as join17 } from "path";
36619
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
36620
+ import { join as join20 } from "path";
35892
36621
 
35893
36622
  // ../../packages/plugin-sdk/src/paths.ts
35894
36623
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
35895
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
36624
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
35896
36625
 
35897
36626
  // ../../packages/plugin-sdk/src/project-files.ts
35898
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
35899
- import { basename as basename5, join as join18 } from "path";
36627
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
36628
+ import { basename as basename5, join as join21 } from "path";
35900
36629
 
35901
36630
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35902
36631
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35932,7 +36661,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
35932
36661
 
35933
36662
  // ../../packages/plugin-sdk/src/throttle.ts
35934
36663
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35935
- import { join as join19 } from "path";
36664
+ import { join as join22 } from "path";
35936
36665
 
35937
36666
  // ../../packages/plugin-sdk/src/tokenize.ts
35938
36667
  function redactedPlaceholder(category) {
@@ -35994,14 +36723,26 @@ var SecretVaultGlue = class {
35994
36723
  }
35995
36724
  async tokenizeText(text, opts) {
35996
36725
  try {
35997
- const findings = opts?.findings ?? this.#selfScan(text);
35998
- const reversible = opts?.reversible;
35999
- const keeps = (finding) => reversible === void 0 || reversible.has(finding);
36000
- if (findings === null) return { text: "[REDACTED]", pointers: [], degraded: [] };
36001
- if (findings.length === 0) return { text, pointers: [], degraded: [] };
36726
+ const supplied = opts?.findings;
36727
+ const resolver = opts?.resolver;
36728
+ const scanned = supplied ?? this.#selfScan(text);
36729
+ if (scanned === null) {
36730
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
36731
+ }
36732
+ const findings = supplied === void 0 && resolver !== void 0 ? scanned.filter(
36733
+ (f) => isActionAtLeast(resolver.actionFor(f.ruleId, f.category), "redact")
36734
+ ) : scanned;
36735
+ let reversible = opts?.reversible;
36736
+ if (resolver !== void 0 && reversible === void 0) {
36737
+ reversible = new Set(findings.filter((f) => resolver.isReversible(f.ruleId)));
36738
+ }
36739
+ const reversibleSet = reversible;
36740
+ const keeps = (finding) => reversibleSet === void 0 || reversibleSet.has(finding);
36741
+ if (findings.length === 0) return { text, pointers: [], degraded: [], redacted: [] };
36002
36742
  const groups = groupSpans(text, findings);
36003
36743
  const pointers = [];
36004
36744
  const degraded = [];
36745
+ const redacted = [];
36005
36746
  let out = text;
36006
36747
  for (const group of [...groups].reverse()) {
36007
36748
  const original = text.slice(group.start, group.end);
@@ -36015,6 +36756,7 @@ var SecretVaultGlue = class {
36015
36756
  degraded.unshift({ category: group.category });
36016
36757
  } else if (!keeps(finding)) {
36017
36758
  replacement = redactedPlaceholder(finding.category);
36759
+ redacted.unshift({ category: finding.category });
36018
36760
  } else {
36019
36761
  replacement = await this.tokenizeValue(finding.rawMatch, {
36020
36762
  ruleId: finding.ruleId,
@@ -36035,9 +36777,9 @@ var SecretVaultGlue = class {
36035
36777
  }
36036
36778
  }
36037
36779
  }
36038
- return { text: out, pointers, degraded };
36780
+ return { text: out, pointers, degraded, redacted };
36039
36781
  } catch {
36040
- return { text: "[REDACTED]", pointers: [], degraded: [] };
36782
+ return { text: "[REDACTED]", pointers: [], degraded: [], redacted: [] };
36041
36783
  }
36042
36784
  }
36043
36785
  async detokenizeText(text, opts) {
@@ -36249,13 +36991,13 @@ function describePointerSafe(token) {
36249
36991
  import {
36250
36992
  mkdirSync as mkdirSync5,
36251
36993
  readdirSync as readdirSync5,
36252
- readFileSync as readFileSync12,
36994
+ readFileSync as readFileSync14,
36253
36995
  renameSync as renameSync5,
36254
36996
  rmSync as rmSync7,
36255
36997
  statSync as statSync9,
36256
36998
  writeFileSync as writeFileSync8
36257
36999
  } from "fs";
36258
- import { dirname as dirname5, join as join20 } from "path";
37000
+ import { dirname as dirname6, join as join23 } from "path";
36259
37001
  var EMPTY_CARRY = Object.freeze({
36260
37002
  tail: "",
36261
37003
  fence: null,
@@ -36452,7 +37194,7 @@ var CARRY_FILE_PREFIX = "display-carry";
36452
37194
  var STALE_CARRY_MS = 15 * 60 * 1e3;
36453
37195
  function carryFilePath(dataDir2, sessionId) {
36454
37196
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
36455
- return join20(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
37197
+ return join23(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
36456
37198
  }
36457
37199
  function parseFence(value) {
36458
37200
  if (typeof value !== "object" || value === null) return null;
@@ -36463,7 +37205,7 @@ function parseFence(value) {
36463
37205
  }
36464
37206
  function loadCarry(file2, keys) {
36465
37207
  try {
36466
- const parsed = JSON.parse(readFileSync12(file2, "utf8"));
37208
+ const parsed = JSON.parse(readFileSync14(file2, "utf8"));
36467
37209
  if (typeof parsed !== "object" || parsed === null) return EMPTY_CARRY;
36468
37210
  const record2 = parsed;
36469
37211
  const revealedCount = record2.messageKey === keys.messageKey && typeof record2.revealedCount === "number" && Number.isFinite(record2.revealedCount) ? record2.revealedCount : 0;
@@ -36516,7 +37258,7 @@ function removeStaleCarryFiles(dir, keep) {
36516
37258
  const cutoff = Date.now() - STALE_CARRY_MS;
36517
37259
  for (const name of readdirSync5(dir)) {
36518
37260
  if (!name.startsWith(CARRY_FILE_PREFIX) || !name.endsWith(".json")) continue;
36519
- const path = join20(dir, name);
37261
+ const path = join23(dir, name);
36520
37262
  if (path === keep) continue;
36521
37263
  try {
36522
37264
  if (statSync9(path).mtimeMs < cutoff) rmSync7(path, { force: true });
@@ -36528,7 +37270,7 @@ function removeStaleCarryFiles(dir, keep) {
36528
37270
  }
36529
37271
  function saveCarry(file2, keys, carry) {
36530
37272
  try {
36531
- const dir = dirname5(file2);
37273
+ const dir = dirname6(file2);
36532
37274
  mkdirSync5(dir, { recursive: true });
36533
37275
  removeStaleCarryFiles(dir, file2);
36534
37276
  writeCarryRecord(file2, keys.blockKey, keys.messageKey, carry);
@@ -36538,7 +37280,7 @@ function saveCarry(file2, keys, carry) {
36538
37280
  function finalizeCarry(file2, keys, carry) {
36539
37281
  try {
36540
37282
  if (carry.revealedCount > 0) {
36541
- mkdirSync5(dirname5(file2), { recursive: true });
37283
+ mkdirSync5(dirname6(file2), { recursive: true });
36542
37284
  writeCarryRecord(file2, null, keys.messageKey, {
36543
37285
  ...EMPTY_CARRY,
36544
37286
  revealedCount: carry.revealedCount
@@ -36547,7 +37289,7 @@ function finalizeCarry(file2, keys, carry) {
36547
37289
  }
36548
37290
  let owned = true;
36549
37291
  try {
36550
- const parsed = JSON.parse(readFileSync12(file2, "utf8"));
37292
+ const parsed = JSON.parse(readFileSync14(file2, "utf8"));
36551
37293
  if (typeof parsed === "object" && parsed !== null) {
36552
37294
  const record2 = parsed;
36553
37295
  owned = record2.blockKey === keys.blockKey || record2.messageKey === keys.messageKey;