@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.
@@ -494,6 +494,7 @@ var require_ignore = __commonJS({
494
494
  // ../../packages/persistence/src/attached-derived.ts
495
495
  import { rmSync } from "fs";
496
496
  import { join } from "path";
497
+ var POLICY_CACHE_FILENAME = "policy-cache.json";
497
498
 
498
499
  // ../../packages/persistence/src/control-plane-credential.ts
499
500
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
@@ -592,6 +593,30 @@ var SQLITE_MIGRATIONS = [
592
593
  {
593
594
  tag: "0022_audit_inspection_ms",
594
595
  sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
596
+ },
597
+ {
598
+ tag: "0023_secret_vault_user_authorized",
599
+ sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
600
+ },
601
+ {
602
+ tag: "0024_finding_resolution_key_created_index",
603
+ 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`);"
604
+ },
605
+ {
606
+ tag: "0025_audit_capture_attribute_columns",
607
+ 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;"
608
+ },
609
+ {
610
+ tag: "0026_audit_llm_call_usage_columns",
611
+ 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;"
612
+ },
613
+ {
614
+ tag: "0027_audit_llm_usage_index",
615
+ 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;"
616
+ },
617
+ {
618
+ tag: "0028_activity_session_probe_indexes",
619
+ 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"
595
620
  }
596
621
  ];
597
622
 
@@ -20712,13 +20737,11 @@ var FindingGroup = external_exports.object({
20712
20737
  latestDetectedAt: external_exports.iso.datetime(),
20713
20738
  instances: external_exports.array(FindingInstance),
20714
20739
  // Derived from instances' statuses with open-dominates precedence (see
20715
- // buildFindingGroups). Undefined only when no instance carries a status.
20740
+ // foldGroupStatus). Undefined only when no instance carries a status.
20716
20741
  status: FindingStatus.optional(),
20717
- // The distinct people across the WHOLE group, not just the `instances`
20718
- // preview — from the store's whole-group aggregate when it supplies one,
20719
- // else folded from the rows (see buildFindingGroups). Undefined when no
20720
- // instance carries a user, or when the store supplied whole-group folds
20721
- // without one.
20742
+ // The distinct people across the WHOLE group, not just the instances
20743
+ // carried here. Undefined when no instance carries a user, or when the
20744
+ // store supplied whole-group folds without one.
20722
20745
  users: external_exports.array(FindingUser).optional()
20723
20746
  }).meta({ id: "FindingGroup" });
20724
20747
  var FindingStats = external_exports.object({
@@ -20747,21 +20770,31 @@ var FindingFacets = external_exports.object({
20747
20770
  // counted under no value.
20748
20771
  status: external_exports.array(FindingFacetItem),
20749
20772
  // Host tool (attributes.tool_name). Present only on the instance-level
20750
- // reads, which can filter by it; the grouped read omits the dimension
20773
+ // reads, which can filter by it; the type-level read omits the dimension
20751
20774
  // because a group spans tools.
20752
20775
  tool: external_exports.array(FindingFacetItem).optional()
20753
20776
  }).meta({ id: "FindingFacets" });
20754
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20755
- var ListGroupedFindingsQuery = external_exports.object({
20777
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20778
+ id: "FindingTypeSummary"
20779
+ });
20780
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20781
+ var MAX_FINDING_TYPES_LIMIT = 100;
20782
+ var ListFindingTypesQuery = external_exports.object({
20756
20783
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20757
- // FindingAction.
20784
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20785
+ // firing version carries, and this list pages types.
20786
+ //
20787
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20788
+ // definition versions at different severities, so a type kept by this filter
20789
+ // can hold findings that individually do not match — see totals.findings on
20790
+ // ListFindingTypesResponse, which counts them all.
20758
20791
  severity: external_exports.array(Severity).optional(),
20759
20792
  subtype: external_exports.array(external_exports.string()).optional(),
20760
20793
  provider: external_exports.array(FindingProvider).optional(),
20761
20794
  action: external_exports.array(FindingAction).optional(),
20762
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20763
- // individual instances' — so a filtered group's Status column always reads
20764
- // one of the requested values.
20795
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20796
+ // individual findings' — so a filtered row's status always reads one of the
20797
+ // requested values.
20765
20798
  status: external_exports.array(FindingStatus).optional(),
20766
20799
  q: external_exports.string().optional(),
20767
20800
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20771,23 +20804,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20771
20804
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20772
20805
  // means all time — this list has no default window.
20773
20806
  from: external_exports.iso.datetime().optional(),
20774
- // A group or instance id that must appear in the page even when the cursor
20775
- // has already advanced past its sort position. This is what keeps the
20776
- // Findings page's one-shot ?finding= deep link resolving once the list
20777
- // paginates: the target group is appended out of sort order rather than
20778
- // scanning forward for it. Never affects totals, facets or the cursor.
20807
+ // A RULE id that must appear in the page even when the cursor has already
20808
+ // advanced past its sort position. This is what keeps the selected type
20809
+ // visible in the list once it paginates: the target is appended out of sort
20810
+ // order rather than scanned forward for. Never affects totals, facets or the
20811
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20812
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20813
+ // and so is not bounded by what any page happens to hold.
20779
20814
  includeId: external_exports.string().optional(),
20780
- groupBy: external_exports.literal("type").optional(),
20781
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20815
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20782
20816
  cursor: external_exports.string().optional()
20783
20817
  });
20784
- var ListGroupedFindingsResponse = external_exports.object({
20818
+ var ListFindingTypesResponse = external_exports.object({
20785
20819
  totals: external_exports.object({
20820
+ // Findings belonging to the matching TYPES — not findings that each match
20821
+ // the filters. The filters here select types, so a type that survives
20822
+ // contributes its whole instanceCount.
20823
+ //
20824
+ // `status` is the one exception, narrowed per finding via
20825
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20826
+ // this can exceed what the instance read reports for the same filters: a
20827
+ // rule whose severity moved between versions is kept on its newest and
20828
+ // still counts its older findings. Narrowing the other three needs
20829
+ // per-dimension counts the aggregate does not carry today.
20786
20830
  findings: external_exports.number().int().nonnegative(),
20787
- groups: external_exports.number().int().nonnegative()
20831
+ // Counts TYPES, which is the unit this read pages. The instance read's
20832
+ // own totals count findings; the two deliberately answer different
20833
+ // questions and are never summed.
20834
+ types: external_exports.number().int().nonnegative()
20788
20835
  }),
20789
20836
  facets: FindingFacets,
20790
- items: external_exports.array(FindingGroup),
20837
+ items: external_exports.array(FindingTypeSummary),
20791
20838
  nextCursor: external_exports.string().nullable(),
20792
20839
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20793
20840
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20795,7 +20842,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20795
20842
  // every firing, so the two numbers legitimately differ — this map lets a
20796
20843
  // session-scoped view show both.
20797
20844
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20798
- }).meta({ id: "ListGroupedFindingsResponse" });
20845
+ }).meta({ id: "ListFindingTypesResponse" });
20799
20846
  var ApplyFindingActionRequest = external_exports.object({
20800
20847
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20801
20848
  // it, so it is excluded from the request contract. The mapping helper
@@ -20825,12 +20872,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20825
20872
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20826
20873
  var ListFindingInstancesQuery = external_exports.object({
20827
20874
  severity: external_exports.array(Severity).optional(),
20828
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20875
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20876
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20829
20877
  subtype: external_exports.array(external_exports.string()).optional(),
20830
20878
  provider: external_exports.array(FindingProvider).optional(),
20831
20879
  action: external_exports.array(FindingAction).optional(),
20832
20880
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20833
- // the grouped query's group-level fold.
20881
+ // the types query's type-level fold.
20834
20882
  status: external_exports.array(FindingStatus).optional(),
20835
20883
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20836
20884
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20847,37 +20895,47 @@ var ListFindingInstancesQuery = external_exports.object({
20847
20895
  });
20848
20896
  var ListFindingInstancesResponse = external_exports.object({
20849
20897
  // Instances matching the filters across the whole scope, not just this
20850
- // page — cursor-independent, like the grouped list's totals.
20898
+ // page — cursor-independent, like the types list's totals.
20851
20899
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20852
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20900
+ // Counts in INSTANCES here, where the types response counts types. Each
20853
20901
  // dimension still excludes its own filter.
20854
20902
  facets: FindingFacets,
20855
20903
  items: external_exports.array(FindingInstanceDetail),
20856
20904
  nextCursor: external_exports.string().nullable()
20857
20905
  }).meta({ id: "ListFindingInstancesResponse" });
20858
- var FindingLocationFile = external_exports.object({
20859
- // Empty when the instances carried no file path (a prompt or a tool call
20860
- // with no file attribution).
20861
- file: external_exports.string(),
20862
- instanceCount: external_exports.number().int().nonnegative(),
20863
- maxSeverity: Severity,
20864
- latestDetectedAt: external_exports.iso.datetime(),
20865
- // Folded from the instances' derived statuses with the same
20866
- // open-dominates precedence a group uses.
20867
- status: FindingStatus.optional(),
20868
- // Distinct rules seen at this location, capped — the row shows them as
20869
- // chips, and the count is what conveys scale.
20870
- ruleIds: external_exports.array(external_exports.string())
20871
- }).meta({ id: "FindingLocationFile" });
20872
- var FindingLocationRepo = external_exports.object({
20906
+ var FindingLocationSummary = external_exports.object({
20907
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20908
+ // because a location's identity is two values and a URL param carries one:
20909
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20910
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20911
+ // client's page dedupe — never decoded, and never a sort key.
20912
+ id: external_exports.string(),
20873
20913
  /** Empty when the instances carried no repo attribute. */
20874
20914
  repo: external_exports.string(),
20915
+ // Empty when the instances carried no file path (a prompt, or a tool call
20916
+ // with no file attribution). Both halves empty is a real location — usually
20917
+ // the largest one in a store — and is selectable like any other.
20918
+ file: external_exports.string(),
20875
20919
  instanceCount: external_exports.number().int().nonnegative(),
20920
+ // The WORST severity present, not the first row's. It is this list's primary
20921
+ // sort key, so it is also what explains why a row is where it is, and it is
20922
+ // how a reader decides what to open without opening everything.
20876
20923
  maxSeverity: Severity,
20877
20924
  latestDetectedAt: external_exports.iso.datetime(),
20925
+ // Folded from the instances' derived statuses with the same open-dominates
20926
+ // precedence a group uses, so it answers "is anything left to do here" and
20927
+ // not much more: a location holding 1 open among 40 resolved reads like one
20928
+ // holding 40 open. That loss is accepted — the panel beside this list
20929
+ // carries each finding's own status, and instanceCount sits next to the
20930
+ // badge.
20878
20931
  status: FindingStatus.optional(),
20879
- files: external_exports.array(FindingLocationFile)
20880
- }).meta({ id: "FindingLocationRepo" });
20932
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20933
+ // tally rather than a sample and a row can say how many there are. Bounded
20934
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20935
+ ruleIds: external_exports.array(external_exports.string())
20936
+ }).meta({ id: "FindingLocationSummary" });
20937
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20938
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20881
20939
  var ListFindingLocationsQuery = external_exports.object({
20882
20940
  severity: external_exports.array(Severity).optional(),
20883
20941
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20890,18 +20948,42 @@ var ListFindingLocationsQuery = external_exports.object({
20890
20948
  q: external_exports.string().optional(),
20891
20949
  sessionId: external_exports.string().optional(),
20892
20950
  from: external_exports.iso.datetime().optional(),
20893
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
20951
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
20952
+ // even when the cursor has already advanced past its sort position — the
20953
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
20954
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
20955
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
20956
+ // into the thousands, a selection sitting off page 0 is the ordinary case
20957
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
20958
+ includeId: external_exports.string().optional(),
20959
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
20960
+ cursor: external_exports.string().optional()
20894
20961
  });
20895
20962
  var ListFindingLocationsResponse = external_exports.object({
20896
20963
  totals: external_exports.object({
20964
+ // Findings matching the filters across the whole scope. Unlike the types
20965
+ // read's same-named field this needs no caveat: the filters here narrow
20966
+ // per finding, so this is the sum of every row's instanceCount.
20897
20967
  findings: external_exports.number().int().nonnegative(),
20898
- repos: external_exports.number().int().nonnegative(),
20899
- files: external_exports.number().int().nonnegative()
20968
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
20969
+ // states. The facets beside it count FINDINGS (see below); a surface
20970
+ // showing both says which is which.
20971
+ locations: external_exports.number().int().nonnegative()
20900
20972
  }),
20901
- /** Sorted by max severity, then most recent. */
20902
- items: external_exports.array(FindingLocationRepo),
20903
- /** Whether `limit` truncated the repo list. */
20904
- hasMore: external_exports.boolean()
20973
+ // Counts in FINDINGS, where the types response counts types, each dimension
20974
+ // still excluding its own filter. Deliberately not locations: counting those
20975
+ // needs a set of location keys per dimension per value — memory tracking the
20976
+ // store times the vocabulary, in a read whose scan promises flat memory —
20977
+ // and the cheap per-location version is not an approximation but WRONG. A
20978
+ // location holding {claudecode, block} and {codex, warn} would survive
20979
+ // provider=claudecode AND action=warn, under which no single finding
20980
+ // matches, so the facet would contradict the instanceCount this whole view
20981
+ // rests on. Findings also keep the toolbar in the same unit as the page
20982
+ // tally and the panel it sits above.
20983
+ facets: FindingFacets,
20984
+ /** Sorted by max severity, then most recent, then (repo, file). */
20985
+ items: external_exports.array(FindingLocationSummary),
20986
+ nextCursor: external_exports.string().nullable()
20905
20987
  }).meta({ id: "ListFindingLocationsResponse" });
20906
20988
 
20907
20989
  // ../../packages/schema/src/zod/meta.ts
@@ -22069,6 +22151,14 @@ var ControlPlaneErrorBody = external_exports.object({
22069
22151
  message: external_exports.string().optional()
22070
22152
  }).optional()
22071
22153
  });
22154
+ var RemoteFailureKind = external_exports.enum([
22155
+ "unauthorized",
22156
+ "forbidden",
22157
+ "route-absent",
22158
+ "invalid-request",
22159
+ "rejected",
22160
+ "unreachable"
22161
+ ]);
22072
22162
  var AttachDeviceRequest = external_exports.object({
22073
22163
  // This machine's own continuity id, so re-attaching ROTATES the credential
22074
22164
  // on one machine record instead of producing a second one. Client-minted
@@ -22130,6 +22220,26 @@ var AttachTokenResponse = external_exports.union([
22130
22220
  AttachTokenExpired,
22131
22221
  external_exports.object({ status: printable(64) })
22132
22222
  ]);
22223
+ var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
22224
+ var DeviceCommand = external_exports.object({
22225
+ id: printable(128).min(1),
22226
+ kind: DeviceCommandKind,
22227
+ issuedAt: printable(64).min(1),
22228
+ expiresAt: printable(64).min(1)
22229
+ }).strict();
22230
+ var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
22231
+ var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
22232
+ var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
22233
+ external_exports.object({
22234
+ outcome: external_exports.literal("reported"),
22235
+ projectsScanned: external_exports.number().int().nonnegative()
22236
+ }).strict(),
22237
+ external_exports.object({
22238
+ outcome: external_exports.literal("failed"),
22239
+ reason: DeviceCommandFailureReason,
22240
+ projectsScanned: external_exports.number().int().nonnegative()
22241
+ }).strict()
22242
+ ]);
22133
22243
 
22134
22244
  // ../../packages/schema/src/zod/registry.ts
22135
22245
  var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
@@ -22296,7 +22406,7 @@ var PackManifest = external_exports.object({
22296
22406
  }).meta({ id: "PackManifest" });
22297
22407
 
22298
22408
  // ../../packages/schema/src/zod/detection.ts
22299
- var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
22409
+ var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
22300
22410
  var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
22301
22411
  var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
22302
22412
  var DetectionCounts = external_exports.object({
@@ -22433,14 +22543,17 @@ function optional2(key, parsed, raw) {
22433
22543
  function isStringArray(value) {
22434
22544
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22435
22545
  }
22546
+ var ORIGIN_VALUES = { library: true, custom: true };
22547
+ function resolveOrigin(origin) {
22548
+ return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
22549
+ }
22436
22550
  function summaryToDetectionListItem(s) {
22437
22551
  return {
22438
22552
  id: `${s.namespace}/${s.packId}`,
22439
22553
  name: s.name,
22440
22554
  version: s.version,
22441
22555
  enabled: s.enabled,
22442
- origin: "library",
22443
- // v1: every installed pack is library origin
22556
+ origin: resolveOrigin(s.origin),
22444
22557
  namespace: s.namespace,
22445
22558
  packId: s.packId,
22446
22559
  ruleCount: s.ruleCount,
@@ -22492,7 +22605,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
22492
22605
  name: row.name,
22493
22606
  version: row.version,
22494
22607
  enabled: row.enabled,
22495
- origin: "library",
22608
+ origin: resolveOrigin(row.origin),
22496
22609
  namespace: row.namespace,
22497
22610
  packId: row.packId,
22498
22611
  ruleCount: row.rules.length,
@@ -22512,16 +22625,20 @@ function splitDetectionId(id) {
22512
22625
  }
22513
22626
  function buildDetectionsList(summaries, query) {
22514
22627
  const withUpdate = summaries.filter((s) => s.latestVersion != null);
22628
+ const originOf = (s) => resolveOrigin(s.origin);
22515
22629
  const counts = {
22516
22630
  all: summaries.length,
22517
- library: summaries.length,
22518
- // all origin=library in v1
22519
- custom: 0,
22631
+ library: summaries.filter((s) => originOf(s) === "library").length,
22632
+ custom: summaries.filter((s) => originOf(s) === "custom").length,
22633
+ // No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
22634
+ // omission: `customized` would mean a LIBRARY pack whose rules were edited in
22635
+ // place, and that state does not exist — editing a library pack forks it. See
22636
+ // OriginEnum.
22520
22637
  customized: 0,
22521
22638
  updates: withUpdate.length
22522
22639
  };
22523
22640
  const filter = query.filter;
22524
- let filtered = filter === "custom" || filter === "customized" ? [] : filter === "updates" ? [...withUpdate] : [...summaries];
22641
+ let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
22525
22642
  if (query.q) {
22526
22643
  const q = query.q.toLowerCase();
22527
22644
  filtered = filtered.filter(
@@ -22601,8 +22718,9 @@ var Event = external_exports.object({
22601
22718
  metadata: EventMetadata.optional()
22602
22719
  }).meta({ id: "Event" });
22603
22720
  var IngestEvent = Event.meta({ id: "IngestEvent" });
22721
+ var INGEST_BATCH_MAX = 100;
22604
22722
  var IngestBatch = external_exports.object({
22605
- events: external_exports.array(IngestEvent).min(1).max(100),
22723
+ events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
22606
22724
  // Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
22607
22725
  // additionally rejects any event whose contentHash the store has already
22608
22726
  // recorded — for re-runnable bulk ingest (worktree scan, transcript
@@ -22734,139 +22852,62 @@ function deriveFindingStatus(row) {
22734
22852
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22735
22853
  return "open";
22736
22854
  }
22737
- function distinctUsers(instances) {
22738
- const seen = /* @__PURE__ */ new Set();
22739
- const users = [];
22740
- for (const i of instances) {
22741
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22742
- seen.add(i.user.id);
22743
- users.push(i.user);
22744
- }
22745
- return users;
22746
- }
22747
22855
  function sortUsers(users) {
22748
22856
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22749
22857
  }
22750
- function buildFindingGroups(rows, opts = {}) {
22751
- const overrides = opts.overrides;
22858
+ function buildFindingTypes(aggregates, opts = {}) {
22752
22859
  const packNames = opts.packNames;
22753
- const aggregates = opts.aggregates;
22754
- const byRuleId = /* @__PURE__ */ new Map();
22755
- for (const row of rows) {
22756
- const existing = byRuleId.get(row.ruleId);
22757
- if (existing) existing.push(row);
22758
- else byRuleId.set(row.ruleId, [row]);
22759
- }
22760
- const groups = [];
22761
- for (const [ruleId, ruleRows] of byRuleId) {
22762
- const instances = ruleRows.map((r) => {
22763
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22764
- return {
22765
- id: r.id,
22766
- provider: toApiProvider(r.sourceTool),
22767
- repo: r.repo,
22768
- file: r.file,
22769
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22770
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22771
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22772
- ...r.user === void 0 ? {} : { user: r.user },
22773
- action: toApiAction(effectiveDbAction),
22774
- detectedAt: r.occurredAt,
22775
- confidence: r.confidence,
22776
- status: r.status
22777
- };
22778
- });
22779
- const agg = aggregates?.get(ruleId);
22780
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22781
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22782
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22783
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22784
- );
22785
- const seenProviders = /* @__PURE__ */ new Set();
22786
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22787
- if (seenProviders.has(p)) return false;
22788
- seenProviders.add(p);
22789
- return true;
22790
- });
22791
- const actionSet = new Set(
22792
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22793
- );
22860
+ const types = [];
22861
+ for (const [ruleId, agg] of aggregates) {
22862
+ const users = sortUsers(agg.users ?? []);
22863
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22864
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22794
22865
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22795
- const severity = ruleRows[0]?.severity ?? "low";
22796
- const detection = {
22797
- id: ruleId,
22798
- name: packNames?.get(ruleId) ?? null
22799
- };
22800
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22801
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22802
- const match = {
22803
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22804
- contextPrefix: ""
22805
- // empty (pending privacy review)
22806
- };
22807
- const status = foldGroupStatus(
22808
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22809
- );
22810
- const group = {
22866
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22867
+ const type = {
22811
22868
  id: ruleId,
22812
22869
  category: apiCategory,
22813
22870
  subtype: ruleId,
22814
22871
  // human label comes with pack metadata later
22815
- severity,
22816
- match,
22817
- detection,
22818
- policy,
22819
- instanceCount: agg?.instanceCount ?? instances.length,
22872
+ severity: agg.severity ?? "low",
22873
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22874
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22875
+ instanceCount: agg.instanceCount,
22820
22876
  providers,
22821
22877
  aggregateAction,
22822
- latestDetectedAt,
22823
- instances,
22824
- status,
22878
+ latestDetectedAt: agg.latestDetectedAt,
22879
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22825
22880
  ...users.length > 0 ? { users } : {}
22826
22881
  };
22827
- if (agg) {
22828
- actionsCache.set(group, [...actionSet]);
22829
- if (agg.searchText !== void 0) {
22830
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22831
- }
22882
+ actionsCache.set(type, [...actionSet]);
22883
+ if (agg.searchText !== void 0) {
22884
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22832
22885
  }
22833
- groups.push(group);
22886
+ types.push(type);
22834
22887
  }
22835
- return groups;
22888
+ return types;
22836
22889
  }
22837
22890
  var haystackCache = /* @__PURE__ */ new WeakMap();
22838
- function buildHaystack(g, extra) {
22891
+ function buildHaystack(t, extra) {
22839
22892
  return [
22840
- g.subtype,
22841
- g.category,
22842
- g.match.maskedValue,
22843
- g.policy.name,
22844
- g.id,
22845
- ...g.instances.map((i) => i.repo),
22846
- ...g.instances.map((i) => i.file),
22847
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22848
- ...g.instances.map((i) => i.id),
22849
- // The people: the whole group's list when the store folded one, plus the
22850
- // preview's own — the two overlap, and a haystack does not mind.
22851
- ...(g.users ?? []).map((u) => u.name),
22852
- ...g.instances.map((i) => i.user?.name ?? ""),
22893
+ t.subtype,
22894
+ t.category,
22895
+ t.policy.name,
22896
+ t.id,
22897
+ ...(t.users ?? []).map((u) => u.name),
22853
22898
  ...extra === void 0 ? [] : [extra]
22854
22899
  ].join(" ").toLowerCase();
22855
22900
  }
22856
- function groupHaystack(g) {
22857
- const cached2 = haystackCache.get(g);
22901
+ function typeHaystack(t) {
22902
+ const cached2 = haystackCache.get(t);
22858
22903
  if (cached2 !== void 0) return cached2;
22859
- const haystack = buildHaystack(g);
22860
- haystackCache.set(g, haystack);
22904
+ const haystack = buildHaystack(t);
22905
+ haystackCache.set(t, haystack);
22861
22906
  return haystack;
22862
22907
  }
22863
22908
  var actionsCache = /* @__PURE__ */ new WeakMap();
22864
- function groupActions(g) {
22865
- const cached2 = actionsCache.get(g);
22866
- if (cached2 !== void 0) return cached2;
22867
- const actions = [...new Set(g.instances.map((i) => i.action))];
22868
- actionsCache.set(g, actions);
22869
- return actions;
22909
+ function typeActions(t) {
22910
+ return actionsCache.get(t) ?? [];
22870
22911
  }
22871
22912
  function countInstancesByStatus(statusInputs, statuses) {
22872
22913
  const statusSet = new Set(statuses);
@@ -22877,8 +22918,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22877
22918
  }
22878
22919
  return sum;
22879
22920
  }
22880
- function applyFindingFilters(groups, opts) {
22881
- let filtered = groups;
22921
+ function applyFindingFilters(types, opts) {
22922
+ let filtered = types;
22882
22923
  if (opts.severity && opts.severity.length > 0) {
22883
22924
  const sevSet = new Set(opts.severity);
22884
22925
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22889,7 +22930,7 @@ function applyFindingFilters(groups, opts) {
22889
22930
  }
22890
22931
  if (opts.actions && opts.actions.length > 0) {
22891
22932
  const actionSet = new Set(opts.actions);
22892
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
22933
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22893
22934
  }
22894
22935
  if (opts.subtype && opts.subtype.length > 0) {
22895
22936
  const subtypeSet = new Set(opts.subtype);
@@ -22901,7 +22942,7 @@ function applyFindingFilters(groups, opts) {
22901
22942
  }
22902
22943
  if (opts.q) {
22903
22944
  const q = opts.q.toLowerCase();
22904
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
22945
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22905
22946
  }
22906
22947
  return filtered;
22907
22948
  }
@@ -22916,11 +22957,11 @@ function compareFindingGroupOrder(a, b) {
22916
22957
  if (recencyDiff !== 0) return recencyDiff;
22917
22958
  return a.id.localeCompare(b.id);
22918
22959
  }
22919
- function sortFindingGroups(groups) {
22920
- return [...groups].sort(compareFindingGroupOrder);
22960
+ function sortFindingTypes(types) {
22961
+ return [...types].sort(compareFindingGroupOrder);
22921
22962
  }
22922
- function computeFindingFacets(allGroups, opts) {
22923
- const forSeverity = applyFindingFilters(allGroups, {
22963
+ function computeFindingFacets(allTypes, opts) {
22964
+ const forSeverity = applyFindingFilters(allTypes, {
22924
22965
  providers: opts.providers,
22925
22966
  actions: opts.actions,
22926
22967
  statuses: opts.statuses,
@@ -22931,7 +22972,7 @@ function computeFindingFacets(allGroups, opts) {
22931
22972
  for (const g of forSeverity) {
22932
22973
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22933
22974
  }
22934
- const forProvider = applyFindingFilters(allGroups, {
22975
+ const forProvider = applyFindingFilters(allTypes, {
22935
22976
  actions: opts.actions,
22936
22977
  statuses: opts.statuses,
22937
22978
  q: opts.q,
@@ -22942,7 +22983,7 @@ function computeFindingFacets(allGroups, opts) {
22942
22983
  for (const g of forProvider) {
22943
22984
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
22944
22985
  }
22945
- const forAction = applyFindingFilters(allGroups, {
22986
+ const forAction = applyFindingFilters(allTypes, {
22946
22987
  providers: opts.providers,
22947
22988
  statuses: opts.statuses,
22948
22989
  q: opts.q,
@@ -22951,9 +22992,9 @@ function computeFindingFacets(allGroups, opts) {
22951
22992
  });
22952
22993
  const actionMap = /* @__PURE__ */ new Map();
22953
22994
  for (const g of forAction) {
22954
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
22995
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
22955
22996
  }
22956
- const forSubtype = applyFindingFilters(allGroups, {
22997
+ const forSubtype = applyFindingFilters(allTypes, {
22957
22998
  providers: opts.providers,
22958
22999
  actions: opts.actions,
22959
23000
  statuses: opts.statuses,
@@ -22962,7 +23003,7 @@ function computeFindingFacets(allGroups, opts) {
22962
23003
  });
22963
23004
  const subtypeMap = /* @__PURE__ */ new Map();
22964
23005
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
22965
- const forStatus = applyFindingFilters(allGroups, {
23006
+ const forStatus = applyFindingFilters(allTypes, {
22966
23007
  providers: opts.providers,
22967
23008
  actions: opts.actions,
22968
23009
  q: opts.q,
@@ -23010,10 +23051,20 @@ function matchesDimension(row, opts, dimension) {
23010
23051
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23011
23052
  case "tools":
23012
23053
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23054
+ // An EMPTY value is a real filter here, not an absent one. The location
23055
+ // list buckets a finding whose event recorded no repo — or no file — under
23056
+ // the empty string, and selecting that bucket has to narrow the panel to
23057
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23058
+ // row omits the key, which every call site already does.
23059
+ //
23060
+ // Reading '' as unset is what this replaced, and it failed in the one place
23061
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23062
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23063
+ // — a row reading 3 findings beside a panel listing every finding there is.
23013
23064
  case "repo":
23014
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23065
+ return opts.repo === void 0 || row.repo === opts.repo;
23015
23066
  case "file":
23016
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23067
+ return opts.file === void 0 || row.file === opts.file;
23017
23068
  case "q":
23018
23069
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23019
23070
  }
@@ -23127,6 +23178,23 @@ function addToLocation(acc, row) {
23127
23178
  acc.statuses.push(row.status);
23128
23179
  acc.ruleIds.add(row.ruleId);
23129
23180
  }
23181
+ function compareLocationOrder(a, b) {
23182
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23183
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23184
+ if (rankA !== rankB) return rankA - rankB;
23185
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23186
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23187
+ }
23188
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23189
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23190
+ return 0;
23191
+ }
23192
+ function encodeLocationId(repo, file2) {
23193
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23194
+ }
23195
+ function encodePart(value) {
23196
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23197
+ }
23130
23198
 
23131
23199
  // ../../packages/schema/src/zod/installed-pack.ts
23132
23200
  var InstalledPack = external_exports.object({
@@ -23158,111 +23226,370 @@ var PatchInstalledPackRequest = external_exports.object({
23158
23226
  message: "At least one field must be provided"
23159
23227
  }).meta({ id: "PatchInstalledPackRequest" });
23160
23228
 
23161
- // ../../packages/schema/src/zod/vault.ts
23162
- var POINTER_FORMAT_VERSION = 2;
23163
- var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23164
- var POINTER_TOKEN_PATTERN = new RegExp(
23165
- `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23166
- );
23167
- var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23168
- var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23169
- var ParsedPointer = external_exports.object({
23170
- category: DetectionCategory,
23171
- keyVersion: external_exports.number().int().positive(),
23172
- pointerId: external_exports.string(),
23173
- tag: external_exports.string()
23174
- });
23175
- var VaultEntry = external_exports.object({
23176
- pointerId: external_exports.string(),
23177
- // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23178
- // derived under. This is what a reveal-to-model grant matches on, and it rotates
23179
- // independently of the vault encryption key below.
23180
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23181
- fingerprintKeyVersion: external_exports.number().int().positive(),
23182
- // The vault-key epoch this row's ciphertext was sealed under.
23183
- keyVersion: external_exports.number().int().positive(),
23184
- // Fixed at first mint and never updated: the same value detected later under a
23185
- // different rule's category keeps the category it was minted with, so one
23186
- // value always produces exactly one wire token.
23187
- category: DetectionCategory,
23188
- ruleId: external_exports.string(),
23189
- // Partial-reveal preview for badges and listings. Never the raw value.
23190
- maskedMatch: external_exports.string(),
23191
- provider: external_exports.string().optional(),
23192
- ciphertext: external_exports.string(),
23193
- nonce: external_exports.string(),
23194
- authTag: external_exports.string(),
23195
- // How many times this value has been detected on this machine — the reuse
23196
- // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23197
- occurrenceCount: external_exports.number().int().nonnegative(),
23198
- firstSeen: external_exports.string(),
23199
- lastSeen: external_exports.string()
23200
- });
23201
- var PointerDescriptor = external_exports.object({
23202
- category: DetectionCategory,
23203
- provider: external_exports.string().optional(),
23204
- maskedMatch: external_exports.string(),
23205
- occurrences: external_exports.number().int().nonnegative(),
23206
- firstSeen: external_exports.string(),
23207
- lastSeen: external_exports.string()
23208
- });
23209
- var PointerIdentity = external_exports.object({
23210
- ruleId: external_exports.string(),
23211
- valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23212
- fingerprintKeyVersion: external_exports.number().int().positive()
23213
- });
23214
- var DetokenizeTarget = external_exports.enum(["human", "model"]);
23215
- var VaultDerefReason = external_exports.enum([
23216
- "display",
23217
- "explicit-reveal",
23218
- "view-render",
23219
- "model-input",
23220
- "remediation",
23221
- "purge"
23222
- ]);
23223
- var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23224
- var VaultDeref = external_exports.object({
23229
+ // ../../packages/schema/src/zod/policy.ts
23230
+ var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23231
+ var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23232
+ var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23233
+ var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
23234
+ var Policy = external_exports.object({
23225
23235
  id: external_exports.guid(),
23226
- pointerId: external_exports.string(),
23227
- at: external_exports.string(),
23228
- target: DetokenizeTarget,
23229
- reason: VaultDerefReason,
23230
- outcome: VaultDerefOutcome,
23231
- // Present only on a model-target crossing that a reveal grant authorized.
23232
- grantId: external_exports.string().optional(),
23233
- // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23234
- // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23235
- pointerCount: external_exports.number().int().positive().default(1)
23236
- });
23237
- var VaultSightingKind = external_exports.enum([
23238
- "prompt",
23239
- "tool-input",
23240
- "tool-output",
23241
- "file",
23242
- "transcript"
23243
- ]);
23244
- var VaultSighting = external_exports.object({
23245
- location: external_exports.string(),
23246
- kind: VaultSightingKind,
23247
- firstSeen: external_exports.string(),
23248
- lastSeen: external_exports.string()
23249
- });
23250
- var VaultInventoryEntry = external_exports.object({
23251
- pointerId: external_exports.string(),
23252
- category: DetectionCategory,
23253
- provider: external_exports.string().optional(),
23254
- maskedMatch: external_exports.string(),
23255
- occurrences: external_exports.number().int().nonnegative(),
23256
- firstSeen: external_exports.string(),
23257
- lastSeen: external_exports.string(),
23258
- // The active reveal-to-model grant covering this value, when one exists —
23259
- // the inventory badges it, the row links to revocation.
23260
- revealGrantId: external_exports.string().nullable(),
23261
- sightings: external_exports.array(VaultSighting)
23236
+ scope: PolicyScope,
23237
+ target: PolicyTarget,
23238
+ action: ActionTaken,
23239
+ enabled: external_exports.boolean().default(true),
23240
+ customKeywords: external_exports.array(external_exports.string()).optional(),
23241
+ // Display name — optional so older policy rows without name still parse.
23242
+ // Added for the findings API (policy.name column migration).
23243
+ name: external_exports.string().optional(),
23244
+ // Whether an AUTHORED policy governs this row's target — not a claim about
23245
+ // which row this is. A producer that collapses several rows onto one target
23246
+ // must carry the marker onto whichever row survives, or the collapse decides
23247
+ // the answer; a survivor may therefore be a built-in expansion still marked
23248
+ // 'authored' because an authored sibling targeted the same thing.
23249
+ // Optional so an older producer — and an older on-disk cache — still parses;
23250
+ // absent reads as 'builtin', which is the behaviour that predates the field.
23251
+ //
23252
+ // Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
23253
+ // built-in archetype catalog entry a policy is, which every catalog surface
23254
+ // reads and which a caller may state. This one is a statement the PRODUCER
23255
+ // of a bundle makes about a row, and only the bundle builder ever stamps it
23256
+ // — the CRUD routes neither accept nor set it.
23257
+ //
23258
+ // A device consumes this in exactly one direction: an 'authored' policy
23259
+ // arriving from a control plane marks the rules it targets as not
23260
+ // locally re-assignable. That can only ever ADD a refusal, never relax one,
23261
+ // which is what makes it safe to honour from an unsigned cache — the same
23262
+ // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23263
+ provenance: PolicyProvenance.optional()
23264
+ }).meta({ id: "Policy" });
23265
+ var PolicyBundle = external_exports.object({
23266
+ version: external_exports.string(),
23267
+ policies: external_exports.array(Policy),
23268
+ // Rules from the installed marketplace packs (snapshotted by the
23269
+ // control plane). The plugin registers these in addition to its bundled
23270
+ // packs. Optional so older backends — and older on-disk caches — that omit
23271
+ // the field still parse; consumers read `bundle.rules ?? []`.
23272
+ rules: external_exports.array(Rule).optional(),
23273
+ // When true, `rules` IS the complete effective ruleset and the runtime must
23274
+ // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23275
+ // after reading the user's installed snapshot (installed_packs, enabled
23276
+ // packs only), which is how detection updates stay manual: new bundled
23277
+ // rules run only after the user applies the pack update. Absent/false keeps
23278
+ // the historical composition (bundled packs + rules) — older caches.
23279
+ rulesComplete: external_exports.boolean().optional(),
23280
+ // Active detection exceptions, evaluation subset only (see
23281
+ // ExceptionBundleEntry). Optional so older bundle producers — and older
23282
+ // on-disk caches — that omit the field still parse; consumers read
23283
+ // `bundle.exceptions ?? []`.
23284
+ exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23285
+ // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23286
+ // A second axis over the same `redact` action, carried beside the policies
23287
+ // rather than on them: nothing writes ruleId-targeted policies to disk, so
23288
+ // widening Policy itself would change a persisted shape to express something
23289
+ // only the in-memory bundle needs. Optional so an older producer — or an
23290
+ // older on-disk cache — still parses; consumers read `?? []` and get the
23291
+ // pre-existing one-way behaviour, which is the safe direction to default.
23292
+ reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23293
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
23294
+ // from a versioned installed pack. Optional so older backends — and older
23295
+ // on-disk caches — that omit the field still parse; consumers fall back to
23296
+ // the rule's own spec version. NOT the bundle version above — see
23297
+ // installedRuleset's ruleVersions for the source of truth.
23298
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23299
+ // Model ids (the raw `model` string a harness reports, e.g.
23300
+ // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23301
+ // a session onto one (PreModelSwitch) and refuses a turn that would run on
23302
+ // one (UserPromptSubmit). Optional so an older backend — and an older
23303
+ // on-disk cache — still parses; consumers read `?? []`, which is the
23304
+ // unenforced behaviour that predates this field and the safe direction to
23305
+ // default.
23306
+ //
23307
+ // Ids, not display names: the governance decision is keyed on the exact
23308
+ // string the harness reports (`model_status_override.versionId` in the
23309
+ // control plane), so no name resolution stands between the decision and the
23310
+ // comparison.
23311
+ prohibitedModels: external_exports.array(external_exports.string()).optional(),
23312
+ customKeywords: external_exports.array(external_exports.string()),
23313
+ fetchedAt: external_exports.iso.datetime()
23314
+ }).meta({ id: "PolicyBundle" });
23315
+ var POLICY_BUNDLE_SHAPE_ID = [
23316
+ ...Object.keys(PolicyBundle.shape),
23317
+ ...Object.keys(Policy.shape).map((key) => `policies.${key}`),
23318
+ ...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
23319
+ ...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
23320
+ ].sort().join(",");
23321
+ var OBSERVE_ONLY_CATEGORIES = ["config"];
23322
+ var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23323
+ var CATEGORY_PEAK_SEVERITY = {
23324
+ secret: "critical",
23325
+ financial: "critical",
23326
+ // core-financial/credit-card
23327
+ code_flaw: "critical",
23328
+ pii: "high",
23329
+ phi: "high",
23330
+ custom: "high",
23331
+ // user-defined; conservative
23332
+ code_context: "low",
23333
+ config: "low"
23334
+ // observe-only; floors to monitor regardless
23335
+ };
23336
+ function severityFloorPolicy(category) {
23337
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23338
+ const peak = CATEGORY_PEAK_SEVERITY[category];
23339
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
23340
+ }
23341
+ function severityFloorPosture() {
23342
+ const out = {};
23343
+ for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23344
+ return out;
23345
+ }
23346
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23347
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23348
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23349
+ id: "RedactFallback"
23262
23350
  });
23263
- var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23264
- var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23265
- var MAX_VAULT_PAGE_LIMIT = 200;
23351
+ var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23352
+ var BUILTIN_POLICY_SPECS = {
23353
+ monitor: {
23354
+ name: "Monitor",
23355
+ action: "log",
23356
+ reversible: false,
23357
+ description: "Log every match for audit. The request is allowed through untouched."
23358
+ },
23359
+ warn: {
23360
+ name: "Warn",
23361
+ action: "warn",
23362
+ reversible: false,
23363
+ description: "Allow the request, but warn the user inline before it is sent."
23364
+ },
23365
+ redact: {
23366
+ name: "Redact",
23367
+ action: "redact",
23368
+ reversible: false,
23369
+ description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23370
+ },
23371
+ vault: {
23372
+ name: "Redact & Vault",
23373
+ action: "redact",
23374
+ reversible: true,
23375
+ 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."
23376
+ },
23377
+ block: {
23378
+ name: "Block",
23379
+ action: "block",
23380
+ reversible: false,
23381
+ description: "Refuse the request entirely whenever any rule in this detection matches."
23382
+ }
23383
+ };
23384
+ function builtinPolicyToAction(id) {
23385
+ return BUILTIN_POLICY_SPECS[id].action;
23386
+ }
23387
+ var PALETTE_WEAKEST_FIRST = [
23388
+ ...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
23389
+ ];
23390
+ var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
23391
+ (action) => !PALETTE_WEAKEST_FIRST.includes(action)
23392
+ );
23393
+ var ACTION_STRENGTH_ORDER = [
23394
+ ...BELOW_PALETTE,
23395
+ ...PALETTE_WEAKEST_FIRST
23396
+ ];
23397
+ function actionRank(action) {
23398
+ return ACTION_STRENGTH_ORDER.indexOf(action);
23399
+ }
23400
+ function isActionAtLeast(action, floor) {
23401
+ return actionRank(action) >= actionRank(floor);
23402
+ }
23403
+ function strongerAction(a, b) {
23404
+ return actionRank(a) >= actionRank(b) ? a : b;
23405
+ }
23406
+ function weakestBuiltinAtLeast(floor) {
23407
+ return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23408
+ }
23409
+ var PackPolicyFloor = external_exports.object({
23410
+ /**
23411
+ * The weakest archetype the device may assign. Stated as a BuiltinPolicyId
23412
+ * rather than a raw ActionTaken because that is the vocabulary the user
23413
+ * picks from — a floor a UI cannot name is one it cannot explain.
23414
+ */
23415
+ floor: BuiltinPolicyId,
23416
+ /**
23417
+ * True when the organization AUTHORED a policy governing this pack rather
23418
+ * than stating a minimum: it gave the answer, so the pack is not
23419
+ * re-assignable locally in either direction.
23420
+ */
23421
+ locked: external_exports.boolean()
23422
+ }).describe("PackPolicyFloor");
23423
+ var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23424
+ (id) => !BUILTIN_POLICY_SPECS[id].reversible
23425
+ );
23426
+ var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23427
+ (id) => BUILTIN_POLICY_SPECS[id].reversible
23428
+ );
23429
+ var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23430
+ function builtinPolicyIsReversible(id) {
23431
+ return BUILTIN_POLICY_SPECS[id].reversible;
23432
+ }
23433
+ function policyIdIsReversible(policyId) {
23434
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23435
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23436
+ return builtinPolicyIsReversible(id);
23437
+ }
23438
+ var DEFAULT_ACTIONS = Object.fromEntries(
23439
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23440
+ );
23441
+ var BUILTIN_POLICIES = Object.fromEntries(
23442
+ KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23443
+ );
23444
+ var DEFAULT_PACK_POLICY_ID = "monitor";
23445
+ function policyIdToAction(policyId) {
23446
+ const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23447
+ const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23448
+ return BUILTIN_POLICIES[id].action;
23449
+ }
23450
+ var UsedByItem = external_exports.object({
23451
+ id: external_exports.string(),
23452
+ name: external_exports.string(),
23453
+ ruleCount: external_exports.number().int().nonnegative(),
23454
+ enabled: external_exports.boolean()
23455
+ }).meta({ id: "UsedByItem" });
23456
+ var PolicyListItem = external_exports.object({
23457
+ id: external_exports.string(),
23458
+ kind: PolicyKind,
23459
+ name: external_exports.string(),
23460
+ enabled: external_exports.boolean(),
23461
+ usedByCount: external_exports.number().int().nonnegative()
23462
+ }).meta({ id: "PolicyListItem" });
23463
+ var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23464
+ var PolicyDetail = external_exports.object({
23465
+ specVersion: external_exports.literal(1),
23466
+ id: external_exports.string(),
23467
+ kind: PolicyKind,
23468
+ name: external_exports.string(),
23469
+ enabled: external_exports.boolean(),
23470
+ description: external_exports.string(),
23471
+ usedBy: external_exports.array(UsedByItem)
23472
+ }).meta({ id: "PolicyDetail" });
23473
+ var PolicyStatsResponse = external_exports.object({
23474
+ policies: external_exports.number().int().nonnegative(),
23475
+ builtin: external_exports.number().int().nonnegative(),
23476
+ custom: external_exports.number().int().nonnegative(),
23477
+ detectionsGoverned: external_exports.number().int().nonnegative()
23478
+ }).meta({ id: "PolicyStatsResponse" });
23479
+
23480
+ // ../../packages/schema/src/zod/vault.ts
23481
+ var POINTER_FORMAT_VERSION = 2;
23482
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
23483
+ var POINTER_TOKEN_PATTERN = new RegExp(
23484
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
23485
+ );
23486
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
23487
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
23488
+ var ParsedPointer = external_exports.object({
23489
+ category: DetectionCategory,
23490
+ keyVersion: external_exports.number().int().positive(),
23491
+ pointerId: external_exports.string(),
23492
+ tag: external_exports.string()
23493
+ });
23494
+ var VaultEntry = external_exports.object({
23495
+ pointerId: external_exports.string(),
23496
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
23497
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
23498
+ // independently of the vault encryption key below.
23499
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23500
+ fingerprintKeyVersion: external_exports.number().int().positive(),
23501
+ // The vault-key epoch this row's ciphertext was sealed under.
23502
+ keyVersion: external_exports.number().int().positive(),
23503
+ // Fixed at first mint and never updated: the same value detected later under a
23504
+ // different rule's category keeps the category it was minted with, so one
23505
+ // value always produces exactly one wire token.
23506
+ category: DetectionCategory,
23507
+ ruleId: external_exports.string(),
23508
+ // Partial-reveal preview for badges and listings. Never the raw value.
23509
+ maskedMatch: external_exports.string(),
23510
+ provider: external_exports.string().optional(),
23511
+ ciphertext: external_exports.string(),
23512
+ nonce: external_exports.string(),
23513
+ authTag: external_exports.string(),
23514
+ // How many times this value has been detected on this machine — the reuse
23515
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
23516
+ occurrenceCount: external_exports.number().int().nonnegative(),
23517
+ // True when a PERSON asked for this value to be replaced — the surfaced-
23518
+ // secrets strike — rather than a pack enforcing its assignment. One value is
23519
+ // one row however many paths vault it, so this is what tells a policy sweep
23520
+ // that the row carries somebody's own instruction and not just an assignment
23521
+ // that has since been lowered. STICKY and MONOTONIC: a later automatic
23522
+ // vaulting of the same value must never clear it — what the user said about
23523
+ // the value does not expire.
23524
+ userAuthorized: external_exports.boolean(),
23525
+ firstSeen: external_exports.string(),
23526
+ lastSeen: external_exports.string()
23527
+ });
23528
+ var PointerDescriptor = external_exports.object({
23529
+ category: DetectionCategory,
23530
+ provider: external_exports.string().optional(),
23531
+ maskedMatch: external_exports.string(),
23532
+ occurrences: external_exports.number().int().nonnegative(),
23533
+ firstSeen: external_exports.string(),
23534
+ lastSeen: external_exports.string()
23535
+ });
23536
+ var PointerIdentity = external_exports.object({
23537
+ ruleId: external_exports.string(),
23538
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
23539
+ fingerprintKeyVersion: external_exports.number().int().positive()
23540
+ });
23541
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
23542
+ var VaultDerefReason = external_exports.enum([
23543
+ "display",
23544
+ "explicit-reveal",
23545
+ "view-render",
23546
+ "model-input",
23547
+ "remediation",
23548
+ "purge"
23549
+ ]);
23550
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
23551
+ var VaultDeref = external_exports.object({
23552
+ id: external_exports.guid(),
23553
+ pointerId: external_exports.string(),
23554
+ at: external_exports.string(),
23555
+ target: DetokenizeTarget,
23556
+ reason: VaultDerefReason,
23557
+ outcome: VaultDerefOutcome,
23558
+ // Present only on a model-target crossing that a reveal grant authorized.
23559
+ grantId: external_exports.string().optional(),
23560
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
23561
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
23562
+ pointerCount: external_exports.number().int().positive().default(1)
23563
+ });
23564
+ var VaultSightingKind = external_exports.enum([
23565
+ "prompt",
23566
+ "tool-input",
23567
+ "tool-output",
23568
+ "file",
23569
+ "transcript"
23570
+ ]);
23571
+ var VaultSighting = external_exports.object({
23572
+ location: external_exports.string(),
23573
+ kind: VaultSightingKind,
23574
+ firstSeen: external_exports.string(),
23575
+ lastSeen: external_exports.string()
23576
+ });
23577
+ var VaultInventoryEntry = external_exports.object({
23578
+ pointerId: external_exports.string(),
23579
+ category: DetectionCategory,
23580
+ provider: external_exports.string().optional(),
23581
+ maskedMatch: external_exports.string(),
23582
+ occurrences: external_exports.number().int().nonnegative(),
23583
+ firstSeen: external_exports.string(),
23584
+ lastSeen: external_exports.string(),
23585
+ // The active reveal-to-model grant covering this value, when one exists —
23586
+ // the inventory badges it, the row links to revocation.
23587
+ revealGrantId: external_exports.string().nullable(),
23588
+ sightings: external_exports.array(VaultSighting)
23589
+ });
23590
+ var DEFAULT_VAULT_INVENTORY_LIMIT = 50;
23591
+ var DEFAULT_VAULT_DEREFS_LIMIT = 50;
23592
+ var MAX_VAULT_PAGE_LIMIT = 200;
23266
23593
  var ListVaultInventoryQuery = external_exports.object({
23267
23594
  limit: external_exports.coerce.number().int().min(1).max(MAX_VAULT_PAGE_LIMIT).optional(),
23268
23595
  // Opaque; names the last row of the page just served.
@@ -23316,7 +23643,7 @@ function isVaultConsentValid(consent) {
23316
23643
  }
23317
23644
 
23318
23645
  // ../../packages/schema/src/zod/local.ts
23319
- var WORKSPACE_SETTINGS_SPEC_VERSION = 6;
23646
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23320
23647
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23321
23648
  var RunMode = external_exports.enum(["standalone", "attached"]);
23322
23649
  var ControlPlaneConnection = external_exports.object({
@@ -23361,6 +23688,19 @@ var WorkspaceSettings = external_exports.object({
23361
23688
  vaultKeyCustody: VaultKeyCustody.default("file"),
23362
23689
  // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
23363
23690
  vaultInlineReveal: VaultInlineReveal.default("masked"),
23691
+ // What a `redact` policy degrades to on a FIELD the host cannot rewrite in
23692
+ // place. Not a handling policy: the policy has already resolved to redact,
23693
+ // and this only says what happens when the host offers no channel to carry it
23694
+ // out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
23695
+ // Claude Code decline to mask a field that EXECUTES because masking would
23696
+ // change what runs. Per FIELD rather than per host, so a host that can
23697
+ // rewrite some inputs keeps true redaction on those.
23698
+ //
23699
+ // Spelled in the built-in policy vocabulary rather than as a fresh enum, so
23700
+ // an attached machine's merge is `strongerAction` over the one action ladder
23701
+ // and no second rank order exists to drift from it. 'deny' is a host wire
23702
+ // word and stays out of the stored value.
23703
+ redactFallback: RedactFallback.default("warn"),
23364
23704
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
23365
23705
  onboardedAt: external_exports.iso.datetime().optional(),
23366
23706
  // Records that the user consented to sending findings to the model API for
@@ -23368,15 +23708,20 @@ var WorkspaceSettings = external_exports.object({
23368
23708
  // Absent until granted; a stale payloadVersion means the consent no longer
23369
23709
  // covers the current payload and must be re-granted.
23370
23710
  modelJudgeConsent: ModelJudgeConsent.optional(),
23371
- // Records that the user consented to sending the activity already recorded on
23372
- // this machine to the deployment it is attached to, along with the payload
23373
- // shape and the endpoint they agreed to. Absent until granted, and a grant for
23374
- // a different endpoint or an older payload no longer counts.
23711
+ // Records that the user consented to the DEFERRED send — the outbox — along
23712
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23713
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23714
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23715
+ // both widenings. Absent until granted, and a grant for a different endpoint
23716
+ // or an older payload no longer counts.
23375
23717
  historySyncConsent: HistorySyncConsent.optional()
23376
23718
  });
23377
23719
  function defaultWorkspaceSettings() {
23378
23720
  return WorkspaceSettings.parse({});
23379
23721
  }
23722
+ function isAttached(settings) {
23723
+ return settings.runMode === "attached" && settings.controlPlane !== void 0;
23724
+ }
23380
23725
  function toInventoryRow(input2, id, now) {
23381
23726
  return {
23382
23727
  id,
@@ -23496,8 +23841,12 @@ var ManagedSettingKey = external_exports.enum([
23496
23841
  "vaultKeyCustody",
23497
23842
  "vaultInlineReveal",
23498
23843
  "modelJudgeConsent",
23499
- "dataSharesInPlace"
23844
+ "dataSharesInPlace",
23845
+ "redactFallback"
23500
23846
  ]).meta({ id: "ManagedSettingKey" });
23847
+ function isManagedSettingKey(value) {
23848
+ return ManagedSettingKey.safeParse(value).success;
23849
+ }
23501
23850
  var ManagedSettingsValues = external_exports.object({
23502
23851
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23503
23852
  controlPlane: external_exports.object({
@@ -23509,7 +23858,8 @@ var ManagedSettingsValues = external_exports.object({
23509
23858
  vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
23510
23859
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23511
23860
  modelJudgeConsent: external_exports.boolean().optional(),
23512
- dataSharesInPlace: external_exports.boolean().optional()
23861
+ dataSharesInPlace: external_exports.boolean().optional(),
23862
+ redactFallback: RedactFallback.optional()
23513
23863
  }).meta({ id: "ManagedSettingsValues" });
23514
23864
  var ManagedSettings = external_exports.object({
23515
23865
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23521,195 +23871,30 @@ var ManagedSettings = external_exports.object({
23521
23871
  // Which of those the user may not change. A key here with no matching value
23522
23872
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23523
23873
  // the user may still override. The two are separable on purpose.
23524
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23874
+ //
23875
+ // Parsed as NAMES rather than as the enum, and split below: a name this
23876
+ // build does not know is dropped from the locked set and reported, never a
23877
+ // reason to refuse the file. The same shape reaches an older build whenever
23878
+ // an administrator locks a key a newer build added, and refusing it there
23879
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
23880
+ // the fleets most likely to carry a version skew. A name outside the enum
23881
+ // is still never HONOURED: the lockable set stays explicit above.
23882
+ lockedFields: external_exports.array(external_exports.string()).default([])
23883
+ }).transform(({ lockedFields, ...rest }) => {
23884
+ const known = [];
23885
+ const unknown2 = [];
23886
+ for (const name of lockedFields) {
23887
+ if (isManagedSettingKey(name)) known.push(name);
23888
+ else unknown2.push(name);
23889
+ }
23890
+ return {
23891
+ ...rest,
23892
+ lockedFields: known,
23893
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
23894
+ };
23525
23895
  }).meta({ id: "ManagedSettings" });
23526
23896
  var NO_MANAGED_CONTEXT = { present: false, lockedFields: [] };
23527
23897
 
23528
- // ../../packages/schema/src/zod/policy.ts
23529
- var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
23530
- var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
23531
- var Policy = external_exports.object({
23532
- id: external_exports.guid(),
23533
- scope: PolicyScope,
23534
- target: PolicyTarget,
23535
- action: ActionTaken,
23536
- enabled: external_exports.boolean().default(true),
23537
- customKeywords: external_exports.array(external_exports.string()).optional(),
23538
- // Display name — optional so older policy rows without name still parse.
23539
- // Added for the findings API (policy.name column migration).
23540
- name: external_exports.string().optional()
23541
- }).meta({ id: "Policy" });
23542
- var PolicyBundle = external_exports.object({
23543
- version: external_exports.string(),
23544
- policies: external_exports.array(Policy),
23545
- // Rules from the installed marketplace packs (snapshotted by the
23546
- // control plane). The plugin registers these in addition to its bundled
23547
- // packs. Optional so older backends — and older on-disk caches — that omit
23548
- // the field still parse; consumers read `bundle.rules ?? []`.
23549
- rules: external_exports.array(Rule).optional(),
23550
- // When true, `rules` IS the complete effective ruleset and the runtime must
23551
- // NOT merge its compiled-in bundled packs — the standalone gateway sets this
23552
- // after reading the user's installed snapshot (installed_packs, enabled
23553
- // packs only), which is how detection updates stay manual: new bundled
23554
- // rules run only after the user applies the pack update. Absent/false keeps
23555
- // the historical composition (bundled packs + rules) — older caches.
23556
- rulesComplete: external_exports.boolean().optional(),
23557
- // Active detection exceptions, evaluation subset only (see
23558
- // ExceptionBundleEntry). Optional so older bundle producers — and older
23559
- // on-disk caches — that omit the field still parse; consumers read
23560
- // `bundle.exceptions ?? []`.
23561
- exceptions: external_exports.array(ExceptionBundleEntry).optional(),
23562
- // Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
23563
- // A second axis over the same `redact` action, carried beside the policies
23564
- // rather than on them: nothing writes ruleId-targeted policies to disk, so
23565
- // widening Policy itself would change a persisted shape to express something
23566
- // only the in-memory bundle needs. Optional so an older producer — or an
23567
- // older on-disk cache — still parses; consumers read `?? []` and get the
23568
- // pre-existing one-way behaviour, which is the safe direction to default.
23569
- reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
23570
- // Installed pack version, keyed by ruleId, for rules in `rules` that came
23571
- // from a versioned installed pack. Optional so older backends — and older
23572
- // on-disk caches — that omit the field still parse; consumers fall back to
23573
- // the rule's own spec version. NOT the bundle version above — see
23574
- // installedRuleset's ruleVersions for the source of truth.
23575
- ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
23576
- // Model ids (the raw `model` string a harness reports, e.g.
23577
- // `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
23578
- // a session onto one (PreModelSwitch) and refuses a turn that would run on
23579
- // one (UserPromptSubmit). Optional so an older backend — and an older
23580
- // on-disk cache — still parses; consumers read `?? []`, which is the
23581
- // unenforced behaviour that predates this field and the safe direction to
23582
- // default.
23583
- //
23584
- // Ids, not display names: the governance decision is keyed on the exact
23585
- // string the harness reports (`model_status_override.versionId` in the
23586
- // control plane), so no name resolution stands between the decision and the
23587
- // comparison.
23588
- prohibitedModels: external_exports.array(external_exports.string()).optional(),
23589
- customKeywords: external_exports.array(external_exports.string()),
23590
- fetchedAt: external_exports.iso.datetime()
23591
- }).meta({ id: "PolicyBundle" });
23592
- var OBSERVE_ONLY_CATEGORIES = ["config"];
23593
- var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
23594
- var CATEGORY_PEAK_SEVERITY = {
23595
- secret: "critical",
23596
- financial: "critical",
23597
- // core-financial/credit-card
23598
- code_flaw: "critical",
23599
- pii: "high",
23600
- phi: "high",
23601
- custom: "high",
23602
- // user-defined; conservative
23603
- code_context: "low",
23604
- config: "low"
23605
- // observe-only; floors to monitor regardless
23606
- };
23607
- function severityFloorPolicy(category) {
23608
- if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
23609
- const peak = CATEGORY_PEAK_SEVERITY[category];
23610
- return peak === "critical" || peak === "high" ? "warn" : "monitor";
23611
- }
23612
- function severityFloorPosture() {
23613
- const out = {};
23614
- for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23615
- return out;
23616
- }
23617
- var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
23618
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23619
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23620
- var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23621
- var BUILTIN_POLICY_SPECS = {
23622
- monitor: {
23623
- name: "Monitor",
23624
- action: "log",
23625
- reversible: false,
23626
- description: "Log every match for audit. The request is allowed through untouched."
23627
- },
23628
- warn: {
23629
- name: "Warn",
23630
- action: "warn",
23631
- reversible: false,
23632
- description: "Allow the request, but warn the user inline before it is sent."
23633
- },
23634
- redact: {
23635
- name: "Redact",
23636
- action: "redact",
23637
- reversible: false,
23638
- description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
23639
- },
23640
- vault: {
23641
- name: "Redact & Vault",
23642
- action: "redact",
23643
- reversible: true,
23644
- 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."
23645
- },
23646
- block: {
23647
- name: "Block",
23648
- action: "block",
23649
- reversible: false,
23650
- description: "Refuse the request entirely whenever any rule in this detection matches."
23651
- }
23652
- };
23653
- function builtinPolicyToAction(id) {
23654
- return BUILTIN_POLICY_SPECS[id].action;
23655
- }
23656
- var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23657
- (id) => !BUILTIN_POLICY_SPECS[id].reversible
23658
- );
23659
- var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
23660
- (id) => BUILTIN_POLICY_SPECS[id].reversible
23661
- );
23662
- var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
23663
- function builtinPolicyIsReversible(id) {
23664
- return BUILTIN_POLICY_SPECS[id].reversible;
23665
- }
23666
- function policyIdIsReversible(policyId) {
23667
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23668
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23669
- return builtinPolicyIsReversible(id);
23670
- }
23671
- var DEFAULT_ACTIONS = Object.fromEntries(
23672
- DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
23673
- );
23674
- var BUILTIN_POLICIES = Object.fromEntries(
23675
- KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
23676
- );
23677
- var DEFAULT_PACK_POLICY_ID = "monitor";
23678
- function policyIdToAction(policyId) {
23679
- const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
23680
- const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
23681
- return BUILTIN_POLICIES[id].action;
23682
- }
23683
- var UsedByItem = external_exports.object({
23684
- id: external_exports.string(),
23685
- name: external_exports.string(),
23686
- ruleCount: external_exports.number().int().nonnegative(),
23687
- enabled: external_exports.boolean()
23688
- }).meta({ id: "UsedByItem" });
23689
- var PolicyListItem = external_exports.object({
23690
- id: external_exports.string(),
23691
- kind: PolicyKind,
23692
- name: external_exports.string(),
23693
- enabled: external_exports.boolean(),
23694
- usedByCount: external_exports.number().int().nonnegative()
23695
- }).meta({ id: "PolicyListItem" });
23696
- var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
23697
- var PolicyDetail = external_exports.object({
23698
- specVersion: external_exports.literal(1),
23699
- id: external_exports.string(),
23700
- kind: PolicyKind,
23701
- name: external_exports.string(),
23702
- enabled: external_exports.boolean(),
23703
- description: external_exports.string(),
23704
- usedBy: external_exports.array(UsedByItem)
23705
- }).meta({ id: "PolicyDetail" });
23706
- var PolicyStatsResponse = external_exports.object({
23707
- policies: external_exports.number().int().nonnegative(),
23708
- builtin: external_exports.number().int().nonnegative(),
23709
- custom: external_exports.number().int().nonnegative(),
23710
- detectionsGoverned: external_exports.number().int().nonnegative()
23711
- }).meta({ id: "PolicyStatsResponse" });
23712
-
23713
23898
  // ../../packages/schema/src/zod/project-files.ts
23714
23899
  var ProjectFileInput = external_exports.object({
23715
23900
  path: external_exports.string().min(1),
@@ -23831,7 +24016,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23831
24016
  timestamp: external_exports.iso.date(),
23832
24017
  critical: external_exports.number().int().nonnegative(),
23833
24018
  high: external_exports.number().int().nonnegative(),
23834
- medium: external_exports.number().int().nonnegative()
24019
+ medium: external_exports.number().int().nonnegative(),
24020
+ // Optional and additive, so a producer written against the earlier
24021
+ // three-series contract keeps validating. A consumer plotting it resolves the
24022
+ // absent case itself — the chart point requires a number.
24023
+ low: external_exports.number().int().nonnegative().optional()
23835
24024
  }).meta({ id: "FindingsTimeseriesPoint" });
23836
24025
  var FindingsTimeseriesResponse = external_exports.object({
23837
24026
  range: TimeRange,
@@ -23857,6 +24046,10 @@ var ResolvedFeedItem = external_exports.object({
23857
24046
  findingKey: external_exports.string(),
23858
24047
  ruleId: external_exports.string(),
23859
24048
  severity: Severity,
24049
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24050
+ // identifies the file: a bare path matches the same name in every repo.
24051
+ // Optional and additive; empty when the event carried no repo.
24052
+ repo: external_exports.string().optional(),
23860
24053
  path: external_exports.string(),
23861
24054
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
23862
24055
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -23955,10 +24148,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
23955
24148
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
23956
24149
 
23957
24150
  // ../../packages/schema/src/zod/settings-action.ts
24151
+ var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24152
+ var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
23958
24153
  var SaveSettingsInput = external_exports.object({
23959
24154
  historicalAccess: external_exports.string(),
23960
- modelJudgeConsent: external_exports.boolean(),
23961
- historySyncConsent: external_exports.boolean(),
24155
+ modelJudgeConsent: ModelJudgeConsentChoice,
24156
+ historySyncConsent: HistorySyncConsentChoice,
23962
24157
  vaultConsent: external_exports.string(),
23963
24158
  vaultInlineReveal: external_exports.string()
23964
24159
  });
@@ -24108,9 +24303,9 @@ function deriveReviewReasons(trust, transports) {
24108
24303
  if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
24109
24304
  return reasons;
24110
24305
  }
24111
- function buildReviewInfo(trust, transports) {
24306
+ function buildReviewInfo(trust, transports, decided) {
24112
24307
  const reasons = deriveReviewReasons(trust, transports);
24113
- return { needsReview: reasons.length > 0, reasons };
24308
+ return { needsReview: reasons.length > 0 && !decided, reasons };
24114
24309
  }
24115
24310
  function distinctTransports(transports) {
24116
24311
  return Array.from(new Set(transports));
@@ -24201,8 +24396,8 @@ function writeOwnerOnlyFileSync(file2, data) {
24201
24396
  }
24202
24397
 
24203
24398
  // ../../packages/persistence/src/database.ts
24204
- import { randomUUID as randomUUID10 } from "crypto";
24205
- import { join as join4, sep } from "path";
24399
+ import { randomUUID as randomUUID11 } from "crypto";
24400
+ import { dirname as dirname2, join as join7, sep } from "path";
24206
24401
  import { DatabaseSync } from "node:sqlite";
24207
24402
 
24208
24403
  // ../../packages/persistence/src/ids.ts
@@ -24446,6 +24641,10 @@ function allRows(stmt, params) {
24446
24641
  if (Array.isArray(params)) return stmt.all(...params);
24447
24642
  return stmt.all(params);
24448
24643
  }
24644
+ function* iterateRows(stmt, params) {
24645
+ const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
24646
+ for (const row of rows) yield row;
24647
+ }
24449
24648
  function getRow(stmt, params) {
24450
24649
  if (params === void 0) return stmt.get();
24451
24650
  if (Array.isArray(params)) return stmt.get(...params);
@@ -24914,10 +25113,17 @@ function ensureSyncedAtColumn(db, table2) {
24914
25113
  if (!columns.includes("sync_claimed_at")) {
24915
25114
  db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
24916
25115
  }
25116
+ if (!columns.includes("outbox_owed")) {
25117
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25118
+ }
24917
25119
  db.exec(
24918
25120
  `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
24919
25121
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
24920
25122
  );
25123
+ db.exec(
25124
+ `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25125
+ ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
25126
+ );
24921
25127
  db.exec(
24922
25128
  `CREATE INDEX IF NOT EXISTS idx_audit_claimed
24923
25129
  ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
@@ -25022,7 +25228,6 @@ function decodeKeysetCursor(cursor) {
25022
25228
  // ../../packages/persistence/src/repositories/activity.ts
25023
25229
  var DAY_MS = 864e5;
25024
25230
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
25025
- var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
25026
25231
  function defaultTimeZone() {
25027
25232
  try {
25028
25233
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -25077,6 +25282,7 @@ var DB_EVENT_TYPE_TO_KIND = {
25077
25282
  error: "error",
25078
25283
  active: "active"
25079
25284
  };
25285
+ var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
25080
25286
  function safeParseStringArray(raw) {
25081
25287
  if (!raw) return [];
25082
25288
  const parsed = safeJson(raw, null);
@@ -25150,6 +25356,37 @@ var TIMELINE_COLUMNS = `
25150
25356
  json_extract(attributes, '$.targetId') AS target_id,
25151
25357
  json_extract(attributes, '$.internal') AS internal,
25152
25358
  json_extract(attributes, '$.flagged') AS flagged`;
25359
+ var LLM_USAGE_SELECT = `
25360
+ SELECT root_session_id AS sessionId,
25361
+ provider,
25362
+ model,
25363
+ service_tier AS serviceTier,
25364
+ coalesce(sum(input_tokens), 0) AS inputTokens,
25365
+ coalesce(sum(output_tokens), 0) AS outputTokens,
25366
+ coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
25367
+ coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
25368
+ coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
25369
+ coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
25370
+ coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
25371
+ var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
25372
+ var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
25373
+ function usageLeaves(rows) {
25374
+ return rows.map((row) => {
25375
+ const attributes = {
25376
+ input_tokens: row.inputTokens,
25377
+ output_tokens: row.outputTokens,
25378
+ cache_creation_input_tokens: row.cacheCreationTokens,
25379
+ cache_read_input_tokens: row.cacheReadTokens,
25380
+ ephemeral_1h_input_tokens: row.ephemeral1hTokens,
25381
+ ephemeral_5m_input_tokens: row.ephemeral5mTokens,
25382
+ web_search_requests: row.webSearchRequests
25383
+ };
25384
+ if (row.provider !== null) attributes.provider = row.provider;
25385
+ if (row.model !== null) attributes.model = row.model;
25386
+ if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
25387
+ return { sessionId: row.sessionId, attributes };
25388
+ });
25389
+ }
25153
25390
  var SESSION_ROOT = `event_type = 'session'`;
25154
25391
  var HAS_ACTIVITY = `EXISTS (
25155
25392
  SELECT 1 FROM audit_events c
@@ -25175,16 +25412,17 @@ var SqliteActivityRepository = class {
25175
25412
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
25176
25413
  const liveNow = countScalar(
25177
25414
  this.db,
25178
- `SELECT count(*) AS n FROM audit_events s
25415
+ `SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
25179
25416
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
25180
- AND max(
25181
- s.started_at,
25182
- coalesce(
25183
- (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
25184
- s.started_at
25185
- )
25186
- ) >= ?`,
25187
- [liveThreshold]
25417
+ AND s.id IN (
25418
+ SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25419
+ UNION
25420
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25421
+ WHERE started_at >= ?
25422
+ UNION
25423
+ SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25424
+ WHERE ended_at >= ?)`,
25425
+ [liveThreshold, liveThreshold, liveThreshold]
25188
25426
  );
25189
25427
  const toolCallsToday = countScalar(
25190
25428
  this.db,
@@ -25237,7 +25475,8 @@ var SqliteActivityRepository = class {
25237
25475
  SELECT 1 FROM audit_events d
25238
25476
  WHERE d.root_session_id = audit_events.id
25239
25477
  AND (d.content LIKE ? ESCAPE '\\'
25240
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25478
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25479
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25241
25480
  );
25242
25481
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25243
25482
  }
@@ -25314,7 +25553,7 @@ var SqliteActivityRepository = class {
25314
25553
  this.db.prepare(
25315
25554
  `SELECT ${TIMELINE_COLUMNS}
25316
25555
  FROM audit_events
25317
- WHERE id = ? OR root_session_id = ?
25556
+ WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
25318
25557
  ORDER BY started_at ASC, id ASC`
25319
25558
  ),
25320
25559
  [sessionId, sessionId]
@@ -25327,14 +25566,14 @@ var SqliteActivityRepository = class {
25327
25566
  coalesce(sum(output_tokens), 0) AS output,
25328
25567
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
25329
25568
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
25330
- FROM audit_events
25569
+ FROM audit_events INDEXED BY idx_audit_session_type
25331
25570
  WHERE root_session_id = ? AND event_type = 'llm_call'`
25332
25571
  ),
25333
25572
  [sessionId]
25334
25573
  ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
25335
25574
  const primaryModel = getRow(
25336
25575
  this.db.prepare(
25337
- `SELECT model, provider FROM audit_events
25576
+ `SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
25338
25577
  WHERE root_session_id = ? AND event_type = 'llm_call'
25339
25578
  ORDER BY started_at ASC, id ASC
25340
25579
  LIMIT 1`
@@ -25345,7 +25584,7 @@ var SqliteActivityRepository = class {
25345
25584
  this.db.prepare(
25346
25585
  `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25347
25586
  count(*) AS n
25348
- FROM audit_events
25587
+ FROM audit_events INDEXED BY idx_audit_session
25349
25588
  WHERE root_session_id = ? AND event_type = 'tool_call'
25350
25589
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
25351
25590
  ),
@@ -25353,7 +25592,7 @@ var SqliteActivityRepository = class {
25353
25592
  );
25354
25593
  const modelRows = allRows(
25355
25594
  this.db.prepare(
25356
- `SELECT DISTINCT model FROM audit_events
25595
+ `SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
25357
25596
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
25358
25597
  ORDER BY model`
25359
25598
  ),
@@ -25362,7 +25601,7 @@ var SqliteActivityRepository = class {
25362
25601
  const derivedModels = modelRows.map((r) => r.model);
25363
25602
  const commits = countScalar(
25364
25603
  this.db,
25365
- `SELECT count(*) AS n FROM audit_events
25604
+ `SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
25366
25605
  WHERE root_session_id = ? AND event_type = 'commit'`,
25367
25606
  [sessionId]
25368
25607
  );
@@ -25398,25 +25637,57 @@ var SqliteActivityRepository = class {
25398
25637
  return Promise.resolve(session);
25399
25638
  }
25400
25639
  /**
25401
- * Cross-session token report — every `llm_call` leaf (optionally windowed to
25402
- * `started_at >= fromMs`) grouped into per-session `SessionTokenReport`s, with
25403
- * USD cost DERIVED at read time via the shared `defaultCostModel` (never
25404
- * stored). `fromMs` lets the Activity page scope the usage panel to its
25405
- * selected time range; omit it for all-time (the CLI/TUI overview). The
25406
- * caller collapses these onto per-model rows with `aggregateTokenUsage`.
25640
+ * Cross-session token report — every `llm_call` in the store (or in a
25641
+ * `started_at >= fromMs` window, the Activity page's range) grouped per
25642
+ * session, with USD cost DERIVED at read time via the shared
25643
+ * `defaultCostModel` (never stored). The caller collapses these onto
25644
+ * per-model rows with `aggregateTokenUsage`.
25645
+ *
25646
+ * Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
25647
+ * the members the rollup sums — and priced once per group, which is exact
25648
+ * (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
25649
+ * for a seven-day window at 50k calls, and naming the VIRTUAL columns
25650
+ * against the table 40 ms, since each is a json_extract recomputed per row;
25651
+ * the index stores the values once, at write, and answers the same window in
25652
+ * 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
25653
+ * planner prefers the general event-type index and fetches every row to
25654
+ * recompute the columns it could have read. The index is one every open
25655
+ * store carries, since opening runs the migrations, so the hard requirement
25656
+ * `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
25657
+ * the plan. All-time is a scan of the whole index — still one narrow entry
25658
+ * per call, no bag parsed.
25407
25659
  */
25408
25660
  tokenReports(fromMs) {
25409
- const leaves = this.readLlmCallLeaves(fromMs === void 0 ? {} : { fromMs });
25410
- return Promise.resolve(buildTokenReports(leaves, defaultCostModel));
25661
+ const rows = allRows(
25662
+ this.db.prepare(
25663
+ `${LLM_USAGE_SELECT}
25664
+ FROM audit_events INDEXED BY idx_audit_llm_usage
25665
+ WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
25666
+ ${LLM_USAGE_GROUP}`
25667
+ ),
25668
+ fromMs === void 0 ? void 0 : [fromMs]
25669
+ );
25670
+ return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
25411
25671
  }
25412
25672
  /**
25413
- * One session's token report — its `llm_call` leaves grouped per (provider,
25414
- * model) with derived cost, or `null` when the session made no `llm_call`s
25415
- * (an empty/tool-only session). Feeds the session-detail pane's per-model
25416
- * breakdown + estimated cost.
25673
+ * One session's token report — its `llm_call`s grouped per (provider,
25674
+ * model, tier) with derived cost, or `null` when the session made no
25675
+ * `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
25676
+ * per-model breakdown + estimated cost. The same rollup as `tokenReports`,
25677
+ * seeking one root through a root-led `llm_call` index; the bag-reading fold
25678
+ * it replaces walked every `llm_call` in the store to find one session's.
25417
25679
  */
25418
25680
  tokenReportForSession(sessionId) {
25419
- const reports = buildTokenReports(this.readLlmCallLeaves({ sessionId }), defaultCostModel);
25681
+ const rows = allRows(
25682
+ this.db.prepare(
25683
+ `${LLM_USAGE_SELECT}
25684
+ FROM audit_events
25685
+ WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
25686
+ ${LLM_USAGE_GROUP}`
25687
+ ),
25688
+ [sessionId]
25689
+ );
25690
+ const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
25420
25691
  return Promise.resolve(reports[0] ?? null);
25421
25692
  }
25422
25693
  /**
@@ -25440,42 +25711,6 @@ var SqliteActivityRepository = class {
25440
25711
  for (const row of rows) seen.add(toHarness(row.harness));
25441
25712
  return Promise.resolve([...seen]);
25442
25713
  }
25443
- /**
25444
- * The raw `llm_call` leaves (session id + parsed attribute bag) for the token
25445
- * rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
25446
- * window. A leaf whose attributes blob is NULL or unparseable is skipped
25447
- * (best-effort read — a corrupt bag never breaks the report). `root_session_id`
25448
- * is the leaf's session (the reconciler sets parent_id = root_session_id).
25449
- */
25450
- readLlmCallLeaves(opts = {}) {
25451
- const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
25452
- const params = [];
25453
- if (opts.sessionId !== void 0) {
25454
- conditions.push("root_session_id = ?");
25455
- params.push(opts.sessionId);
25456
- }
25457
- if (opts.fromMs !== void 0) {
25458
- conditions.push("started_at >= ?");
25459
- params.push(opts.fromMs);
25460
- }
25461
- const rows = allRows(
25462
- this.db.prepare(
25463
- `SELECT root_session_id AS sessionId, attributes
25464
- FROM audit_events
25465
- WHERE ${conditions.join(" AND ")}`
25466
- ),
25467
- params
25468
- );
25469
- return mapRowsTolerant(
25470
- rows.filter(
25471
- (row) => row.sessionId !== null
25472
- ),
25473
- (row) => ({
25474
- sessionId: row.sessionId,
25475
- attributes: JSON.parse(row.attributes)
25476
- })
25477
- );
25478
- }
25479
25714
  /**
25480
25715
  * Per-session turns/findings/shares + last-activity for a page of session ids,
25481
25716
  * in grouped queries (not one per row). An id with no matching rows still
@@ -25490,20 +25725,23 @@ var SqliteActivityRepository = class {
25490
25725
  const inClause = placeholders(sessionIds.length);
25491
25726
  const lastActivityRows = allRows(
25492
25727
  this.db.prepare(
25493
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
25494
- WHERE root_session_id IN (${inClause})
25495
- GROUP BY root_session_id`
25728
+ `SELECT ids.value AS id,
25729
+ (SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
25730
+ (SELECT max(ended_at) FROM audit_events e
25731
+ WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
25732
+ FROM json_each(?) AS ids`
25496
25733
  ),
25497
- sessionIds
25734
+ [JSON.stringify(sessionIds)]
25498
25735
  );
25499
25736
  for (const row of lastActivityRows) {
25500
- if (row.id === null) continue;
25501
25737
  const entry = result.get(row.id);
25502
- if (entry && row.m !== null) entry.lastActivityMs = row.m;
25738
+ const last = Math.max(row.ms ?? 0, row.me ?? 0);
25739
+ if (entry && last > 0) entry.lastActivityMs = last;
25503
25740
  }
25504
25741
  const turnsRows = allRows(
25505
25742
  this.db.prepare(
25506
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
25743
+ `SELECT root_session_id AS id, count(*) AS n
25744
+ FROM audit_events INDEXED BY idx_audit_session_prompt
25507
25745
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
25508
25746
  GROUP BY root_session_id`
25509
25747
  ),
@@ -25518,7 +25756,7 @@ var SqliteActivityRepository = class {
25518
25756
  this.db.prepare(
25519
25757
  `SELECT root_session_id AS id,
25520
25758
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
25521
- FROM audit_events
25759
+ FROM audit_events INDEXED BY idx_audit_session_run_key
25522
25760
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
25523
25761
  AND json_extract(attributes, '$.run_key') IS NOT NULL
25524
25762
  GROUP BY root_session_id`
@@ -25548,7 +25786,7 @@ var SqliteActivityRepository = class {
25548
25786
  this.db.prepare(
25549
25787
  `SELECT root_session_id AS id,
25550
25788
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
25551
- FROM audit_events
25789
+ FROM audit_events INDEXED BY idx_audit_session_share
25552
25790
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
25553
25791
  GROUP BY root_session_id`
25554
25792
  ),
@@ -26576,24 +26814,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26576
26814
  )`;
26577
26815
 
26578
26816
  // ../../packages/persistence/src/repositories/findings.ts
26579
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26580
- var SCAN_BATCH_ROWS = 1e3;
26581
- var DEFAULT_LOCATIONS_LIMIT = 100;
26582
- var LOCATION_RULE_IDS_CAP = 20;
26583
- function compareLocationOrder(a, b) {
26584
- return compareFindingGroupOrder(
26585
- {
26586
- severity: a.maxSeverity,
26587
- latestDetectedAt: a.latestDetectedAt,
26588
- id: ""
26589
- },
26590
- {
26591
- severity: b.maxSeverity,
26592
- latestDetectedAt: b.latestDetectedAt,
26593
- id: ""
26594
- }
26595
- );
26596
- }
26597
26817
  var CONCAT_SEP = ",";
26598
26818
  var TUPLE_SEP = "|";
26599
26819
  function splitConcat(value) {
@@ -26606,6 +26826,25 @@ function deriveInstanceStatus(row) {
26606
26826
  latestResolutionStatus: row.latest_status
26607
26827
  });
26608
26828
  }
26829
+ function toFlatFindingRow(r) {
26830
+ return {
26831
+ id: r.id,
26832
+ ruleId: r.rule_id,
26833
+ category: r.category,
26834
+ severity: r.severity,
26835
+ maskedMatch: r.masked_match,
26836
+ actionTaken: r.action_taken,
26837
+ confidence: r.confidence,
26838
+ occurredAt: epochMillisToIso(r.occurred_at),
26839
+ sourceTool: r.source_tool,
26840
+ repo: r.repo ?? "",
26841
+ file: r.file ?? "",
26842
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
26843
+ eventId: r.event_id,
26844
+ ...r.session_id === null ? {} : { sessionId: r.session_id },
26845
+ status: deriveInstanceStatus(r)
26846
+ };
26847
+ }
26609
26848
  function encodeGroupCursor(group) {
26610
26849
  const payload = {
26611
26850
  sev: group.severity,
@@ -26626,13 +26865,48 @@ function decodeGroupCursor(cursor) {
26626
26865
  return null;
26627
26866
  }
26628
26867
  function firstAfter(sorted, cursor) {
26629
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
26868
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26630
26869
  return index === -1 ? sorted.length : index;
26631
26870
  }
26632
26871
  function findDeepLinked(sorted, page, id) {
26633
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26634
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
26872
+ if (page.some((t) => t.id === id)) return void 0;
26873
+ return sorted.find((t) => t.id === id);
26874
+ }
26875
+ function encodeLocationCursor(location) {
26876
+ const payload = {
26877
+ sev: location.maxSeverity,
26878
+ t: location.latestDetectedAt,
26879
+ r: location.repo,
26880
+ f: location.file
26881
+ };
26882
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
26883
+ }
26884
+ function decodeLocationCursor(cursor) {
26885
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
26886
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
26887
+ return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
26888
+ }
26889
+ return null;
26890
+ }
26891
+ function firstLocationAfter(sorted, cursor) {
26892
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
26893
+ return index === -1 ? sorted.length : index;
26894
+ }
26895
+ function findDeepLinkedLocation(sorted, page, id) {
26896
+ if (page.some((l) => l.id === id)) return void 0;
26897
+ return sorted.find((l) => l.id === id);
26635
26898
  }
26899
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
26900
+ d.severity AS severity, f.masked_match AS masked_match,
26901
+ f.action_taken AS action_taken, f.confidence AS confidence,
26902
+ e.started_at AS occurred_at,
26903
+ e.source_tool AS source_tool,
26904
+ e.repo AS repo,
26905
+ e.file_path AS file,
26906
+ e.tool_name AS tool_name,
26907
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
26908
+ e.event_type AS kind, f.finding_key AS finding_key,
26909
+ ${latestResolutionStatusSql("f")} AS latest_status`;
26636
26910
  var DAY_MS3 = 864e5;
26637
26911
  var SqliteFindingsRepository = class {
26638
26912
  constructor(db) {
@@ -26681,7 +26955,7 @@ var SqliteFindingsRepository = class {
26681
26955
  this.db.prepare(
26682
26956
  `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
26683
26957
  f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
26684
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26958
+ e.source_tool AS source_tool,
26685
26959
  e.event_type AS kind
26686
26960
  FROM audit_events e
26687
26961
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
@@ -26753,30 +27027,26 @@ var SqliteFindingsRepository = class {
26753
27027
  );
26754
27028
  }
26755
27029
  /**
26756
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
26757
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
26758
- * attributes bag, rule_id/category/severity from the definition), scoped to
26759
- * the four capture kinds (audit_events also holds structural/reconciler/scan
26760
- * rows this list must never surface), groups by ruleId, computes
26761
- * per-filter-excluded facets, applies the requested filters, and sorts by
26762
- * severity then recency. Filtering
26763
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
26764
- * reflect the full filtered set; `items` is the requested
26765
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
26766
- * filter, `totals.findings` counts only instances whose derived status was
26767
- * requested, and each item's instance preview is narrowed the same way.
27030
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27031
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27032
+ * list must never surface), with per-filter-excluded facets, the requested
27033
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27034
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27035
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27036
+ * Under a `status` filter, `totals.findings` counts only findings whose
27037
+ * derived status was requested.
27038
+ *
27039
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27040
+ * folding EVERY finding into the numbers a type row and the filters need
27041
+ * (count, severity, category, providers, actions, statuses, latest, search
27042
+ * text). The findings OF a type come from listFindingInstances scoped to
27043
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
26768
27044
  *
26769
- * Two reads, neither of which materializes a row per finding:
26770
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
26771
- * the group and the filters need (count, providers, actions, statuses,
26772
- * latest, search text);
26773
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
26774
- * populate `instances` for the table's expanded rows.
26775
27045
  * The aggregates carry raw DB values and are translated by the same
26776
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
26777
- * rule is ever restated in SQL.
27046
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27047
+ * status rule is ever restated in SQL.
26778
27048
  */
26779
- listGroupedFindings(query) {
27049
+ listFindingTypes(query) {
26780
27050
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
26781
27051
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
26782
27052
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -26789,57 +27059,7 @@ var SqliteFindingsRepository = class {
26789
27059
  predicate,
26790
27060
  params: sessionParams
26791
27061
  });
26792
- const rows = allRows(
26793
- this.db.prepare(
26794
- `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
26795
- occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
26796
- kind, finding_key, latest_status
26797
- FROM (
26798
- SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
26799
- d.severity AS severity, f.masked_match AS masked_match,
26800
- f.action_taken AS action_taken, f.confidence AS confidence,
26801
- e.started_at AS occurred_at,
26802
- json_extract(e.attributes, '$.source_tool') AS source_tool,
26803
- json_extract(e.attributes, '$.repo') AS repo,
26804
- json_extract(e.attributes, '$.file_path') AS file,
26805
- json_extract(e.attributes, '$.tool_name') AS tool_name,
26806
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
26807
- e.event_type AS kind, f.finding_key AS finding_key,
26808
- latest.status AS latest_status,
26809
- ROW_NUMBER() OVER (
26810
- PARTITION BY d.rule_id
26811
- ORDER BY e.started_at DESC, f.id DESC
26812
- ) AS rn
26813
- FROM inspection_findings f
26814
- JOIN audit_events e ON e.id = f.audit_event_id
26815
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
26816
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
26817
- ON latest.finding_key = f.finding_key
26818
- ${predicate}
26819
- )
26820
- WHERE rn <= :cap
26821
- ORDER BY occurred_at DESC, id DESC`
26822
- ),
26823
- { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
26824
- );
26825
- const groupable = rows.map((r) => ({
26826
- id: r.id,
26827
- ruleId: r.rule_id,
26828
- category: r.category,
26829
- severity: r.severity,
26830
- maskedMatch: r.masked_match,
26831
- actionTaken: r.action_taken,
26832
- confidence: r.confidence,
26833
- occurredAt: epochMillisToIso(r.occurred_at),
26834
- sourceTool: r.source_tool,
26835
- repo: r.repo ?? "",
26836
- file: r.file ?? "",
26837
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
26838
- eventId: r.event_id,
26839
- ...r.session_id === null ? {} : { sessionId: r.session_id },
26840
- status: deriveInstanceStatus(r)
26841
- }));
26842
- const allGroups = buildFindingGroups(groupable, { aggregates });
27062
+ const allTypes = buildFindingTypes(aggregates);
26843
27063
  const filterOpts = {
26844
27064
  severity: query.severity,
26845
27065
  providers: query.provider,
@@ -26848,30 +27068,25 @@ var SqliteFindingsRepository = class {
26848
27068
  subtype: query.subtype,
26849
27069
  q: query.q
26850
27070
  };
26851
- const facets = computeFindingFacets(allGroups, filterOpts);
26852
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27071
+ const facets = computeFindingFacets(allTypes, filterOpts);
27072
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
26853
27073
  const statusFilter = query.status ?? [];
26854
27074
  const totals = {
26855
- findings: sorted.reduce((acc, g) => {
26856
- if (statusFilter.length === 0) return acc + g.instanceCount;
26857
- const agg = aggregates.get(g.id);
26858
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27075
+ findings: sorted.reduce((acc, t) => {
27076
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27077
+ const agg = aggregates.get(t.id);
27078
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
26859
27079
  }, 0),
26860
- groups: sorted.length
27080
+ types: sorted.length
26861
27081
  };
26862
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27082
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
26863
27083
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
26864
27084
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
26865
27085
  const page = sorted.slice(start, start + limit);
26866
27086
  const lastOnPage = page.at(-1);
26867
27087
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
26868
27088
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
26869
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
26870
- const narrow = (g) => statusSet ? {
26871
- ...g,
26872
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
26873
- } : g;
26874
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27089
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
26875
27090
  return Promise.resolve({
26876
27091
  totals,
26877
27092
  facets,
@@ -26882,7 +27097,7 @@ var SqliteFindingsRepository = class {
26882
27097
  }
26883
27098
  /**
26884
27099
  * One row per rule_id, folding EVERY instance of the group into the values
26885
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27100
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
26886
27101
  * distinct rule_ids (the installed packs' rules), not by the store's size.
26887
27102
  *
26888
27103
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -26924,8 +27139,10 @@ var SqliteFindingsRepository = class {
26924
27139
  *
26925
27140
  * The scan runs from the top of the scope on every request, not from the
26926
27141
  * cursor: `totals` and `facets` describe the whole filtered scope and must not
26927
- * move as the caller pages. Rows are pulled in batches so memory stays flat
26928
- * while the counting runs, and only the page itself is retained.
27142
+ * move as the caller pages. Rows come off ONE statement, iterated rather
27143
+ * than materialized (`scanFindingRows`), so memory stays flat while the
27144
+ * counting runs — a generator streaming the index order, not a sequence of
27145
+ * fetched batches; only the page itself is retained.
26929
27146
  */
26930
27147
  listFindingInstances(query) {
26931
27148
  const opts = {
@@ -26941,6 +27158,10 @@ var SqliteFindingsRepository = class {
26941
27158
  };
26942
27159
  const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
26943
27160
  const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
27161
+ const isPastCursor = cursor === null ? () => true : (row) => {
27162
+ const rowMs = isoToEpochMillis(row.occurredAt);
27163
+ return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
27164
+ };
26944
27165
  const accumulator = createInstanceFacetAccumulator(opts);
26945
27166
  const items = [];
26946
27167
  let total = 0;
@@ -26953,6 +27174,7 @@ var SqliteFindingsRepository = class {
26953
27174
  accumulator.add(row);
26954
27175
  if (!matchesInstanceFilters(row, opts)) continue;
26955
27176
  total += 1;
27177
+ if (!isPastCursor(row)) continue;
26956
27178
  if (items.length < limit) {
26957
27179
  items.push(toInstanceDetail(row));
26958
27180
  last = row;
@@ -26961,15 +27183,6 @@ var SqliteFindingsRepository = class {
26961
27183
  }
26962
27184
  }
26963
27185
  const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
26964
- if (cursor !== null) {
26965
- const resumed = this.pageAfter(cursor, opts, limit, query);
26966
- return Promise.resolve({
26967
- totals: { findings: total },
26968
- facets: accumulator.facets(),
26969
- items: resumed.items,
26970
- nextCursor: resumed.nextCursor
26971
- });
26972
- }
26973
27186
  return Promise.resolve({
26974
27187
  totals: { findings: total },
26975
27188
  facets: accumulator.facets(),
@@ -26978,42 +27191,25 @@ var SqliteFindingsRepository = class {
26978
27191
  });
26979
27192
  }
26980
27193
  /**
26981
- * The page of matching rows strictly after `cursor`. Separate from the
26982
- * counting pass because that one starts at the top of the scope by design;
26983
- * this one narrows the scan with the same keyset predicate the activity list
26984
- * uses, so a later page costs less than the first rather than more.
26985
- */
26986
- pageAfter(cursor, opts, limit, query) {
26987
- const items = [];
26988
- let last;
26989
- let hasMore = false;
26990
- for (const row of this.scanFindingRows({
26991
- sessionId: query.sessionId,
26992
- from: query.from,
26993
- after: cursor
26994
- })) {
26995
- if (!matchesInstanceFilters(row, opts)) continue;
26996
- if (items.length < limit) {
26997
- items.push(toInstanceDetail(row));
26998
- last = row;
26999
- } else {
27000
- hasMore = true;
27001
- break;
27002
- }
27003
- }
27004
- return {
27005
- items,
27006
- nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
27007
- };
27008
- }
27009
- /**
27010
- * The same findings folded by location: repository, then file within it.
27194
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27011
27195
  *
27012
27196
  * The grouping keys come from the capturing event's attributes, which is what
27013
- * the local store relates a finding to — there is no finding↔asset row to
27014
- * group by instead. A repo or file the event did not record folds into the
27015
- * empty-string bucket, which the view renders but does not link, since no
27016
- * filter can name it.
27197
+ * the local store relates a finding to; there is no finding↔asset row to group
27198
+ * by instead. A repo or file the event did not record folds into the
27199
+ * empty-string bucket, which is a real location like any other: it is listed,
27200
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27201
+ *
27202
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27203
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27204
+ * list was rebuilt to remove — and two-level pagination inside an
27205
+ * expand/collapse table is what pushed that view to master/detail in the first
27206
+ * place.
27207
+ *
27208
+ * Every filter narrows the FINDINGS and the locations fall out of what
27209
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27210
+ * reports for the same filters scoped to that pair. The view depends on it:
27211
+ * one toolbar sits over both panels precisely because a location owns none of
27212
+ * its fields.
27017
27213
  */
27018
27214
  listFindingLocations(query) {
27019
27215
  const opts = {
@@ -27025,13 +27221,16 @@ var SqliteFindingsRepository = class {
27025
27221
  tools: query.tool,
27026
27222
  q: query.q
27027
27223
  };
27028
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27224
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27225
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27029
27226
  const byRepo = /* @__PURE__ */ new Map();
27227
+ const accumulator = createInstanceFacetAccumulator(opts);
27030
27228
  let total = 0;
27031
27229
  for (const row of this.scanFindingRows({
27032
27230
  sessionId: query.sessionId,
27033
27231
  from: query.from
27034
27232
  })) {
27233
+ accumulator.add(row);
27035
27234
  if (!matchesInstanceFilters(row, opts)) continue;
27036
27235
  total += 1;
27037
27236
  let files = byRepo.get(row.repo);
@@ -27046,67 +27245,112 @@ var SqliteFindingsRepository = class {
27046
27245
  }
27047
27246
  addToLocation(acc, row);
27048
27247
  }
27049
- let fileCount = 0;
27050
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27051
- fileCount += files.size;
27052
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27053
- file: file2,
27054
- instanceCount: acc.instanceCount,
27055
- maxSeverity: acc.maxSeverity,
27056
- latestDetectedAt: acc.latestDetectedAt,
27057
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27058
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27059
- })).sort(compareLocationOrder);
27060
- const rollup = fileRows.reduce(
27061
- (a, f) => ({
27062
- instanceCount: a.instanceCount + f.instanceCount,
27063
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27064
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27065
- }),
27066
- {
27067
- instanceCount: 0,
27068
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27069
- latestDetectedAt: ""
27070
- }
27071
- );
27072
- const statuses = fileRows.map((f) => f.status);
27073
- const folded = foldGroupStatus(statuses);
27074
- return {
27075
- repo,
27076
- instanceCount: rollup.instanceCount,
27077
- maxSeverity: rollup.maxSeverity,
27078
- latestDetectedAt: rollup.latestDetectedAt,
27079
- ...folded === void 0 ? {} : { status: folded },
27080
- files: fileRows
27081
- };
27082
- });
27083
- repos.sort(compareLocationOrder);
27248
+ const sorted = [];
27249
+ for (const [repo, files] of byRepo) {
27250
+ for (const [file2, acc] of files) {
27251
+ const status = foldGroupStatus(acc.statuses);
27252
+ sorted.push({
27253
+ id: encodeLocationId(repo, file2),
27254
+ repo,
27255
+ file: file2,
27256
+ instanceCount: acc.instanceCount,
27257
+ maxSeverity: acc.maxSeverity,
27258
+ latestDetectedAt: acc.latestDetectedAt,
27259
+ ...status === void 0 ? {} : { status },
27260
+ ruleIds: [...acc.ruleIds]
27261
+ });
27262
+ }
27263
+ }
27264
+ sorted.sort(compareLocationOrder);
27265
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27266
+ const page = sorted.slice(start, start + limit);
27267
+ const lastOnPage = page.at(-1);
27268
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27269
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27084
27270
  return Promise.resolve({
27085
- totals: { findings: total, repos: repos.length, files: fileCount },
27086
- items: repos.slice(0, limit),
27087
- hasMore: repos.length > limit
27271
+ totals: { findings: total, locations: sorted.length },
27272
+ facets: accumulator.facets(),
27273
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27274
+ nextCursor
27088
27275
  });
27089
27276
  }
27090
27277
  /**
27091
- * Every finding in scope as a FlatFindingRow, newest first, pulled in batches.
27278
+ * Every finding in scope as a FlatFindingRow, newest first, streamed.
27092
27279
  *
27093
27280
  * A generator so a caller streams the scope without it ever being an array:
27094
27281
  * the flat list counts and facets the whole filtered scope, which on a large
27095
- * store is far more rows than any page. Each batch advances the same keyset
27096
- * predicate the page read uses, so the scan is a sequence of bounded reads
27097
- * rather than one unbounded result set.
27282
+ * store is far more rows than any page. The rows come off ONE statement,
27283
+ * iterated rather than materialized, in the index order `findingScanSql`
27284
+ * arranges — so the scan is a single pass with a block sort of the id
27285
+ * tie-break only, never a sort of the scope, where a sequence of
27286
+ * keyset-bounded batches re-sorted everything below the cursor on every
27287
+ * batch and cost the square of the scope.
27098
27288
  *
27099
- * The latest-resolution lookup is the CORRELATED form, not the derived table
27100
- * the grouped path joins: only `status` is needed, idx_finding_resolution_key
27101
- * makes it a point lookup per row, and the derived table would re-materialize
27102
- * a window over the whole resolution table once per batch.
27103
- *
27104
- * `scope` carries ONLY what no facet counts. A filter dimension narrowed here
27105
- * would be missing from its own facet, which is computed by excluding that
27106
- * dimension — see listFindingInstances.
27289
+ * `sessionId` and `from` carry ONLY what no facet counts — a filter
27290
+ * dimension narrowed here would be missing from its own facet, which is
27291
+ * computed by excluding that dimension (see listFindingInstances). There is
27292
+ * no `after`/cursor parameter: a keyset page is collected inline from this
27293
+ * same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
27294
+ * narrower statement, since the counting pass already visits every row a
27295
+ * page-2+ request would otherwise re-seek for.
27107
27296
  */
27108
27297
  *scanFindingRows(scope) {
27109
- const conditions = [`e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27298
+ const { sql, params } = this.findingScanSql(scope);
27299
+ for (const r of iterateRows(this.db.prepare(sql), params)) {
27300
+ yield toFlatFindingRow(r);
27301
+ }
27302
+ }
27303
+ /**
27304
+ * One finding by its own id, or null when no such row exists.
27305
+ *
27306
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27307
+ * the store — and, unlike anything derived from a list page, it resolves a
27308
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27309
+ * deep link needs: the id it carries may name a finding thousands of rows
27310
+ * older than anything a first page holds.
27311
+ *
27312
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27313
+ * RESOLVES an id; whether that row would survive the list's current filters is
27314
+ * a different question, and hiding the target because a filter excludes it is
27315
+ * worse than showing it.
27316
+ *
27317
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27318
+ * type should the list select?" and "what does the drawer show?".
27319
+ */
27320
+ findingInstance(id) {
27321
+ const row = this.db.prepare(
27322
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27323
+ FROM inspection_findings f
27324
+ JOIN audit_events e ON e.id = f.audit_event_id
27325
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27326
+ WHERE f.id = ?`
27327
+ ).get(id);
27328
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27329
+ }
27330
+ /**
27331
+ * The one statement both instance-level scans run: every finding in scope,
27332
+ * joined to its event and definition, newest first.
27333
+ *
27334
+ * THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
27335
+ * the same two `recentFindings` documents at length, for the same reason:
27336
+ *
27337
+ * - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
27338
+ * the planner cannot pick `idx_audit_type_t` and then sort. That index
27339
+ * yields `started_at` order per event type, not across the four, so
27340
+ * satisfying the ORDER BY from it would need a merge SQLite does not do.
27341
+ * Freed of it, the planner walks `idx_audit_started_at` backwards — or
27342
+ * `idx_audit_session` for a session scope, which is also `started_at`
27343
+ * ordered within the session — and the order falls out of the index.
27344
+ * - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
27345
+ * JOINs the planner drives from the findings and sorts everything.
27346
+ *
27347
+ * The latest-resolution lookup is the CORRELATED form: only `status` is
27348
+ * needed, `idx_finding_resolution_key_created` answers it with one backward
27349
+ * index probe per keyed row, and a derived table over the whole resolution
27350
+ * table would be materialized before the first row streamed.
27351
+ */
27352
+ findingScanSql(scope) {
27353
+ const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
27110
27354
  const params = [];
27111
27355
  if (scope.sessionId !== void 0 && scope.sessionId !== "") {
27112
27356
  conditions.push("e.root_session_id = ?");
@@ -27116,64 +27360,40 @@ var SqliteFindingsRepository = class {
27116
27360
  conditions.push("e.started_at >= ?");
27117
27361
  params.push(isoToEpochMillis(scope.from));
27118
27362
  }
27119
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27120
- d.severity AS severity, f.masked_match AS masked_match,
27121
- f.action_taken AS action_taken, f.confidence AS confidence,
27122
- e.started_at AS occurred_at,
27123
- json_extract(e.attributes, '$.source_tool') AS source_tool,
27124
- json_extract(e.attributes, '$.repo') AS repo,
27125
- json_extract(e.attributes, '$.file_path') AS file,
27126
- json_extract(e.attributes, '$.tool_name') AS tool_name,
27127
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27128
- e.event_type AS kind, f.finding_key AS finding_key,
27129
- ${latestResolutionStatusSql("f")} AS latest_status
27130
- FROM inspection_findings f
27131
- JOIN audit_events e ON e.id = f.audit_event_id
27132
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27363
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27364
+ FROM audit_events e
27365
+ CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27366
+ CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27133
27367
  WHERE ${conditions.join(" AND ")}
27134
- AND (e.started_at < ? OR (e.started_at = ? AND f.id < ?))
27135
- ORDER BY e.started_at DESC, f.id DESC
27136
- LIMIT ?`;
27137
- let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
27138
- for (; ; ) {
27139
- const rows = allRows(this.db.prepare(sql), [
27140
- ...params,
27141
- after.startedAtMs,
27142
- after.startedAtMs,
27143
- after.id,
27144
- SCAN_BATCH_ROWS
27145
- ]);
27146
- for (const r of rows) {
27147
- yield {
27148
- id: r.id,
27149
- ruleId: r.rule_id,
27150
- category: r.category,
27151
- severity: r.severity,
27152
- maskedMatch: r.masked_match,
27153
- actionTaken: r.action_taken,
27154
- confidence: r.confidence,
27155
- occurredAt: epochMillisToIso(r.occurred_at),
27156
- sourceTool: r.source_tool,
27157
- repo: r.repo ?? "",
27158
- file: r.file ?? "",
27159
- ...r.tool_name === null ? {} : { toolName: r.tool_name },
27160
- eventId: r.event_id,
27161
- ...r.session_id === null ? {} : { sessionId: r.session_id },
27162
- status: deriveInstanceStatus(r)
27163
- };
27164
- }
27165
- if (rows.length < SCAN_BATCH_ROWS) return;
27166
- const lastRow = rows[rows.length - 1];
27167
- if (lastRow === void 0) return;
27168
- after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
27169
- }
27368
+ ORDER BY e.started_at DESC, f.id DESC`;
27369
+ return { sql, params };
27170
27370
  }
27171
27371
  groupAggregates(withSearchText, scope) {
27172
- const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
27173
- group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
27174
- group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27372
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
27373
+ group_concat(DISTINCT e.file_path) AS files,
27374
+ group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27175
27375
  const rows = this.db.prepare(
27176
27376
  `SELECT rule_id,
27377
+ -- BARE columns beside max(latest_at), which is deliberate and
27378
+ -- is SQLite's documented behaviour: with a single min()/max()
27379
+ -- in an aggregate query, every bare column takes its value from
27380
+ -- the row that produced the extremum. So these are the severity
27381
+ -- and category of the definition whose finding is NEWEST, which
27382
+ -- is what the row-based build they replaced read off its first
27383
+ -- (newest-first) row.
27384
+ --
27385
+ -- min() is WRONG here and was the defect: inspection_definitions
27386
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27387
+ -- mints a new row), so a rule whose severity moved between
27388
+ -- versions has several, and min() picks the ALPHABETICALLY
27389
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27390
+ -- That is arbitrary in direction, and it feeds the badge, the
27391
+ -- filter, the facet counts and the primary sort key.
27392
+ --
27393
+ -- Adding a second min()/max() aggregate here would make these
27394
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27395
+ severity,
27396
+ category,
27177
27397
  sum(tuple_count) AS instance_count,
27178
27398
  max(latest_at) AS latest_at,
27179
27399
  group_concat(source_tools) AS source_tools,
@@ -27184,12 +27404,20 @@ var SqliteFindingsRepository = class {
27184
27404
  group_concat(tool_names) AS tool_names
27185
27405
  FROM (
27186
27406
  SELECT d.rule_id AS rule_id,
27407
+ -- Severity and category are columns of the DEFINITION, and
27408
+ -- a rule can have SEVERAL definitions (one per version), so
27409
+ -- these are grouped on below and resolved to the newest
27410
+ -- firing version by the outer query's bare-column select.
27411
+ -- They ride the aggregate because the type build has no rows
27412
+ -- to read them off \u2014 see buildFindingTypes.
27413
+ d.severity AS severity,
27414
+ d.category AS category,
27187
27415
  e.event_type || '${TUPLE_SEP}' ||
27188
27416
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27189
27417
  coalesce(latest.status, '') AS status_tuple,
27190
27418
  count(*) AS tuple_count,
27191
27419
  max(e.started_at) AS latest_at,
27192
- group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
27420
+ group_concat(DISTINCT e.source_tool) AS source_tools,
27193
27421
  group_concat(DISTINCT f.action_taken) AS actions_taken
27194
27422
  ${innerSearchColumns}
27195
27423
  FROM inspection_findings f
@@ -27198,7 +27426,7 @@ var SqliteFindingsRepository = class {
27198
27426
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27199
27427
  ON latest.finding_key = f.finding_key
27200
27428
  ${scope.predicate}
27201
- GROUP BY d.rule_id, status_tuple
27429
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27202
27430
  )
27203
27431
  GROUP BY rule_id`
27204
27432
  ).all(scope.params);
@@ -27207,6 +27435,8 @@ var SqliteFindingsRepository = class {
27207
27435
  r.rule_id,
27208
27436
  {
27209
27437
  instanceCount: r.instance_count,
27438
+ severity: r.severity,
27439
+ category: r.category,
27210
27440
  sourceTools: splitConcat(r.source_tools),
27211
27441
  actionsTaken: splitConcat(r.actions_taken),
27212
27442
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27223,7 +27453,7 @@ var SqliteFindingsRepository = class {
27223
27453
  latestDetectedAt: epochMillisToIso(r.latest_at),
27224
27454
  // Free text only — joined and substring-matched, so group_concat's
27225
27455
  // commas need no unpicking (a repo/path containing one still matches).
27226
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27456
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27227
27457
  // tell "no q this request" from "a group with no repo/file at all"
27228
27458
  // and skip priming a haystack nothing will read.
27229
27459
  ...withSearchText ? {
@@ -27320,6 +27550,8 @@ function isoDay(ms) {
27320
27550
  // ../../packages/persistence/src/repositories/history-sync.ts
27321
27551
  var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27322
27552
  var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27553
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27554
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27323
27555
  var SKIPPED = -1;
27324
27556
  var ROW_COLUMNS = `id,
27325
27557
  parent_id AS parentId,
@@ -27359,6 +27591,26 @@ var SqliteHistorySyncRepository = class {
27359
27591
  ORDER BY (event_type = 'session') DESC, started_at
27360
27592
  LIMIT :limit`
27361
27593
  );
27594
+ this.captureRowsStmt = db.prepare(
27595
+ `SELECT ${ROW_COLUMNS}
27596
+ FROM audit_events
27597
+ WHERE synced_at IS NULL
27598
+ AND sync_claimed_at IS NULL
27599
+ AND outbox_owed = 1
27600
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27601
+ AND started_at < :before
27602
+ ORDER BY started_at
27603
+ LIMIT :limit`
27604
+ );
27605
+ this.markOwedStmt = db.prepare(
27606
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27607
+ );
27608
+ this.markCaptureBacklogOwedStmt = db.prepare(
27609
+ `UPDATE audit_events SET outbox_owed = 1
27610
+ WHERE synced_at IS NULL
27611
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27612
+ AND started_at < :before`
27613
+ );
27362
27614
  this.stampStmt = db.prepare(
27363
27615
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27364
27616
  );
@@ -27390,6 +27642,12 @@ var SqliteHistorySyncRepository = class {
27390
27642
  FROM audit_events
27391
27643
  WHERE event_type IN (${TYPE_LIST})`
27392
27644
  );
27645
+ this.captureSkipCountStmt = db.prepare(
27646
+ `SELECT COUNT(*) AS skipped
27647
+ FROM audit_events
27648
+ WHERE synced_at = ${String(SKIPPED)}
27649
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
27650
+ );
27393
27651
  this.fingerprintStmt = db.prepare(
27394
27652
  `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27395
27653
  FROM history_sync WHERE id = 1`
@@ -27399,6 +27657,12 @@ var SqliteHistorySyncRepository = class {
27399
27657
  SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27400
27658
  WHERE id = 1`
27401
27659
  );
27660
+ this.disownCapturesStmt = db.prepare(
27661
+ `UPDATE audit_events SET outbox_owed = NULL
27662
+ WHERE outbox_owed IS NOT NULL
27663
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27664
+ AND started_at < :attachedAt`
27665
+ );
27402
27666
  this.rearmStmt = db.prepare(
27403
27667
  `UPDATE audit_events SET synced_at = NULL
27404
27668
  WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
@@ -27471,6 +27735,11 @@ var SqliteHistorySyncRepository = class {
27471
27735
  closeWindowStmt;
27472
27736
  releaseBoundaryStmt;
27473
27737
  freezeBoundaryStmt;
27738
+ captureRowsStmt;
27739
+ markOwedStmt;
27740
+ markCaptureBacklogOwedStmt;
27741
+ captureSkipCountStmt;
27742
+ disownCapturesStmt;
27474
27743
  partitionStmt;
27475
27744
  claimRowStmt;
27476
27745
  releaseRowStmt;
@@ -27504,6 +27773,51 @@ var SqliteHistorySyncRepository = class {
27504
27773
  pendingRows(sessionId, limit, before) {
27505
27774
  return allRows(this.rowsStmt, { sessionId, limit, before });
27506
27775
  }
27776
+ /**
27777
+ * Captures this machine still owes the deployment, oldest first.
27778
+ *
27779
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
27780
+ * by a time window — see captureRowsStmt for why a window could not express
27781
+ * this. `before` is the grace window that leaves a just-recorded capture to
27782
+ * the live path.
27783
+ */
27784
+ pendingCaptureRows(limit, before) {
27785
+ return allRows(this.captureRowsStmt, { limit, before });
27786
+ }
27787
+ /**
27788
+ * Record that a capture is OWED to the deployment.
27789
+ *
27790
+ * Written by the attached forward path when a live send did not confirm
27791
+ * delivery, and read by the drain as the whole of its eligibility test. It is
27792
+ * a fact rather than an inference: the machine was attached, the send did not
27793
+ * land, so the row is owed — which no time window can state, because the same
27794
+ * window that holds the rows a past attachment left owed also holds every
27795
+ * capture recorded while the machine was DETACHED, and those were never
27796
+ * offered to anyone.
27797
+ *
27798
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27799
+ * out of the drain's read.
27800
+ */
27801
+ markCaptureOwed(id) {
27802
+ this.markOwedStmt.run({ id });
27803
+ }
27804
+ /**
27805
+ * Mark every capture already on disk as owed, as of `before`.
27806
+ *
27807
+ * The consent-time backfill, called once from `aka attach` when a human
27808
+ * grants existing-history consent — never from an ongoing drain pass, and
27809
+ * never inferred from a boundary that could later move. `before` is the
27810
+ * caller's own "now" at the moment consent was granted, so what this marks
27811
+ * is exactly the backlog the consent prompt already counted, not whatever a
27812
+ * later re-attach or key rotation might widen it to.
27813
+ *
27814
+ * Returns how many rows matched, for the caller to log or test against. Not a
27815
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
27816
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27817
+ */
27818
+ markCaptureBacklogOwed(before) {
27819
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27820
+ }
27507
27821
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27508
27822
  markSynced(ids, atMs) {
27509
27823
  this.stampAll(ids, atMs);
@@ -27587,10 +27901,12 @@ var SqliteHistorySyncRepository = class {
27587
27901
  this.countsStmt,
27588
27902
  { before }
27589
27903
  );
27904
+ const captures = getRow(this.captureSkipCountStmt);
27590
27905
  return {
27591
27906
  pending: row?.pending ?? 0,
27592
27907
  sent: row?.sent ?? 0,
27593
- skipped: row?.skipped ?? 0
27908
+ skipped: row?.skipped ?? 0,
27909
+ capturesSkipped: captures?.skipped ?? 0
27594
27910
  };
27595
27911
  }
27596
27912
  /**
@@ -27618,20 +27934,54 @@ var SqliteHistorySyncRepository = class {
27618
27934
  *
27619
27935
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
27620
27936
  * machine has just left are undelivered as far as the new one is concerned.
27621
- * All three in one transaction, so a crash between them cannot leave stamps
27622
- * attributed to the wrong deployment, or a boundary that belongs to another.
27937
+ * All four in one transaction, so a crash between them cannot leave stamps
27938
+ * attributed to the wrong deployment, a boundary that belongs to another, or
27939
+ * a disown with no re-mark to follow it.
27623
27940
  *
27624
27941
  * The boundary is written HERE and only here, which is what freezes it: a
27625
27942
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27626
27943
  * unchanged, so this never runs and the backlog does not widen back over rows
27627
27944
  * the live path has since delivered.
27945
+ *
27946
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
27947
+ * granted existing-history consent for the deployment this call is arming —
27948
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
27949
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
27950
+ * apart. Passed only when that grant is valid, since this method has no way
27951
+ * to check consent itself and must not mark a row owed for a machine that
27952
+ * never agreed to it. Applied AFTER the disown above, in the SAME
27953
+ * transaction: what the disown clears is every marker below `backlogBefore`,
27954
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
27955
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
27956
+ * on the cleared side of that bound — and the re-mark in the same
27957
+ * transaction is what puts those rows back. A crash between the two cannot
27958
+ * strand the ledger disowned with nothing re-marked — the transaction either
27959
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
27960
+ * committed re-enters this method on the very next pass. Omit it (the
27961
+ * structural-only tests do) to exercise the disown in isolation.
27962
+ *
27963
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
27964
+ * touching a marker the NEW deployment's OWN live path has already set: B's
27965
+ * live path can mark a capture owed from the moment `aka attach` writes the
27966
+ * descriptor, before the drain's first pass ever reaches this method, and
27967
+ * such a row sits at or after the bound rather than below it. What keeps the
27968
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
27969
+ * bound — disown runs first, re-mark second, both inside the one
27970
+ * transaction above.
27628
27971
  */
27629
- rearmFor(fingerprint, backlogBefore) {
27972
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27630
27973
  this.ensureRowStmt.run();
27631
27974
  withTransaction(
27632
27975
  this.db,
27633
27976
  () => {
27977
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
27634
27978
  this.rearmStmt.run();
27979
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27980
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
27981
+ }
27982
+ if (backfillCapturesBefore !== void 0) {
27983
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27984
+ }
27635
27985
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27636
27986
  },
27637
27987
  "IMMEDIATE"
@@ -27791,44 +28141,589 @@ var SqliteInspectionFindingsRepository = class {
27791
28141
  LIMIT 1`
27792
28142
  );
27793
28143
  }
27794
- db;
27795
- insertStmt;
27796
- sessionDupStmt;
27797
- eventDupStmt;
27798
- // True when an earlier event in the same session already recorded a finding
27799
- // with the same rule and masked value. The current event's own findings are
27800
- // inserted one at a time in caller order, so an earlier finding in the SAME
27801
- // recordCapture call is visible to a later duplicate check within it too.
27802
- isSessionDuplicate(ruleId, maskedMatch, sessionId) {
27803
- return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
28144
+ db;
28145
+ insertStmt;
28146
+ sessionDupStmt;
28147
+ eventDupStmt;
28148
+ // True when an earlier event in the same session already recorded a finding
28149
+ // with the same rule and masked value. The current event's own findings are
28150
+ // inserted one at a time in caller order, so an earlier finding in the SAME
28151
+ // recordCapture call is visible to a later duplicate check within it too.
28152
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
28153
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
28154
+ }
28155
+ // True when this exact detection (rule + masked value + span) is already
28156
+ // recorded against the given audit event.
28157
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
28158
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
28159
+ }
28160
+ insertFinding(input2) {
28161
+ const row = toInspectionFindingRow(input2);
28162
+ this.insertStmt.run(
28163
+ bindParams({
28164
+ id: row.id,
28165
+ auditEventId: row.auditEventId,
28166
+ inspectionDefinitionId: row.inspectionDefinitionId,
28167
+ classifiedDataId: row.classifiedDataId,
28168
+ spanStart: row.spanStart,
28169
+ spanEnd: row.spanEnd,
28170
+ maskedMatch: row.maskedMatch,
28171
+ actionTaken: row.actionTaken,
28172
+ confidence: row.confidence,
28173
+ findingKey: row.findingKey,
28174
+ firstDetectedAt: row.firstDetectedAt
28175
+ })
28176
+ );
28177
+ }
28178
+ };
28179
+
28180
+ // ../../packages/persistence/src/repositories/installed-packs.ts
28181
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
28182
+
28183
+ // ../../packages/persistence/src/policy-floor.ts
28184
+ import { readFileSync as readFileSync5 } from "fs";
28185
+ import { join as join6 } from "path";
28186
+
28187
+ // ../../packages/persistence/src/local-layout.ts
28188
+ import { renameSync as renameSync3 } from "fs";
28189
+ import { mkdir } from "fs/promises";
28190
+ import { homedir } from "os";
28191
+ import { join as join4 } from "path";
28192
+ function defaultDataDir() {
28193
+ return join4(homedir(), ".aka");
28194
+ }
28195
+ function settingsDir(base = defaultDataDir()) {
28196
+ return join4(base, "settings");
28197
+ }
28198
+ function dataDir(base = defaultDataDir()) {
28199
+ return join4(base, "data");
28200
+ }
28201
+ function dbPath(base = defaultDataDir()) {
28202
+ return join4(dataDir(base), "aka.db");
28203
+ }
28204
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
28205
+ ensureDataDirSync(dir);
28206
+ }
28207
+ function migrateLegacyLayout(base = defaultDataDir()) {
28208
+ const moves = [
28209
+ { name: "config.json", dest: settingsDir(base) },
28210
+ { name: "policy-cache.json", dest: dataDir(base) }
28211
+ ];
28212
+ for (const { name, dest } of moves) {
28213
+ try {
28214
+ ensureDataDirSync(dest);
28215
+ const moved = join4(dest, name);
28216
+ renameSync3(join4(base, name), moved);
28217
+ tightenFile(moved);
28218
+ } catch {
28219
+ }
28220
+ }
28221
+ }
28222
+
28223
+ // ../../packages/persistence/src/settings.ts
28224
+ import { readFileSync as readFileSync4 } from "fs";
28225
+ import { join as join5 } from "path";
28226
+
28227
+ // ../../packages/persistence/src/file-lock.ts
28228
+ import { randomUUID as randomUUID3 } from "crypto";
28229
+ import {
28230
+ closeSync,
28231
+ existsSync as existsSync2,
28232
+ openSync,
28233
+ readFileSync as readFileSync2,
28234
+ rmSync as rmSync5,
28235
+ statSync as statSync3,
28236
+ writeFileSync as writeFileSync2
28237
+ } from "fs";
28238
+ import { hostname as hostname3 } from "os";
28239
+ var LOCK_SUFFIX = ".lock";
28240
+ var DEFAULT_TIMEOUT_MS = 5e3;
28241
+ var DEFAULT_STALE_MS = 2e3;
28242
+ var RETRY_INTERVAL_MS = 5;
28243
+ var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
28244
+ var PARK = new Int32Array(new SharedArrayBuffer(4));
28245
+ function sleepSync(ms) {
28246
+ Atomics.wait(PARK, 0, 0, ms);
28247
+ }
28248
+ var FileLockError = class extends Error {
28249
+ reason;
28250
+ file;
28251
+ holderPid;
28252
+ constructor(reason, file2, detail, holderPid, options) {
28253
+ super(
28254
+ `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
28255
+ options
28256
+ );
28257
+ this.name = "FileLockError";
28258
+ this.reason = reason;
28259
+ this.file = file2;
28260
+ this.holderPid = holderPid;
28261
+ }
28262
+ };
28263
+ function lockPathFor(file2) {
28264
+ return `${file2}${LOCK_SUFFIX}`;
28265
+ }
28266
+ function readLockBody(lock) {
28267
+ let raw;
28268
+ try {
28269
+ raw = readFileSync2(lock, "utf8");
28270
+ } catch {
28271
+ return null;
28272
+ }
28273
+ try {
28274
+ const parsed = JSON.parse(raw);
28275
+ const { pid, token, at, host } = parsed;
28276
+ if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
28277
+ return { pid, token, at, host: typeof host === "string" ? host : "" };
28278
+ } catch {
28279
+ return null;
28280
+ }
28281
+ }
28282
+ function holderIsAlive(pid) {
28283
+ if (!Number.isInteger(pid) || pid <= 0) return false;
28284
+ try {
28285
+ process.kill(pid, 0);
28286
+ return true;
28287
+ } catch (err) {
28288
+ return err.code !== "ESRCH";
28289
+ }
28290
+ }
28291
+ function directoryAcceptsCreates(lock) {
28292
+ const probe = `${lock}.probe-${randomUUID3()}`;
28293
+ try {
28294
+ closeSync(openSync(probe, "wx", DATA_FILE_MODE));
28295
+ return true;
28296
+ } catch {
28297
+ return false;
28298
+ } finally {
28299
+ try {
28300
+ rmSync5(probe, { force: true });
28301
+ } catch {
28302
+ }
28303
+ }
28304
+ }
28305
+ function tryAcquire(lock, file2) {
28306
+ const token = randomUUID3();
28307
+ let fd;
28308
+ try {
28309
+ fd = openSync(lock, "wx", DATA_FILE_MODE);
28310
+ } catch (err) {
28311
+ const code = err.code ?? "";
28312
+ if (code === "EEXIST") return null;
28313
+ if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync2(lock) || directoryAcceptsCreates(lock))) {
28314
+ return null;
28315
+ }
28316
+ throw new FileLockError(
28317
+ "unavailable",
28318
+ file2,
28319
+ err instanceof Error ? err.message : String(err),
28320
+ void 0,
28321
+ { cause: err }
28322
+ );
28323
+ }
28324
+ const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
28325
+ try {
28326
+ writeFileSync2(fd, `${JSON.stringify(body)}
28327
+ `);
28328
+ } catch {
28329
+ try {
28330
+ closeSync(fd);
28331
+ } catch {
28332
+ }
28333
+ rmSync5(lock, { force: true });
28334
+ return null;
28335
+ }
28336
+ try {
28337
+ closeSync(fd);
28338
+ } catch {
28339
+ }
28340
+ return token;
28341
+ }
28342
+ function isAbandoned(body, lock, staleMs) {
28343
+ if (!body) {
28344
+ try {
28345
+ return Date.now() - statSync3(lock).mtimeMs >= staleMs;
28346
+ } catch {
28347
+ return false;
28348
+ }
28349
+ }
28350
+ if (Date.now() - body.at < staleMs) return false;
28351
+ if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
28352
+ return Date.now() - body.at >= abandonWindow(staleMs);
28353
+ }
28354
+ function breakIfStale(lock, staleMs) {
28355
+ const breaker2 = `${lock}.break`;
28356
+ let fd;
28357
+ try {
28358
+ fd = openSync(breaker2, "wx", DATA_FILE_MODE);
28359
+ } catch {
28360
+ reapAbandonedBreaker(breaker2);
28361
+ return false;
28362
+ }
28363
+ try {
28364
+ closeSync(fd);
28365
+ } catch {
28366
+ }
28367
+ try {
28368
+ if (!existsSync2(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
28369
+ rmSync5(lock, { force: true });
28370
+ return true;
28371
+ } catch {
28372
+ return false;
28373
+ } finally {
28374
+ try {
28375
+ rmSync5(breaker2, { force: true });
28376
+ } catch {
28377
+ }
28378
+ }
28379
+ }
28380
+ var BREAKER_ABANDONED_MS = 1e4;
28381
+ function reapAbandonedBreaker(breaker2) {
28382
+ try {
28383
+ if (Date.now() - statSync3(breaker2).mtimeMs >= BREAKER_ABANDONED_MS) {
28384
+ rmSync5(breaker2, { force: true });
28385
+ }
28386
+ } catch {
28387
+ }
28388
+ }
28389
+ function abandonWindow(staleMs) {
28390
+ return Math.max(staleMs * 30, 6e4);
28391
+ }
28392
+ function release(lock, token) {
28393
+ try {
28394
+ if (readLockBody(lock)?.token !== token) return;
28395
+ rmSync5(lock, { force: true });
28396
+ } catch {
28397
+ }
28398
+ }
28399
+ function withFileLock(file2, fn, options = {}) {
28400
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28401
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
28402
+ const lock = lockPathFor(file2);
28403
+ const deadline = Date.now() + timeoutMs;
28404
+ let token = tryAcquire(lock, file2);
28405
+ while (token === null) {
28406
+ if (breakIfStale(lock, staleMs)) {
28407
+ token = tryAcquire(lock, file2);
28408
+ continue;
28409
+ }
28410
+ if (Date.now() >= deadline) {
28411
+ throw new FileLockError(
28412
+ "timeout",
28413
+ file2,
28414
+ `still held after ${String(timeoutMs)}ms`,
28415
+ readLockBody(lock)?.pid
28416
+ );
28417
+ }
28418
+ sleepSync(RETRY_INTERVAL_MS);
28419
+ token = tryAcquire(lock, file2);
28420
+ }
28421
+ try {
28422
+ const result = fn();
28423
+ if (isThenable(result)) {
28424
+ void result.then(
28425
+ () => void 0,
28426
+ () => void 0
28427
+ );
28428
+ throw new TypeError(
28429
+ `withFileLock(${file2}) was given an async body; the lock is released as soon as it returns, so the awaited work would run unguarded. Pass a synchronous function.`
28430
+ );
28431
+ }
28432
+ return result;
28433
+ } finally {
28434
+ release(lock, token);
28435
+ }
28436
+ }
28437
+ function isThenable(value) {
28438
+ return typeof value === "object" && value !== null && typeof value.then === "function";
28439
+ }
28440
+
28441
+ // ../../packages/persistence/src/managed-settings.ts
28442
+ import { readFileSync as readFileSync3 } from "fs";
28443
+ import { posix, win32 } from "path";
28444
+ function managedSettingsPaths(platform2 = process.platform) {
28445
+ if (platform2 === "darwin") {
28446
+ return [
28447
+ posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
28448
+ posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
28449
+ ];
28450
+ }
28451
+ if (platform2 === "win32") {
28452
+ return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
28453
+ }
28454
+ return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28455
+ }
28456
+ var testOnlyManagedPaths = null;
28457
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28458
+ for (const path of paths) {
28459
+ let text;
28460
+ try {
28461
+ text = readFileSync3(path, "utf8");
28462
+ } catch {
28463
+ continue;
28464
+ }
28465
+ const record2 = parseJsonObject(text);
28466
+ if (!record2) continue;
28467
+ const parsed = ManagedSettings.safeParse(record2);
28468
+ if (parsed.success) return parsed.data;
28469
+ }
28470
+ return null;
28471
+ }
28472
+ function managedContextOf(managed) {
28473
+ if (!managed) return NO_MANAGED_CONTEXT;
28474
+ return {
28475
+ present: true,
28476
+ ...managed.organization === void 0 ? {} : { organization: managed.organization },
28477
+ lockedFields: managed.lockedFields,
28478
+ ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedFields: managed.unknownLockedFields }
28479
+ };
28480
+ }
28481
+ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
28482
+ if (!managed) return settings;
28483
+ const { values } = managed;
28484
+ const merged = { ...settings };
28485
+ if (values.runMode !== void 0) merged.runMode = values.runMode;
28486
+ if (values.controlPlane !== void 0) {
28487
+ merged.controlPlane = {
28488
+ ...values.controlPlane,
28489
+ // The administrator pinned WHICH deployment, not WHEN this machine
28490
+ // joined it. Keep the user's own attach time when the endpoint is
28491
+ // unchanged, so a managed machine does not appear to re-attach on every
28492
+ // read; stamp a fresh one when the administrator moved it.
28493
+ attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
28494
+ };
28495
+ }
28496
+ if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
28497
+ if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
28498
+ if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28499
+ if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28500
+ if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28501
+ if (values.vaultConsent !== void 0) {
28502
+ merged.vaultConsent = values.vaultConsent ? (
28503
+ // Keep an existing valid grant so its acknowledgedAt survives; mint one
28504
+ // at the current version otherwise.
28505
+ settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
28506
+ ) : void 0;
28507
+ }
28508
+ if (values.modelJudgeConsent !== void 0) {
28509
+ merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
28510
+ acknowledgedAt: now().toISOString(),
28511
+ payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
28512
+ } : void 0;
27804
28513
  }
27805
- // True when this exact detection (rule + masked value + span) is already
27806
- // recorded against the given audit event.
27807
- isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
27808
- return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
28514
+ return merged;
28515
+ }
28516
+ function lockedAmong(context, requested) {
28517
+ if (!context.present) return [];
28518
+ return requested.filter((key) => context.lockedFields.includes(key));
28519
+ }
28520
+
28521
+ // ../../packages/persistence/src/settings.ts
28522
+ var SETTINGS_FILENAME = "settings.json";
28523
+ function readWorkspaceSettings(base = defaultDataDir()) {
28524
+ return overlayManagedSettings(readUserSettings(base), readManagedSettings());
28525
+ }
28526
+ function readUserSettings(base) {
28527
+ const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
28528
+ if (!record2) return defaultWorkspaceSettings();
28529
+ try {
28530
+ return WorkspaceSettings.parse(record2);
28531
+ } catch {
28532
+ return defaultWorkspaceSettings();
27809
28533
  }
27810
- insertFinding(input2) {
27811
- const row = toInspectionFindingRow(input2);
27812
- this.insertStmt.run(
27813
- bindParams({
27814
- id: row.id,
27815
- auditEventId: row.auditEventId,
27816
- inspectionDefinitionId: row.inspectionDefinitionId,
27817
- classifiedDataId: row.classifiedDataId,
27818
- spanStart: row.spanStart,
27819
- spanEnd: row.spanEnd,
27820
- maskedMatch: row.maskedMatch,
27821
- actionTaken: row.actionTaken,
27822
- confidence: row.confidence,
27823
- findingKey: row.findingKey,
27824
- firstDetectedAt: row.firstDetectedAt
27825
- })
27826
- );
28534
+ }
28535
+ var ManagedFieldError = class extends Error {
28536
+ fields;
28537
+ constructor(fields) {
28538
+ super(`refusing to write administratively locked settings: ${fields.join(", ")}`);
28539
+ this.name = "ManagedFieldError";
28540
+ this.fields = fields;
27827
28541
  }
27828
28542
  };
28543
+ function lockableKeysTouched(current, applied) {
28544
+ const keys = [];
28545
+ const changed = (key) => key in applied && applied[key] !== current[key];
28546
+ const descriptorChanged = "controlPlane" in applied && (applied.controlPlane?.endpoint !== current.controlPlane?.endpoint || applied.controlPlane?.label !== current.controlPlane?.label);
28547
+ if (changed("runMode") || descriptorChanged) keys.push("runMode");
28548
+ if (changed("historicalAccess")) keys.push("historicalAccess");
28549
+ if (changed("vaultKeyCustody")) keys.push("vaultKeyCustody");
28550
+ if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
28551
+ if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
28552
+ if (changed("redactFallback")) keys.push("redactFallback");
28553
+ if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
28554
+ keys.push("vaultConsent");
28555
+ }
28556
+ if ("modelJudgeConsent" in applied && isModelJudgeConsentValid(applied.modelJudgeConsent) !== isModelJudgeConsentValid(current.modelJudgeConsent)) {
28557
+ keys.push("modelJudgeConsent");
28558
+ }
28559
+ return keys;
28560
+ }
28561
+ function pinnedKeys(managed) {
28562
+ if (!managed) return [];
28563
+ const { values } = managed;
28564
+ const keys = [];
28565
+ if (values.runMode !== void 0 || values.controlPlane !== void 0) keys.push("runMode");
28566
+ if (values.historicalAccess !== void 0) keys.push("historicalAccess");
28567
+ if (values.vaultConsent !== void 0) keys.push("vaultConsent");
28568
+ if (values.vaultKeyCustody !== void 0) keys.push("vaultKeyCustody");
28569
+ if (values.vaultInlineReveal !== void 0) keys.push("vaultInlineReveal");
28570
+ if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
28571
+ if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
28572
+ if (values.redactFallback !== void 0) keys.push("redactFallback");
28573
+ return keys;
28574
+ }
28575
+ function withoutManagedKeys(applied, managed, pinned, touched) {
28576
+ if (!managed.present) return applied;
28577
+ const strip = (key) => (managed.lockedFields.includes(key) || pinned.includes(key)) && !touched.includes(key);
28578
+ const out = { ...applied };
28579
+ if (strip("runMode")) {
28580
+ delete out.runMode;
28581
+ delete out.controlPlane;
28582
+ }
28583
+ if (strip("historicalAccess")) delete out.historicalAccess;
28584
+ if (strip("vaultConsent")) delete out.vaultConsent;
28585
+ if (strip("vaultKeyCustody")) delete out.vaultKeyCustody;
28586
+ if (strip("vaultInlineReveal")) delete out.vaultInlineReveal;
28587
+ if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
28588
+ if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
28589
+ if (strip("redactFallback")) delete out.redactFallback;
28590
+ return out;
28591
+ }
28592
+ function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
28593
+ const dir = settingsDir(base);
28594
+ ensureDataDirSync(dir);
28595
+ const file2 = join5(dir, SETTINGS_FILENAME);
28596
+ const managedSettings = managedOverride === void 0 ? readManagedSettings() : managedOverride;
28597
+ const managed = managedContextOf(managedSettings);
28598
+ return withFileLock(file2, () => {
28599
+ const current = readUserSettings(base);
28600
+ const applied = typeof answers2 === "function" ? answers2(current) : answers2;
28601
+ const effective = overlayManagedSettings(current, managedSettings);
28602
+ const touched = lockableKeysTouched(effective, applied);
28603
+ const refused = lockedAmong(managed, touched);
28604
+ if (refused.length > 0) throw new ManagedFieldError(refused);
28605
+ const merged = WorkspaceSettings.parse({
28606
+ ...current,
28607
+ // Locked keys are stripped rather than merged. Everything still here is,
28608
+ // by the refusal above, an unchanged ECHO of the administrator's value —
28609
+ // so dropping it discards no answer of the user's, and writing it would
28610
+ // persist the pin into their file, where it would outlive the managed
28611
+ // file and read as their own choice once the lock was gone.
28612
+ ...withoutManagedKeys(applied, managed, pinnedKeys(managedSettings), touched),
28613
+ // First setup stamps the time; later edits keep the original completion mark.
28614
+ onboardedAt: applied.onboardedAt ?? current.onboardedAt ?? (/* @__PURE__ */ new Date()).toISOString()
28615
+ });
28616
+ writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
28617
+ `);
28618
+ return merged;
28619
+ });
28620
+ }
28621
+ function readJson(file2) {
28622
+ let text;
28623
+ try {
28624
+ text = readFileSync4(file2, "utf8");
28625
+ } catch {
28626
+ return null;
28627
+ }
28628
+ return parseJsonObject(text) ?? null;
28629
+ }
27829
28630
 
27830
- // ../../packages/persistence/src/repositories/installed-packs.ts
27831
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
28631
+ // ../../packages/persistence/src/policy-floor.ts
28632
+ function refusalMessage(pack, attempted, floor, refusal) {
28633
+ switch (refusal) {
28634
+ case "lock":
28635
+ return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
28636
+ case "disable":
28637
+ return `refusing to disable '${pack}': it is governed by the connected control plane`;
28638
+ case "floor":
28639
+ return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
28640
+ }
28641
+ }
28642
+ var PolicyFloorError = class extends Error {
28643
+ /** `namespace/packId` of the detection whose write was refused. */
28644
+ pack;
28645
+ /**
28646
+ * The archetype the caller asked for, or null when the write named none —
28647
+ * clearing the assignment, or switching the detection off.
28648
+ */
28649
+ attempted;
28650
+ /** The weakest archetype the control plane permits for this pack. */
28651
+ floor;
28652
+ refusal;
28653
+ constructor(pack, attempted, floor, refusal) {
28654
+ super(refusalMessage(pack, attempted, floor, refusal));
28655
+ this.name = "PolicyFloorError";
28656
+ this.pack = pack;
28657
+ this.attempted = attempted;
28658
+ this.floor = floor;
28659
+ this.refusal = refusal;
28660
+ }
28661
+ };
28662
+ function readCachedPolicyBundle(base = defaultDataDir()) {
28663
+ try {
28664
+ const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
28665
+ const parsed = JSON.parse(raw);
28666
+ if (typeof parsed !== "object" || parsed === null) return null;
28667
+ return PolicyBundle.parse(parsed.bundle);
28668
+ } catch {
28669
+ return null;
28670
+ }
28671
+ }
28672
+ function indexEnabled(policies) {
28673
+ const byRuleId = /* @__PURE__ */ new Map();
28674
+ const byCategory = /* @__PURE__ */ new Map();
28675
+ for (const policy of policies) {
28676
+ if (!policy.enabled) continue;
28677
+ if ("ruleId" in policy.target) {
28678
+ if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
28679
+ } else if (!byCategory.has(policy.target.category)) {
28680
+ byCategory.set(policy.target.category, policy.action);
28681
+ }
28682
+ }
28683
+ return { byRuleId, byCategory };
28684
+ }
28685
+ function hasAuthoredPolicy(policies, rules, byRuleId) {
28686
+ const ruleIds = new Set(rules.map((rule) => rule.id));
28687
+ const categories = new Set(rules.map((rule) => rule.category));
28688
+ const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
28689
+ return policies.some((policy) => {
28690
+ if (!policy.enabled || policy.provenance !== "authored") return false;
28691
+ return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
28692
+ });
28693
+ }
28694
+ function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
28695
+ const floors = openControlPlaneFloors(base);
28696
+ return floors === null ? null : floors.floorFor(rules);
28697
+ }
28698
+ function openControlPlaneFloors(base = defaultDataDir()) {
28699
+ if (!isAttached(readWorkspaceSettings(base))) return null;
28700
+ const bundle = readCachedPolicyBundle(base);
28701
+ if (bundle === null) return null;
28702
+ const indexes = indexEnabled(bundle.policies);
28703
+ return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
28704
+ }
28705
+ function resolveFloor(rules, policies, { byRuleId, byCategory }) {
28706
+ let action = null;
28707
+ for (const rule of rules) {
28708
+ const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
28709
+ if (resolved === void 0) continue;
28710
+ action = action === null ? resolved : strongerAction(action, resolved);
28711
+ }
28712
+ if (action === null) return null;
28713
+ return {
28714
+ floor: weakestBuiltinAtLeast(action),
28715
+ locked: hasAuthoredPolicy(policies, rules, byRuleId)
28716
+ };
28717
+ }
28718
+ function policyAssignmentRefusal(policyId, floor) {
28719
+ if (floor.locked) return "lock";
28720
+ const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
28721
+ return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
28722
+ }
28723
+ function packEnablementRefusal(enabled, floor) {
28724
+ if (floor === null || enabled) return null;
28725
+ return "disable";
28726
+ }
27832
28727
 
27833
28728
  // ../../packages/persistence/src/semver.ts
27834
28729
  function parse3(version2) {
@@ -27922,8 +28817,19 @@ function ruleIdsOf(rulesJson) {
27922
28817
  return ids;
27923
28818
  }
27924
28819
  var SqliteInstalledPacksRepository = class {
27925
- constructor(db) {
28820
+ /**
28821
+ * `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
28822
+ * floor needs both halves of it (settings/ says whether this machine is
28823
+ * attached, data/ holds the cached bundle). It is optional because a caller
28824
+ * holding only a DatabaseSync — every test construction site, and any embedder
28825
+ * that opens the store itself — has no layout to point at, and such a caller
28826
+ * gets the pre-existing behaviour: no floor, no lock. Production threads it in
28827
+ * from `openLocalDatabase`, which is the single construction site that owns a
28828
+ * real `~/.aka`.
28829
+ */
28830
+ constructor(db, baseDir) {
27926
28831
  this.db = db;
28832
+ this.baseDir = baseDir;
27927
28833
  this.insertMissingStmt = db.prepare(
27928
28834
  `INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
27929
28835
  VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
@@ -27945,11 +28851,17 @@ var SqliteInstalledPacksRepository = class {
27945
28851
  this.signatureStmt = db.prepare(
27946
28852
  `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
27947
28853
  );
28854
+ this.packRulesStmt = db.prepare(
28855
+ `SELECT rules_json AS rulesJson FROM installed_packs
28856
+ WHERE namespace = ? AND pack_id = ?`
28857
+ );
27948
28858
  }
27949
28859
  db;
28860
+ baseDir;
27950
28861
  insertMissingStmt;
27951
28862
  upsertAvailableStmt;
27952
28863
  signatureStmt;
28864
+ packRulesStmt;
27953
28865
  /**
27954
28866
  * Record the running binary's detection inventory. Refreshes the
27955
28867
  * available_packs mirror (pruning packs the binary no longer ships) and
@@ -27991,7 +28903,7 @@ var SqliteInstalledPacksRepository = class {
27991
28903
  let behind = false;
27992
28904
  for (const row of rows) {
27993
28905
  const params = {
27994
- id: randomUUID3(),
28906
+ id: randomUUID4(),
27995
28907
  namespace: row.namespace,
27996
28908
  packId: row.packId,
27997
28909
  version: row.version,
@@ -28003,7 +28915,7 @@ var SqliteInstalledPacksRepository = class {
28003
28915
  if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
28004
28916
  this.upsertAvailableStmt.run({
28005
28917
  ...params,
28006
- id: randomUUID3(),
28918
+ id: randomUUID4(),
28007
28919
  recordedBy: meta4?.recordedBy ?? null
28008
28920
  });
28009
28921
  } else {
@@ -28249,9 +29161,65 @@ var SqliteInstalledPacksRepository = class {
28249
29161
  // NOT on the hook path — so, unlike recordInventory, these surface errors to the
28250
29162
  // caller rather than swallowing them. Each returns whether a row matched, so the
28251
29163
  // caller can tell an edit from a no-such-detection.
29164
+ /**
29165
+ * The rules one installed pack owns, reduced to what a floor computation
29166
+ * reads. Display-tolerant parsing on purpose: a pack whose snapshot is
29167
+ * unreadable contributes no rules to a scan either, so it is not a detection
29168
+ * the control plane can be governing, and an empty list correctly imposes no
29169
+ * floor. Enabled state is deliberately not filtered — a disabled pack is one
29170
+ * the user can re-enable, and its assignment stays governed meanwhile.
29171
+ */
29172
+ packFloorRules(namespace, packId) {
29173
+ const row = getRow(this.packRulesStmt, [namespace, packId]);
29174
+ if (!row) return [];
29175
+ return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
29176
+ }
29177
+ /**
29178
+ * What the connected control plane imposes on one installed pack, or null on a
29179
+ * machine that is its own authority (standalone, no cached bundle, or a
29180
+ * repository constructed without a layout base).
29181
+ *
29182
+ * Exposed as a READ so a surface can render the constraint — grey out the
29183
+ * choices below the floor, mark a locked detection as locked — rather than
29184
+ * offer the user a picker whose selections it will then be told it may not
29185
+ * make. The refusal in `setPolicy` does not depend on any surface calling this.
29186
+ */
29187
+ policyFloor(namespace, packId) {
29188
+ if (this.baseDir === void 0) return null;
29189
+ return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
29190
+ }
29191
+ /**
29192
+ * The same answer for several packs, keyed `namespace/packId` and carrying an
29193
+ * entry only for a pack the control plane actually governs.
29194
+ *
29195
+ * A surface listing every detection asks per pack, and asking through
29196
+ * `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
29197
+ * bundle and rebuilds its indexes once per pack — the entire cost of one
29198
+ * answer, repeated for each row, on every render. This reads all of that once.
29199
+ * Packs whose rules the snapshot cannot produce simply contribute no entry,
29200
+ * exactly as the single-pack read returns null for them.
29201
+ */
29202
+ policyFloors(packs) {
29203
+ const floors = /* @__PURE__ */ new Map();
29204
+ if (this.baseDir === void 0) return floors;
29205
+ const source = openControlPlaneFloors(this.baseDir);
29206
+ if (source === null) return floors;
29207
+ for (const pack of packs) {
29208
+ const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
29209
+ if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
29210
+ }
29211
+ return floors;
29212
+ }
28252
29213
  /**
28253
29214
  * Assign (or clear, with null) the enforcement policy for one installed pack.
28254
- * `policyId` must be a known built-in id (monitor/warn/redact/block).
29215
+ * `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
29216
+ *
29217
+ * On an ATTACHED machine the organization's bundle is a floor this refuses to
29218
+ * write below, and a detection the organization has authored a policy for is
29219
+ * refused outright — see policy-floor.ts for both, and for why the refusal is
29220
+ * a throw rather than a silently substituted value. This is the one device-local
29221
+ * write path for the assignment, so the check belongs here rather than on any
29222
+ * surface that offers the choice.
28255
29223
  */
28256
29224
  setPolicy(namespace, packId, policyId) {
28257
29225
  if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
@@ -28259,14 +29227,38 @@ var SqliteInstalledPacksRepository = class {
28259
29227
  `Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
28260
29228
  );
28261
29229
  }
29230
+ const requested = policyId;
29231
+ const floor = this.policyFloor(namespace, packId);
29232
+ if (floor !== null) {
29233
+ const refusal = policyAssignmentRefusal(requested, floor);
29234
+ if (refusal !== null) {
29235
+ throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
29236
+ }
29237
+ }
28262
29238
  const res = this.db.prepare(
28263
29239
  `UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
28264
29240
  WHERE namespace = :namespace AND pack_id = :packId`
28265
29241
  ).run({ policyId, now: Date.now(), namespace, packId });
28266
29242
  return Number(res.changes) > 0;
28267
29243
  }
28268
- /** Enable or disable one installed pack. */
29244
+ /**
29245
+ * Enable or disable one installed pack.
29246
+ *
29247
+ * On an ATTACHED machine a detection the organization's bundle governs at all
29248
+ * may not be switched OFF here — see packEnablementRefusal for why that is not
29249
+ * merely another point below the floor, and why re-enabling stays open. Like
29250
+ * the assignment above, the check belongs at this write path rather than on a
29251
+ * surface: this is the one device-local writer of the column, and a refusal
29252
+ * that lived in a page would leave the CLI free.
29253
+ */
28269
29254
  setEnabled(namespace, packId, enabled) {
29255
+ const floor = this.policyFloor(namespace, packId);
29256
+ if (floor !== null) {
29257
+ const refusal = packEnablementRefusal(enabled, floor);
29258
+ if (refusal !== null) {
29259
+ throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
29260
+ }
29261
+ }
28270
29262
  const res = this.db.prepare(
28271
29263
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
28272
29264
  WHERE namespace = :namespace AND pack_id = :packId`
@@ -28352,7 +29344,7 @@ var SqliteInventoryRepository = class {
28352
29344
  };
28353
29345
 
28354
29346
  // ../../packages/persistence/src/repositories/inventory-assets.ts
28355
- import { randomUUID as randomUUID4 } from "crypto";
29347
+ import { randomUUID as randomUUID5 } from "crypto";
28356
29348
  var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
28357
29349
  var VALID_HARNESS_IDS = new Set(HarnessId.options);
28358
29350
  var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
@@ -28841,7 +29833,7 @@ var SqliteInventoryAssetsRepository = class {
28841
29833
  `INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
28842
29834
  VALUES (:id, :projectId, :path, :access, :now, :now)
28843
29835
  ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
28844
- ).run({ id: randomUUID4(), projectId, path, access, now: Date.now() });
29836
+ ).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
28845
29837
  }
28846
29838
  return true;
28847
29839
  }
@@ -28862,7 +29854,7 @@ var SqliteInventoryAssetsRepository = class {
28862
29854
  `INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
28863
29855
  VALUES (:id, :assetId, :trust, :now, :now)
28864
29856
  ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
28865
- ).run({ id: randomUUID4(), assetId, trust, now: Date.now() });
29857
+ ).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
28866
29858
  }
28867
29859
  this.configRowsCache = void 0;
28868
29860
  return "ok";
@@ -29159,7 +30151,7 @@ var SqliteInventoryAssetsRepository = class {
29159
30151
  };
29160
30152
 
29161
30153
  // ../../packages/persistence/src/repositories/policies.ts
29162
- import { randomUUID as randomUUID5 } from "crypto";
30154
+ import { randomUUID as randomUUID6 } from "crypto";
29163
30155
  var SqlitePoliciesRepository = class {
29164
30156
  constructor(db) {
29165
30157
  this.db = db;
@@ -29194,7 +30186,7 @@ var SqlitePoliciesRepository = class {
29194
30186
  failOpenTransaction(this.db, () => {
29195
30187
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
29196
30188
  stmt.run({
29197
- id: randomUUID5(),
30189
+ id: randomUUID6(),
29198
30190
  target: JSON.stringify({ category }),
29199
30191
  action,
29200
30192
  now: Date.now()
@@ -29214,7 +30206,7 @@ var SqlitePoliciesRepository = class {
29214
30206
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
29215
30207
  VALUES (:id, 'global', :target, :action, 1, :now, :now)
29216
30208
  ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
29217
- ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
30209
+ ).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
29218
30210
  }
29219
30211
  // Caps every global per-category policy currently set to block/redact down
29220
30212
  // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
@@ -29282,7 +30274,7 @@ var SqlitePolicyCatalogRepository = class {
29282
30274
  };
29283
30275
 
29284
30276
  // ../../packages/persistence/src/repositories/project-files.ts
29285
- import { randomUUID as randomUUID6 } from "crypto";
30277
+ import { randomUUID as randomUUID7 } from "crypto";
29286
30278
  var SqliteProjectFilesRepository = class {
29287
30279
  constructor(db) {
29288
30280
  this.db = db;
@@ -29314,7 +30306,7 @@ var SqliteProjectFilesRepository = class {
29314
30306
  const stamp = Math.max(now, maxStamp + 1);
29315
30307
  for (const file2 of scan2.files) {
29316
30308
  this.upsertStmt.run({
29317
- id: randomUUID6(),
30309
+ id: randomUUID7(),
29318
30310
  projectId,
29319
30311
  path: file2.path,
29320
30312
  name: file2.name,
@@ -29328,9 +30320,9 @@ var SqliteProjectFilesRepository = class {
29328
30320
  };
29329
30321
 
29330
30322
  // ../../packages/persistence/src/repositories/resolutions.ts
29331
- import { randomUUID as randomUUID7 } from "crypto";
30323
+ import { randomUUID as randomUUID8 } from "crypto";
29332
30324
  var SqliteResolutionsRepository = class {
29333
- constructor(db, now = () => Date.now(), newId = () => randomUUID7()) {
30325
+ constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
29334
30326
  this.db = db;
29335
30327
  this.now = now;
29336
30328
  this.newId = newId;
@@ -29543,7 +30535,7 @@ var SqliteScanLedgerRepository = class {
29543
30535
  };
29544
30536
 
29545
30537
  // ../../packages/persistence/src/repositories/secret-vault.ts
29546
- import { randomUUID as randomUUID8 } from "crypto";
30538
+ import { randomUUID as randomUUID9 } from "crypto";
29547
30539
  function pageLimit(requested, fallback) {
29548
30540
  if (requested === void 0) return fallback;
29549
30541
  return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
@@ -29589,12 +30581,14 @@ var SELECT_COLUMNS = `
29589
30581
  ciphertext,
29590
30582
  nonce,
29591
30583
  auth_tag AS authTag,
30584
+ user_authorized AS userAuthorized,
29592
30585
  occurrence_count AS occurrenceCount,
29593
30586
  first_seen AS firstSeen,
29594
30587
  last_seen AS lastSeen`;
29595
30588
  function toRow(raw) {
29596
- const { provider, ...rest } = raw;
29597
- return provider === null ? rest : { ...rest, provider };
30589
+ const { provider, userAuthorized, ...rest } = raw;
30590
+ const row = { ...rest, userAuthorized: userAuthorized !== 0 };
30591
+ return provider === null ? row : { ...row, provider };
29598
30592
  }
29599
30593
  var SqliteSecretVaultRepository = class {
29600
30594
  constructor(db) {
@@ -29604,17 +30598,18 @@ var SqliteSecretVaultRepository = class {
29604
30598
  pointer_id, value_fingerprint, fingerprint_key_version, key_version,
29605
30599
  format_version, category, rule_id, masked_match, provider,
29606
30600
  ciphertext, nonce, auth_tag,
29607
- occurrence_count, first_seen, last_seen
30601
+ user_authorized, occurrence_count, first_seen, last_seen
29608
30602
  ) VALUES (
29609
30603
  :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
29610
30604
  :formatVersion, :category, :ruleId, :maskedMatch, :provider,
29611
30605
  :ciphertext, :nonce, :authTag,
29612
- 1, :now, :now
30606
+ :userAuthorized, 1, :now, :now
29613
30607
  )`
29614
30608
  );
29615
30609
  this.bumpStmt = db.prepare(
29616
30610
  `UPDATE secret_vault
29617
- SET occurrence_count = occurrence_count + 1, last_seen = :now
30611
+ SET occurrence_count = occurrence_count + 1, last_seen = :now,
30612
+ user_authorized = max(user_authorized, :userAuthorized)
29618
30613
  WHERE value_fingerprint = :valueFingerprint`
29619
30614
  );
29620
30615
  this.byPointerStmt = db.prepare(
@@ -29634,6 +30629,7 @@ var SqliteSecretVaultRepository = class {
29634
30629
  SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
29635
30630
  WHERE pointer_id = :pointerId`
29636
30631
  );
30632
+ this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
29637
30633
  this.derefStmt = db.prepare(
29638
30634
  `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
29639
30635
  VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
@@ -29647,6 +30643,7 @@ var SqliteSecretVaultRepository = class {
29647
30643
  listStmt;
29648
30644
  replaceCiphertextStmt;
29649
30645
  refreshFingerprintStmt;
30646
+ deleteByPointerStmt;
29650
30647
  derefStmt;
29651
30648
  /**
29652
30649
  * Vault a value, or record another sighting of one already vaulted. Keyed on
@@ -29655,6 +30652,11 @@ var SqliteSecretVaultRepository = class {
29655
30652
  * pointer, category and ciphertext, so the same secret always resolves to one
29656
30653
  * wire token. `minted` is true only when this call created the row.
29657
30654
  *
30655
+ * `userAuthorized` is the one field a repeat call may still change, and only
30656
+ * upwards: it records that a PERSON asked for this value to be replaced, and
30657
+ * the row is shared with every automatic path that vaults the same value. See
30658
+ * `bumpStmt` for why clearing it is the defect this shape exists to refuse.
30659
+ *
29658
30660
  * The read-then-write runs in one IMMEDIATE transaction so two concurrent
29659
30661
  * writers cannot both decide they are minting.
29660
30662
  */
@@ -29681,13 +30683,18 @@ var SqliteSecretVaultRepository = class {
29681
30683
  ciphertext: input2.ciphertext,
29682
30684
  nonce: input2.nonce,
29683
30685
  authTag: input2.authTag,
30686
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
29684
30687
  now
29685
30688
  })
29686
30689
  );
29687
30690
  minted = true;
29688
30691
  return;
29689
30692
  }
29690
- this.bumpStmt.run({ valueFingerprint: input2.valueFingerprint, now });
30693
+ this.bumpStmt.run({
30694
+ valueFingerprint: input2.valueFingerprint,
30695
+ userAuthorized: input2.userAuthorized === true ? 1 : 0,
30696
+ now
30697
+ });
29691
30698
  },
29692
30699
  "IMMEDIATE"
29693
30700
  );
@@ -29747,6 +30754,42 @@ var SqliteSecretVaultRepository = class {
29747
30754
  );
29748
30755
  return destroyed;
29749
30756
  }
30757
+ /**
30758
+ * Destroy the named entries and report WHICH ones went — the scoped
30759
+ * counterpart to `purgeAll`, for a caller that has already put those specific
30760
+ * values back where they came from. Ids the store does not hold are absent
30761
+ * from the answer rather than an error, so a set assembled from a stale read
30762
+ * is not a fault. The deref audit is left alone, exactly as the purge leaves
30763
+ * it.
30764
+ *
30765
+ * The ids come back rather than a count because the caller's next act is to
30766
+ * write a purge row per destroyed entry, and a record of destruction has to
30767
+ * be a record of what was really destroyed: a selection is a claim about a
30768
+ * read that has since gone stale, and auditing from it invents a purge for an
30769
+ * entry still sitting in the vault.
30770
+ *
30771
+ * One transaction over the whole set rather than a statement per id: the
30772
+ * caller hands this the result of a restore pass it has completed, and a
30773
+ * fault partway through must leave the vault as it was found rather than
30774
+ * destroying a prefix of it. The vault holds the only copy of what a pointer
30775
+ * stands for, so half a delete is not a state anything can recover from.
30776
+ */
30777
+ deleteByPointerIds(pointerIds) {
30778
+ if (pointerIds.length === 0) return [];
30779
+ const deleted = [];
30780
+ withTransaction(
30781
+ this.db,
30782
+ () => {
30783
+ for (const pointerId of pointerIds) {
30784
+ if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
30785
+ deleted.push(pointerId);
30786
+ }
30787
+ }
30788
+ },
30789
+ "IMMEDIATE"
30790
+ );
30791
+ return deleted;
30792
+ }
29750
30793
  /**
29751
30794
  * Record (or re-stamp) one place a pointer has been written. One row per
29752
30795
  * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
@@ -29759,7 +30802,7 @@ var SqliteSecretVaultRepository = class {
29759
30802
  VALUES (:id, :pointerId, :location, :kind, :now, :now)
29760
30803
  ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
29761
30804
  ).run({
29762
- id: randomUUID8(),
30805
+ id: randomUUID9(),
29763
30806
  pointerId: entry.pointerId,
29764
30807
  location: entry.location,
29765
30808
  kind: entry.kind,
@@ -29990,7 +31033,7 @@ function toUtcDateString(ms) {
29990
31033
  return new Date(ms).toISOString().slice(0, 10);
29991
31034
  }
29992
31035
  function isTimeseriesSeverity(s) {
29993
- return s === "critical" || s === "high" || s === "medium";
31036
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
29994
31037
  }
29995
31038
  var SqliteSecurityRepository = class {
29996
31039
  constructor(db, now = () => Date.now()) {
@@ -30119,12 +31162,16 @@ var SqliteSecurityRepository = class {
30119
31162
  const now = this.now();
30120
31163
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30121
31164
  const rows = this.findingsInRange(windowStart, now);
30122
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30123
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30124
- critical: 0,
30125
- high: 0,
30126
- medium: 0
30127
- }));
31165
+ const points = Array.from(
31166
+ { length: numBuckets },
31167
+ (_, i) => ({
31168
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
31169
+ critical: 0,
31170
+ high: 0,
31171
+ medium: 0,
31172
+ low: 0
31173
+ })
31174
+ );
30128
31175
  for (const r of rows) {
30129
31176
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30130
31177
  const bucket = points[idx];
@@ -30272,15 +31319,15 @@ var SqliteSecurityRepository = class {
30272
31319
  const from = now - RANGE_DAYS[range] * DAY_MS4;
30273
31320
  const rows = allRows(
30274
31321
  this.db.prepare(
30275
- `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
31322
+ `SELECT e.repo AS repo, count(*) AS c
30276
31323
  FROM inspection_findings f
30277
31324
  JOIN audit_events e ON e.id = f.audit_event_id
30278
31325
  WHERE e.started_at >= :from AND e.started_at < :to
30279
31326
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30280
- AND json_extract(e.attributes, '$.repo') IS NOT NULL
30281
- AND json_extract(e.attributes, '$.repo') != ''
30282
- GROUP BY repo
30283
- ORDER BY c DESC, repo
31327
+ AND e.repo IS NOT NULL
31328
+ AND e.repo != ''
31329
+ GROUP BY e.repo
31330
+ ORDER BY c DESC, e.repo
30284
31331
  LIMIT :limit`
30285
31332
  ),
30286
31333
  { from, to: now, limit }
@@ -30342,7 +31389,8 @@ var SqliteSecurityRepository = class {
30342
31389
  `SELECT f.finding_key AS finding_key,
30343
31390
  d.rule_id AS rule_id,
30344
31391
  d.severity AS severity,
30345
- json_extract(e.attributes, '$.file_path') AS path,
31392
+ e.repo AS repo,
31393
+ e.file_path AS path,
30346
31394
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30347
31395
  latest.resolved_at AS latest_resolved_at
30348
31396
  FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
@@ -30358,17 +31406,67 @@ var SqliteSecurityRepository = class {
30358
31406
  ),
30359
31407
  { limit }
30360
31408
  );
30361
- const items = rows.map((r) => ({
30362
- findingKey: r.finding_key,
30363
- ruleId: r.rule_id,
30364
- severity: r.severity,
30365
- path: r.path ?? "",
30366
- resolvedAt: new Date(r.latest_resolved_at).toISOString(),
30367
- // Preserved first-detection time (see the mttrTrend COALESCE note) — the
30368
- // finding's original sighting, not the latest re-scan's event.
30369
- detectedAt: new Date(r.first_detected_at).toISOString()
30370
- }));
30371
- return Promise.resolve({ items });
31409
+ const items = rows.map((r) => ({
31410
+ findingKey: r.finding_key,
31411
+ ruleId: r.rule_id,
31412
+ repo: r.repo ?? "",
31413
+ severity: r.severity,
31414
+ path: r.path ?? "",
31415
+ resolvedAt: new Date(r.latest_resolved_at).toISOString(),
31416
+ // Preserved first-detection time (see the mttrTrend COALESCE note) — the
31417
+ // finding's original sighting, not the latest re-scan's event.
31418
+ detectedAt: new Date(r.first_detected_at).toISOString()
31419
+ }));
31420
+ return Promise.resolve({ items });
31421
+ }
31422
+ /**
31423
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31424
+ *
31425
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31426
+ * list: a secret committed three weeks ago and never rotated is still the most
31427
+ * important thing to fix, and any window hides it. It carried a "newest N
31428
+ * findings" cap and then a range; the first meant a different span on every
31429
+ * machine, and the second reported "no recommendations" over live exposure.
31430
+ *
31431
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31432
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31433
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31434
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31435
+ * The two answer different questions and only this one has to match a link.
31436
+ *
31437
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31438
+ * whole-store scope costs a grouped scan rather than a row per finding.
31439
+ */
31440
+ recommendationInputs() {
31441
+ const rows = allRows(
31442
+ this.db.prepare(
31443
+ `SELECT d.rule_id AS rule_id,
31444
+ d.category AS category,
31445
+ d.severity AS severity,
31446
+ COUNT(*) AS count
31447
+ FROM inspection_findings f
31448
+ JOIN audit_events e ON e.id = f.audit_event_id
31449
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31450
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31451
+ ON latest.finding_key = f.finding_key
31452
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31453
+ AND e.event_type = 'code_change'
31454
+ AND (
31455
+ f.finding_key IS NULL
31456
+ OR latest.status IS NULL
31457
+ OR latest.status NOT IN ('resolved', 'dismissed')
31458
+ )
31459
+ GROUP BY d.rule_id, d.category, d.severity`
31460
+ )
31461
+ );
31462
+ return Promise.resolve(
31463
+ rows.map((r) => ({
31464
+ ruleId: r.rule_id,
31465
+ category: r.category,
31466
+ severity: r.severity,
31467
+ count: r.count
31468
+ }))
31469
+ );
30372
31470
  }
30373
31471
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
30374
31472
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
@@ -30376,7 +31474,11 @@ var SqliteSecurityRepository = class {
30376
31474
  findingsInRange(fromMs, toMs) {
30377
31475
  const rows = allRows(
30378
31476
  this.db.prepare(
30379
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31477
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31478
+ // joined for `severity`, so they are two more columns off a row this read
31479
+ // already fetches. They feed the recommended-actions rollup.
31480
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31481
+ d.rule_id AS rule_id, d.category AS category
30380
31482
  FROM inspection_findings f
30381
31483
  JOIN audit_events e ON e.id = f.audit_event_id
30382
31484
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -30389,13 +31491,15 @@ var SqliteSecurityRepository = class {
30389
31491
  return rows.map((r) => ({
30390
31492
  occurredAt: r.occurred_at,
30391
31493
  severity: r.severity,
30392
- actionTaken: r.action_taken
31494
+ actionTaken: r.action_taken,
31495
+ ruleId: r.rule_id,
31496
+ category: r.category
30393
31497
  }));
30394
31498
  }
30395
31499
  };
30396
31500
 
30397
31501
  // ../../packages/persistence/src/repositories/shares.ts
30398
- import { randomUUID as randomUUID9 } from "crypto";
31502
+ import { randomUUID as randomUUID10 } from "crypto";
30399
31503
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
30400
31504
  var IN_CHUNK = 500;
30401
31505
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -30483,7 +31587,7 @@ function buildSummary(dest, endpoints) {
30483
31587
  callSiteCount,
30484
31588
  transports: distinctTransports(transports),
30485
31589
  dataClasses: distinctDataClasses(dataClasses),
30486
- review: buildReviewInfo(dest.trust, transports),
31590
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30487
31591
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30488
31592
  endpoints: endpoints.map(toEndpointSummary)
30489
31593
  };
@@ -30510,7 +31614,7 @@ function buildDetail(dest, endpoints, callSites) {
30510
31614
  lastSeen: new Date(lastSeenMs).toISOString(),
30511
31615
  transports: distinctTransports(transports),
30512
31616
  dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
30513
- review: buildReviewInfo(dest.trust, transports),
31617
+ review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
30514
31618
  network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
30515
31619
  note: dest.note,
30516
31620
  endpoints: endpoints.map((ep) => ({
@@ -30539,7 +31643,11 @@ var SqliteSharesRepository = class {
30539
31643
  FROM share_destination d
30540
31644
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
30541
31645
  AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
30542
- WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
31646
+ WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
31647
+ AND NOT EXISTS (
31648
+ SELECT 1 FROM egress_decision_override o
31649
+ WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
31650
+ )`
30543
31651
  );
30544
31652
  const kindCounts = countBy(
30545
31653
  this.db,
@@ -30651,7 +31759,7 @@ var SqliteSharesRepository = class {
30651
31759
  (id, destination_id, host, decision, created_at, updated_at)
30652
31760
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
30653
31761
  ).run({
30654
- id: randomUUID9(),
31762
+ id: randomUUID10(),
30655
31763
  destinationId,
30656
31764
  host: dest.host,
30657
31765
  decision,
@@ -30800,7 +31908,7 @@ var SqliteSharesRepository = class {
30800
31908
  let destinationId = destIds.get(hit.host);
30801
31909
  if (destinationId === void 0) {
30802
31910
  destStmt.run({
30803
- id: randomUUID9(),
31911
+ id: randomUUID10(),
30804
31912
  kind: hit.kind,
30805
31913
  name: hit.name,
30806
31914
  host: hit.host,
@@ -30816,7 +31924,7 @@ var SqliteSharesRepository = class {
30816
31924
  let endpointId = endpointIds.get(endpointKey);
30817
31925
  if (endpointId === void 0) {
30818
31926
  endpointStmt.run({
30819
- id: randomUUID9(),
31927
+ id: randomUUID10(),
30820
31928
  destinationId,
30821
31929
  method: hit.method,
30822
31930
  transport: hit.transport,
@@ -30829,7 +31937,7 @@ var SqliteSharesRepository = class {
30829
31937
  endpointIds.set(endpointKey, endpointId);
30830
31938
  }
30831
31939
  siteStmt.run({
30832
- id: randomUUID9(),
31940
+ id: randomUUID10(),
30833
31941
  endpointId,
30834
31942
  project: input2.project,
30835
31943
  projectKey: input2.projectKey,
@@ -31194,6 +32302,7 @@ function purgeSampleData(db) {
31194
32302
  }
31195
32303
 
31196
32304
  // ../../packages/persistence/src/database.ts
32305
+ var CAPTURE_GRAIN = new Set(EventKind.options);
31197
32306
  var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
31198
32307
  "aka.persistence.unsafeTestOnlyRawHandle"
31199
32308
  );
@@ -31241,7 +32350,7 @@ function backupLegacyStore(db, file2) {
31241
32350
  discardStore(file2, backup);
31242
32351
  return backup;
31243
32352
  }
31244
- function openAndInitialize(file2) {
32353
+ function openAndInitialize(file2, base) {
31245
32354
  let db = openWithPragmas(file2);
31246
32355
  try {
31247
32356
  if (isForeignSqliteLineage(db)) {
@@ -31254,7 +32363,7 @@ function openAndInitialize(file2) {
31254
32363
  applyMigrations(db, file2);
31255
32364
  tightenPerms(file2);
31256
32365
  const policies = new SqlitePoliciesRepository(db);
31257
- const installedPacks = new SqliteInstalledPacksRepository(db);
32366
+ const installedPacks = new SqliteInstalledPacksRepository(db, base);
31258
32367
  const repositories = {
31259
32368
  events: new SqliteEventsRepository(db),
31260
32369
  findings: new SqliteFindingsRepository(db),
@@ -31290,7 +32399,7 @@ function openAndInitialize(file2) {
31290
32399
  }
31291
32400
  function openLocalDatabase(dir) {
31292
32401
  ensureDataDirSync(dir);
31293
- const file2 = join4(dir, DB_FILENAME);
32402
+ const file2 = join7(dir, DB_FILENAME);
31294
32403
  reapStalePartials(file2);
31295
32404
  const {
31296
32405
  db,
@@ -31318,7 +32427,13 @@ function openLocalDatabase(dir) {
31318
32427
  inspectionDefinitions,
31319
32428
  inspectionFindings,
31320
32429
  configInventory
31321
- } = openAndInitialize(file2);
32430
+ } = openAndInitialize(
32431
+ file2,
32432
+ // `dir` is always `<base>/data` — every caller resolves it through
32433
+ // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32434
+ // settings/ and data/, and the pack-policy floor needs both halves.
32435
+ dirname2(dir)
32436
+ );
31322
32437
  function captureRowId(event) {
31323
32438
  return captureId(
31324
32439
  event.metadata?.sessionId ?? null,
@@ -31331,6 +32446,21 @@ function openLocalDatabase(dir) {
31331
32446
  historySync.markSynced([captureRowId(event)], atMs);
31332
32447
  });
31333
32448
  }
32449
+ function markCaptureOwed(event) {
32450
+ failOpenTransaction(db, () => {
32451
+ historySync.markCaptureOwed(captureRowId(event));
32452
+ });
32453
+ }
32454
+ function markAuditEventsDelivered(events2, atMs) {
32455
+ const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
32456
+ if (stampable.length === 0) return;
32457
+ failOpenTransaction(db, () => {
32458
+ historySync.markSynced(
32459
+ stampable.map((event) => event.id),
32460
+ atMs
32461
+ );
32462
+ });
32463
+ }
31334
32464
  function recordCapture(event, detected) {
31335
32465
  failOpenTransaction(db, () => {
31336
32466
  const sessionId = event.metadata?.sessionId;
@@ -31417,7 +32547,7 @@ function openLocalDatabase(dir) {
31417
32547
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
31418
32548
  if (!definitionId) continue;
31419
32549
  inspectionFindings.insertFinding({
31420
- id: randomUUID10(),
32550
+ id: randomUUID11(),
31421
32551
  auditEventId: record2.scanEvent.id,
31422
32552
  inspectionDefinitionId: definitionId,
31423
32553
  span: finding.span,
@@ -31513,477 +32643,50 @@ function openLocalDatabase(dir) {
31513
32643
  inspectionFindings,
31514
32644
  recordCapture,
31515
32645
  markCaptureDelivered,
32646
+ markCaptureOwed,
32647
+ markAuditEventsDelivered,
31516
32648
  ensureInventory,
31517
32649
  recordConfigScan,
31518
32650
  recordProjectFiles,
31519
32651
  reconcileWorktreeProjects,
31520
- configInventoryReport: () => configInventory.report(),
31521
- facets,
31522
- purgeSampleData: () => {
31523
- purgeSampleData(db);
31524
- },
31525
- transaction,
31526
- close: () => {
31527
- db.close();
31528
- },
31529
- // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
31530
- [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
31531
- };
31532
- }
31533
-
31534
- // ../../packages/persistence/src/file-lock.ts
31535
- import { randomUUID as randomUUID11 } from "crypto";
31536
- import {
31537
- closeSync,
31538
- existsSync as existsSync2,
31539
- openSync,
31540
- readFileSync as readFileSync2,
31541
- rmSync as rmSync5,
31542
- statSync as statSync3,
31543
- writeFileSync as writeFileSync2
31544
- } from "fs";
31545
- import { hostname as hostname3 } from "os";
31546
- var LOCK_SUFFIX = ".lock";
31547
- var DEFAULT_TIMEOUT_MS = 5e3;
31548
- var DEFAULT_STALE_MS = 2e3;
31549
- var RETRY_INTERVAL_MS = 5;
31550
- var RETRYABLE_CREATE_ERRNOS = /* @__PURE__ */ new Set(["EEXIST", "EACCES", "EPERM", "EBUSY"]);
31551
- var PARK = new Int32Array(new SharedArrayBuffer(4));
31552
- function sleepSync(ms) {
31553
- Atomics.wait(PARK, 0, 0, ms);
31554
- }
31555
- var FileLockError = class extends Error {
31556
- reason;
31557
- file;
31558
- holderPid;
31559
- constructor(reason, file2, detail, holderPid, options) {
31560
- super(
31561
- `cannot lock ${file2} for writing: ${detail}` + (holderPid === void 0 ? "" : ` (held by pid ${String(holderPid)})`),
31562
- options
31563
- );
31564
- this.name = "FileLockError";
31565
- this.reason = reason;
31566
- this.file = file2;
31567
- this.holderPid = holderPid;
31568
- }
31569
- };
31570
- function lockPathFor(file2) {
31571
- return `${file2}${LOCK_SUFFIX}`;
31572
- }
31573
- function readLockBody(lock) {
31574
- let raw;
31575
- try {
31576
- raw = readFileSync2(lock, "utf8");
31577
- } catch {
31578
- return null;
31579
- }
31580
- try {
31581
- const parsed = JSON.parse(raw);
31582
- const { pid, token, at, host } = parsed;
31583
- if (typeof pid !== "number" || typeof token !== "string" || typeof at !== "number") return null;
31584
- return { pid, token, at, host: typeof host === "string" ? host : "" };
31585
- } catch {
31586
- return null;
31587
- }
31588
- }
31589
- function holderIsAlive(pid) {
31590
- if (!Number.isInteger(pid) || pid <= 0) return false;
31591
- try {
31592
- process.kill(pid, 0);
31593
- return true;
31594
- } catch (err) {
31595
- return err.code !== "ESRCH";
31596
- }
31597
- }
31598
- function directoryAcceptsCreates(lock) {
31599
- const probe = `${lock}.probe-${randomUUID11()}`;
31600
- try {
31601
- closeSync(openSync(probe, "wx", DATA_FILE_MODE));
31602
- return true;
31603
- } catch {
31604
- return false;
31605
- } finally {
31606
- try {
31607
- rmSync5(probe, { force: true });
31608
- } catch {
31609
- }
31610
- }
31611
- }
31612
- function tryAcquire(lock, file2) {
31613
- const token = randomUUID11();
31614
- let fd;
31615
- try {
31616
- fd = openSync(lock, "wx", DATA_FILE_MODE);
31617
- } catch (err) {
31618
- const code = err.code ?? "";
31619
- if (code === "EEXIST") return null;
31620
- if (RETRYABLE_CREATE_ERRNOS.has(code) && (existsSync2(lock) || directoryAcceptsCreates(lock))) {
31621
- return null;
31622
- }
31623
- throw new FileLockError(
31624
- "unavailable",
31625
- file2,
31626
- err instanceof Error ? err.message : String(err),
31627
- void 0,
31628
- { cause: err }
31629
- );
31630
- }
31631
- const body = { pid: process.pid, token, at: Date.now(), host: hostname3() };
31632
- try {
31633
- writeFileSync2(fd, `${JSON.stringify(body)}
31634
- `);
31635
- } catch {
31636
- try {
31637
- closeSync(fd);
31638
- } catch {
31639
- }
31640
- rmSync5(lock, { force: true });
31641
- return null;
31642
- }
31643
- try {
31644
- closeSync(fd);
31645
- } catch {
31646
- }
31647
- return token;
31648
- }
31649
- function isAbandoned(body, lock, staleMs) {
31650
- if (!body) {
31651
- try {
31652
- return Date.now() - statSync3(lock).mtimeMs >= staleMs;
31653
- } catch {
31654
- return false;
31655
- }
31656
- }
31657
- if (Date.now() - body.at < staleMs) return false;
31658
- if (body.host !== hostname3() || !holderIsAlive(body.pid)) return true;
31659
- return Date.now() - body.at >= abandonWindow(staleMs);
31660
- }
31661
- function breakIfStale(lock, staleMs) {
31662
- const breaker2 = `${lock}.break`;
31663
- let fd;
31664
- try {
31665
- fd = openSync(breaker2, "wx", DATA_FILE_MODE);
31666
- } catch {
31667
- reapAbandonedBreaker(breaker2);
31668
- return false;
31669
- }
31670
- try {
31671
- closeSync(fd);
31672
- } catch {
31673
- }
31674
- try {
31675
- if (!existsSync2(lock) || !isAbandoned(readLockBody(lock), lock, staleMs)) return false;
31676
- rmSync5(lock, { force: true });
31677
- return true;
31678
- } catch {
31679
- return false;
31680
- } finally {
31681
- try {
31682
- rmSync5(breaker2, { force: true });
31683
- } catch {
31684
- }
31685
- }
31686
- }
31687
- var BREAKER_ABANDONED_MS = 1e4;
31688
- function reapAbandonedBreaker(breaker2) {
31689
- try {
31690
- if (Date.now() - statSync3(breaker2).mtimeMs >= BREAKER_ABANDONED_MS) {
31691
- rmSync5(breaker2, { force: true });
31692
- }
31693
- } catch {
31694
- }
31695
- }
31696
- function abandonWindow(staleMs) {
31697
- return Math.max(staleMs * 30, 6e4);
31698
- }
31699
- function release(lock, token) {
31700
- try {
31701
- if (readLockBody(lock)?.token !== token) return;
31702
- rmSync5(lock, { force: true });
31703
- } catch {
31704
- }
31705
- }
31706
- function withFileLock(file2, fn, options = {}) {
31707
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
31708
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
31709
- const lock = lockPathFor(file2);
31710
- const deadline = Date.now() + timeoutMs;
31711
- let token = tryAcquire(lock, file2);
31712
- while (token === null) {
31713
- if (breakIfStale(lock, staleMs)) {
31714
- token = tryAcquire(lock, file2);
31715
- continue;
31716
- }
31717
- if (Date.now() >= deadline) {
31718
- throw new FileLockError(
31719
- "timeout",
31720
- file2,
31721
- `still held after ${String(timeoutMs)}ms`,
31722
- readLockBody(lock)?.pid
31723
- );
31724
- }
31725
- sleepSync(RETRY_INTERVAL_MS);
31726
- token = tryAcquire(lock, file2);
31727
- }
31728
- try {
31729
- const result = fn();
31730
- if (isThenable(result)) {
31731
- void result.then(
31732
- () => void 0,
31733
- () => void 0
31734
- );
31735
- throw new TypeError(
31736
- `withFileLock(${file2}) was given an async body; the lock is released as soon as it returns, so the awaited work would run unguarded. Pass a synchronous function.`
31737
- );
31738
- }
31739
- return result;
31740
- } finally {
31741
- release(lock, token);
31742
- }
31743
- }
31744
- function isThenable(value) {
31745
- return typeof value === "object" && value !== null && typeof value.then === "function";
32652
+ configInventoryReport: () => configInventory.report(),
32653
+ facets,
32654
+ purgeSampleData: () => {
32655
+ purgeSampleData(db);
32656
+ },
32657
+ transaction,
32658
+ close: () => {
32659
+ db.close();
32660
+ },
32661
+ // Last, and a plain value rather than a getter, so `{ ...db }` carries it.
32662
+ [UNSAFE_TEST_ONLY_RAW_HANDLE]: db
32663
+ };
31746
32664
  }
31747
32665
 
31748
- // ../../packages/persistence/src/finding-key.ts
32666
+ // ../../packages/persistence/src/egress-wire.ts
31749
32667
  import { createHash as createHash3 } from "crypto";
31750
32668
 
32669
+ // ../../packages/persistence/src/finding-key.ts
32670
+ import { createHash as createHash4 } from "crypto";
32671
+
31751
32672
  // ../../packages/persistence/src/fingerprint.ts
31752
32673
  import { createHmac, randomBytes } from "crypto";
31753
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31754
- import { join as join5 } from "path";
32674
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32675
+ import { join as join8 } from "path";
31755
32676
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
31756
32677
 
31757
- // ../../packages/persistence/src/history-preview.ts
32678
+ // ../../packages/persistence/src/history-backfill.ts
31758
32679
  import { existsSync as existsSync4 } from "fs";
31759
- import { join as join6 } from "path";
31760
- import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31761
-
31762
- // ../../packages/persistence/src/local-layout.ts
31763
- import { renameSync as renameSync3 } from "fs";
31764
- import { mkdir } from "fs/promises";
31765
- import { homedir } from "os";
31766
- import { join as join7 } from "path";
31767
- function defaultDataDir() {
31768
- return join7(homedir(), ".aka");
31769
- }
31770
- function settingsDir(base = defaultDataDir()) {
31771
- return join7(base, "settings");
31772
- }
31773
- function dataDir(base = defaultDataDir()) {
31774
- return join7(base, "data");
31775
- }
31776
- function dbPath(base = defaultDataDir()) {
31777
- return join7(dataDir(base), "aka.db");
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 managedContextOf(managed) {
31829
- if (!managed) return NO_MANAGED_CONTEXT;
31830
- return {
31831
- present: true,
31832
- ...managed.organization === void 0 ? {} : { organization: managed.organization },
31833
- lockedFields: managed.lockedFields
31834
- };
31835
- }
31836
- function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
31837
- if (!managed) return settings;
31838
- const { values } = managed;
31839
- const merged = { ...settings };
31840
- if (values.runMode !== void 0) merged.runMode = values.runMode;
31841
- if (values.controlPlane !== void 0) {
31842
- merged.controlPlane = {
31843
- ...values.controlPlane,
31844
- // The administrator pinned WHICH deployment, not WHEN this machine
31845
- // joined it. Keep the user's own attach time when the endpoint is
31846
- // unchanged, so a managed machine does not appear to re-attach on every
31847
- // read; stamp a fresh one when the administrator moved it.
31848
- attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
31849
- };
31850
- }
31851
- if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
31852
- if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
31853
- if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
31854
- if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
31855
- if (values.vaultConsent !== void 0) {
31856
- merged.vaultConsent = values.vaultConsent ? (
31857
- // Keep an existing valid grant so its acknowledgedAt survives; mint one
31858
- // at the current version otherwise.
31859
- settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
31860
- ) : void 0;
31861
- }
31862
- if (values.modelJudgeConsent !== void 0) {
31863
- merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
31864
- acknowledgedAt: now().toISOString(),
31865
- payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
31866
- } : void 0;
31867
- }
31868
- return merged;
31869
- }
31870
- function lockedAmong(context, requested) {
31871
- if (!context.present) return [];
31872
- return requested.filter((key) => context.lockedFields.includes(key));
31873
- }
32680
+ import { join as join9 } from "path";
31874
32681
 
31875
- // ../../packages/persistence/src/settings.ts
31876
- import { readFileSync as readFileSync5 } from "fs";
31877
- import { join as join8 } from "path";
31878
- var SETTINGS_FILENAME = "settings.json";
31879
- function readWorkspaceSettings(base = defaultDataDir()) {
31880
- return overlayManagedSettings(readUserSettings(base), readManagedSettings());
31881
- }
31882
- function readUserSettings(base) {
31883
- const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
31884
- if (!record2) return defaultWorkspaceSettings();
31885
- try {
31886
- return WorkspaceSettings.parse(record2);
31887
- } catch {
31888
- return defaultWorkspaceSettings();
31889
- }
31890
- }
31891
- var ManagedFieldError = class extends Error {
31892
- fields;
31893
- constructor(fields) {
31894
- super(`refusing to write administratively locked settings: ${fields.join(", ")}`);
31895
- this.name = "ManagedFieldError";
31896
- this.fields = fields;
31897
- }
31898
- };
31899
- function lockableKeysTouched(current, applied) {
31900
- const keys = [];
31901
- const changed = (key) => key in applied && applied[key] !== current[key];
31902
- const descriptorChanged = "controlPlane" in applied && (applied.controlPlane?.endpoint !== current.controlPlane?.endpoint || applied.controlPlane?.label !== current.controlPlane?.label);
31903
- if (changed("runMode") || descriptorChanged) keys.push("runMode");
31904
- if (changed("historicalAccess")) keys.push("historicalAccess");
31905
- if (changed("vaultKeyCustody")) keys.push("vaultKeyCustody");
31906
- if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
31907
- if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
31908
- if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
31909
- keys.push("vaultConsent");
31910
- }
31911
- if ("modelJudgeConsent" in applied && isModelJudgeConsentValid(applied.modelJudgeConsent) !== isModelJudgeConsentValid(current.modelJudgeConsent)) {
31912
- keys.push("modelJudgeConsent");
31913
- }
31914
- return keys;
31915
- }
31916
- function pinnedKeys(managed) {
31917
- if (!managed) return [];
31918
- const { values } = managed;
31919
- const keys = [];
31920
- if (values.runMode !== void 0 || values.controlPlane !== void 0) keys.push("runMode");
31921
- if (values.historicalAccess !== void 0) keys.push("historicalAccess");
31922
- if (values.vaultConsent !== void 0) keys.push("vaultConsent");
31923
- if (values.vaultKeyCustody !== void 0) keys.push("vaultKeyCustody");
31924
- if (values.vaultInlineReveal !== void 0) keys.push("vaultInlineReveal");
31925
- if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
31926
- if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
31927
- return keys;
31928
- }
31929
- function withoutManagedKeys(applied, managed, pinned, touched) {
31930
- if (!managed.present) return applied;
31931
- const strip = (key) => (managed.lockedFields.includes(key) || pinned.includes(key)) && !touched.includes(key);
31932
- const out = { ...applied };
31933
- if (strip("runMode")) {
31934
- delete out.runMode;
31935
- delete out.controlPlane;
31936
- }
31937
- if (strip("historicalAccess")) delete out.historicalAccess;
31938
- if (strip("vaultConsent")) delete out.vaultConsent;
31939
- if (strip("vaultKeyCustody")) delete out.vaultKeyCustody;
31940
- if (strip("vaultInlineReveal")) delete out.vaultInlineReveal;
31941
- if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
31942
- if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
31943
- return out;
31944
- }
31945
- function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
31946
- const dir = settingsDir(base);
31947
- ensureDataDirSync(dir);
31948
- const file2 = join8(dir, SETTINGS_FILENAME);
31949
- const managedSettings = managedOverride === void 0 ? readManagedSettings() : managedOverride;
31950
- const managed = managedContextOf(managedSettings);
31951
- return withFileLock(file2, () => {
31952
- const current = readUserSettings(base);
31953
- const applied = typeof answers2 === "function" ? answers2(current) : answers2;
31954
- const effective = overlayManagedSettings(current, managedSettings);
31955
- const touched = lockableKeysTouched(effective, applied);
31956
- const refused = lockedAmong(managed, touched);
31957
- if (refused.length > 0) throw new ManagedFieldError(refused);
31958
- const merged = WorkspaceSettings.parse({
31959
- ...current,
31960
- // Locked keys are stripped rather than merged. Everything still here is,
31961
- // by the refusal above, an unchanged ECHO of the administrator's value —
31962
- // so dropping it discards no answer of the user's, and writing it would
31963
- // persist the pin into their file, where it would outlive the managed
31964
- // file and read as their own choice once the lock was gone.
31965
- ...withoutManagedKeys(applied, managed, pinnedKeys(managedSettings), touched),
31966
- // First setup stamps the time; later edits keep the original completion mark.
31967
- onboardedAt: applied.onboardedAt ?? current.onboardedAt ?? (/* @__PURE__ */ new Date()).toISOString()
31968
- });
31969
- writeOwnerOnlyFileSync(file2, `${JSON.stringify(merged, null, 2)}
31970
- `);
31971
- return merged;
31972
- });
31973
- }
31974
- function readJson(file2) {
31975
- let text;
31976
- try {
31977
- text = readFileSync5(file2, "utf8");
31978
- } catch {
31979
- return null;
31980
- }
31981
- return parseJsonObject(text) ?? null;
31982
- }
32682
+ // ../../packages/persistence/src/history-preview.ts
32683
+ import { existsSync as existsSync5 } from "fs";
32684
+ import { join as join10 } from "path";
32685
+ import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31983
32686
 
31984
32687
  // ../../packages/persistence/src/store-symlinks.ts
31985
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
31986
- import { dirname as dirname2, join as join9, resolve } from "path";
32688
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32689
+ import { dirname as dirname3, join as join11, resolve } from "path";
31987
32690
 
31988
32691
  // ../../packages/persistence/src/vault/crypto.ts
31989
32692
  import {
@@ -31997,20 +32700,20 @@ import {
31997
32700
  // ../../packages/persistence/src/vault/key-provider.ts
31998
32701
  import { execFileSync } from "child_process";
31999
32702
  import { randomBytes as randomBytes2 } from "crypto";
32000
- import { chmodSync as chmodSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32001
- 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";
32002
32705
 
32003
32706
  // ../../packages/persistence/src/vault/vault.ts
32004
32707
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32005
32708
 
32006
32709
  // ../../packages/persistence/src/warn-era-cap.ts
32007
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32008
- import { join as join11 } from "path";
32710
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32711
+ import { join as join13 } from "path";
32009
32712
  var MARKER = "warn-era-capped";
32010
32713
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32011
32714
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32012
- const marker = join11(dataDir2, MARKER);
32013
- if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32715
+ const marker = join13(dataDir2, MARKER);
32716
+ if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32014
32717
  const capped = db.policies.capCategoryActions();
32015
32718
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
32016
32719
  `, { mode: DATA_FILE_MODE });
@@ -32018,8 +32721,8 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32018
32721
  }
32019
32722
 
32020
32723
  // ../../packages/plugin-sdk/src/config.ts
32021
- import { existsSync as existsSync7 } from "fs";
32022
- import { join as join12 } from "path";
32724
+ import { existsSync as existsSync8 } from "fs";
32725
+ import { join as join14 } from "path";
32023
32726
 
32024
32727
  // ../../packages/plugin-sdk/src/provider-env.ts
32025
32728
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32073,8 +32776,8 @@ function resolveProvider() {
32073
32776
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32074
32777
  try {
32075
32778
  ensureLayoutDirSync(base);
32076
- const settingsFile = join12(settingsDir(base), "settings.json");
32077
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
32779
+ const settingsFile = join14(settingsDir(base), "settings.json");
32780
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
32078
32781
  } catch {
32079
32782
  }
32080
32783
  migrateLegacyLayout(base);
@@ -32097,9 +32800,9 @@ function resolveProviderSafe(resolveProviderFn) {
32097
32800
  }
32098
32801
 
32099
32802
  // ../../packages/plugin-sdk/src/config-inventory.ts
32100
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32803
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32101
32804
  import { homedir as homedir2 } from "os";
32102
- import { basename as basename3, join as join14 } from "path";
32805
+ import { basename as basename3, join as join16 } from "path";
32103
32806
 
32104
32807
  // ../../packages/detections/src/egress/registry.ts
32105
32808
  var EXTRACTOR_VERSION = "1";
@@ -32857,24 +33560,20 @@ var CPU_CORROBORATION_SHARE = 0.2;
32857
33560
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
32858
33561
 
32859
33562
  // ../../packages/plugin-sdk/src/repo.ts
32860
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync6 } from "fs";
32861
- import { basename as basename2, dirname as dirname3, isAbsolute, join as join13, sep as sep2 } from "path";
33563
+ import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
33564
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
32862
33565
 
32863
33566
  // ../../packages/plugin-sdk/src/events.ts
32864
- import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
33567
+ import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
32865
33568
 
32866
33569
  // ../../packages/plugin-sdk/src/isolated-scan.ts
32867
- import { existsSync as existsSync9 } from "fs";
33570
+ import { existsSync as existsSync10 } from "fs";
32868
33571
  import { fileURLToPath } from "url";
32869
33572
  import { Worker } from "worker_threads";
32870
33573
 
32871
- // ../../packages/plugin-sdk/src/ignore-layers.ts
32872
- var import_ignore = __toESM(require_ignore(), 1);
32873
- import { readFileSync as readFileSync9 } from "fs";
32874
- import { join as join15 } from "path";
32875
-
32876
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
32877
- import { arch, hostname as hostname4, platform, release as release2 } from "os";
33574
+ // ../../packages/plugin-sdk/src/host-floor.ts
33575
+ import { readFileSync as readFileSync11 } from "fs";
33576
+ import { join as join18 } from "path";
32878
33577
 
32879
33578
  // ../../packages/plugin-sdk/src/model-governance.ts
32880
33579
  import {
@@ -32886,16 +33585,42 @@ import {
32886
33585
  readSync,
32887
33586
  writeFileSync as writeFileSync5
32888
33587
  } from "fs";
32889
- import { join as join16 } from "path";
33588
+ import { join as join17 } from "path";
32890
33589
  var TAIL_BYTES = 256 * 1024;
32891
33590
 
33591
+ // ../../packages/plugin-sdk/src/host-floor.ts
33592
+ var HOST_FEATURE = {
33593
+ ModelSwitch: "model-switch",
33594
+ VaultPointerDisplay: "vault-pointer-display"
33595
+ };
33596
+ var HOST_FLOORS = {
33597
+ [HOST_FEATURE.ModelSwitch]: {
33598
+ label: "model-switch protection",
33599
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
33600
+ since: "2.1.251"
33601
+ },
33602
+ [HOST_FEATURE.VaultPointerDisplay]: {
33603
+ label: "vault pointer display",
33604
+ hookEvents: ["MessageDisplay"],
33605
+ since: "2.1.152"
33606
+ }
33607
+ };
33608
+
33609
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
33610
+ var import_ignore = __toESM(require_ignore(), 1);
33611
+ import { readFileSync as readFileSync12 } from "fs";
33612
+ import { join as join19 } from "path";
33613
+
33614
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
33615
+ import { arch, hostname as hostname4, platform, release as release2 } from "os";
33616
+
32892
33617
  // ../../packages/plugin-sdk/src/nudge.ts
32893
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
32894
- import { join as join17 } from "path";
33618
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
33619
+ import { join as join20 } from "path";
32895
33620
 
32896
33621
  // ../../packages/plugin-sdk/src/paths.ts
32897
33622
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
32898
- import { basename as basename4, dirname as dirname4, sep as sep3 } from "path";
33623
+ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
32899
33624
 
32900
33625
  // ../../packages/plugin-sdk/src/posture.ts
32901
33626
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
@@ -32908,8 +33633,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
32908
33633
  }
32909
33634
 
32910
33635
  // ../../packages/plugin-sdk/src/project-files.ts
32911
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
32912
- import { basename as basename5, join as join18 } from "path";
33636
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
33637
+ import { basename as basename5, join as join21 } from "path";
32913
33638
 
32914
33639
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
32915
33640
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -32945,7 +33670,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
32945
33670
 
32946
33671
  // ../../packages/plugin-sdk/src/throttle.ts
32947
33672
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
32948
- import { join as join19 } from "path";
33673
+ import { join as join22 } from "path";
32949
33674
 
32950
33675
  // ../../packages/setup-wizard/src/onboard-posture.ts
32951
33676
  function parsePosture(json2) {
@@ -32968,7 +33693,7 @@ function parsePosture(json2) {
32968
33693
 
32969
33694
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
32970
33695
  import { writeFileSync as writeFileSync8 } from "fs";
32971
- import { join as join20 } from "path";
33696
+ import { join as join23 } from "path";
32972
33697
 
32973
33698
  // ../../packages/setup-wizard/src/triage/merge.ts
32974
33699
  var RANK = Object.fromEntries(
@@ -32976,9 +33701,9 @@ var RANK = Object.fromEntries(
32976
33701
  );
32977
33702
 
32978
33703
  // ../../packages/setup-wizard/src/triage/plan-file.ts
32979
- import { mkdtempSync, readFileSync as readFileSync12, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
33704
+ import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
32980
33705
  import { tmpdir } from "os";
32981
- import { basename as basename6, dirname as dirname5, join as join21 } from "path";
33706
+ import { basename as basename6, dirname as dirname6, join as join24 } from "path";
32982
33707
  var SuppressionEntrySchema = external_exports.object({
32983
33708
  ruleId: external_exports.string(),
32984
33709
  category: DetectionCategory,