@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/hooks/hooks.json +1 -1
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +1703 -982
- package/scripts/backfill.js +2669 -1576
- package/scripts/filescan.js +2463 -1429
- package/scripts/firstrun.js +2479 -1450
- package/scripts/history-sync.js +2544 -1498
- package/scripts/intro.js +577 -302
- package/scripts/message-display.js +1716 -974
- package/scripts/onboard.js +2136 -1411
- package/scripts/post-model-switch.js +663 -379
- package/scripts/post-tool-use.js +2655 -1441
- package/scripts/pre-model-switch.js +2371 -1372
- package/scripts/pre-tool-use.js +2698 -1457
- package/scripts/query.js +2557 -1465
- package/scripts/reconcile.js +2469 -1407
- package/scripts/remediate.js +2548 -1445
- package/scripts/scan-worker.js +425 -228
- package/scripts/session-start.js +2460 -1462
- package/scripts/start-light.js +580 -305
- package/scripts/statusline.js +2404 -1409
- package/scripts/stop.js +893 -391
- package/scripts/sync.js +17709 -2077
- package/scripts/user-prompt-submit.js +2506 -1445
|
@@ -492,14 +492,15 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// src/apply-suppressions.ts
|
|
495
|
-
import { existsSync as
|
|
495
|
+
import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
|
|
496
496
|
import { userInfo } from "os";
|
|
497
|
-
import { dirname as
|
|
497
|
+
import { dirname as dirname8, join as join26 } from "path";
|
|
498
498
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
499
499
|
|
|
500
500
|
// ../../packages/persistence/src/attached-derived.ts
|
|
501
501
|
import { rmSync } from "fs";
|
|
502
502
|
import { join } from "path";
|
|
503
|
+
var POLICY_CACHE_FILENAME = "policy-cache.json";
|
|
503
504
|
|
|
504
505
|
// ../../packages/persistence/src/control-plane-credential.ts
|
|
505
506
|
import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
|
|
@@ -598,6 +599,30 @@ var SQLITE_MIGRATIONS = [
|
|
|
598
599
|
{
|
|
599
600
|
tag: "0022_audit_inspection_ms",
|
|
600
601
|
sql: "ALTER TABLE `audit_events` ADD `inspection_ms` integer GENERATED ALWAYS AS (json_extract(attributes, '$.inspection_ms')) VIRTUAL;"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
tag: "0023_secret_vault_user_authorized",
|
|
605
|
+
sql: "ALTER TABLE `secret_vault` ADD `user_authorized` integer DEFAULT 0 NOT NULL;"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
tag: "0024_finding_resolution_key_created_index",
|
|
609
|
+
sql: "DROP INDEX IF EXISTS `idx_finding_resolution_key`;--> statement-breakpoint\nCREATE INDEX `idx_finding_resolution_key_created` ON `finding_resolution` (`finding_key`,`created_at`);"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
tag: "0025_audit_capture_attribute_columns",
|
|
613
|
+
sql: "ALTER TABLE `audit_events` ADD `source_tool` text GENERATED ALWAYS AS (json_extract(attributes, '$.source_tool')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `repo` text GENERATED ALWAYS AS (json_extract(attributes, '$.repo')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `file_path` text GENERATED ALWAYS AS (json_extract(attributes, '$.file_path')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `tool_name` text GENERATED ALWAYS AS (json_extract(attributes, '$.tool_name')) VIRTUAL;"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
tag: "0026_audit_llm_call_usage_columns",
|
|
617
|
+
sql: "ALTER TABLE `audit_events` ADD `service_tier` text GENERATED ALWAYS AS (json_extract(attributes, '$.service_tier')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_1h_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_1h_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `ephemeral_5m_input_tokens` integer GENERATED ALWAYS AS (json_extract(attributes, '$.ephemeral_5m_input_tokens')) VIRTUAL;--> statement-breakpoint\nALTER TABLE `audit_events` ADD `web_search_requests` integer GENERATED ALWAYS AS (json_extract(attributes, '$.web_search_requests')) VIRTUAL;"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
tag: "0027_audit_llm_usage_index",
|
|
621
|
+
sql: "CREATE INDEX `idx_audit_llm_usage` ON `audit_events` (`started_at`,`root_session_id`,`provider`,`model`,`service_tier`,`input_tokens`,`output_tokens`,`cache_creation_input_tokens`,`cache_read_input_tokens`,`ephemeral_1h_input_tokens`,`ephemeral_5m_input_tokens`,`web_search_requests`) WHERE event_type = 'llm_call' AND attributes IS NOT NULL;"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
tag: "0028_activity_session_probe_indexes",
|
|
625
|
+
sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
|
|
601
626
|
}
|
|
602
627
|
];
|
|
603
628
|
|
|
@@ -20718,13 +20743,11 @@ var FindingGroup = external_exports.object({
|
|
|
20718
20743
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20719
20744
|
instances: external_exports.array(FindingInstance),
|
|
20720
20745
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20721
|
-
//
|
|
20746
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20722
20747
|
status: FindingStatus.optional(),
|
|
20723
|
-
// The distinct people across the WHOLE group, not just the
|
|
20724
|
-
//
|
|
20725
|
-
//
|
|
20726
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20727
|
-
// without one.
|
|
20748
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20749
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20750
|
+
// store supplied whole-group folds without one.
|
|
20728
20751
|
users: external_exports.array(FindingUser).optional()
|
|
20729
20752
|
}).meta({ id: "FindingGroup" });
|
|
20730
20753
|
var FindingStats = external_exports.object({
|
|
@@ -20753,21 +20776,31 @@ var FindingFacets = external_exports.object({
|
|
|
20753
20776
|
// counted under no value.
|
|
20754
20777
|
status: external_exports.array(FindingFacetItem),
|
|
20755
20778
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20756
|
-
// reads, which can filter by it; the
|
|
20779
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20757
20780
|
// because a group spans tools.
|
|
20758
20781
|
tool: external_exports.array(FindingFacetItem).optional()
|
|
20759
20782
|
}).meta({ id: "FindingFacets" });
|
|
20760
|
-
var
|
|
20761
|
-
|
|
20783
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20784
|
+
id: "FindingTypeSummary"
|
|
20785
|
+
});
|
|
20786
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20787
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20788
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20762
20789
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20763
|
-
// FindingAction.
|
|
20790
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20791
|
+
// firing version carries, and this list pages types.
|
|
20792
|
+
//
|
|
20793
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20794
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20795
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20796
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20764
20797
|
severity: external_exports.array(Severity).optional(),
|
|
20765
20798
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20766
20799
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20767
20800
|
action: external_exports.array(FindingAction).optional(),
|
|
20768
|
-
// Matches a
|
|
20769
|
-
// individual
|
|
20770
|
-
//
|
|
20801
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20802
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20803
|
+
// requested values.
|
|
20771
20804
|
status: external_exports.array(FindingStatus).optional(),
|
|
20772
20805
|
q: external_exports.string().optional(),
|
|
20773
20806
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20777,23 +20810,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20777
20810
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20778
20811
|
// means all time — this list has no default window.
|
|
20779
20812
|
from: external_exports.iso.datetime().optional(),
|
|
20780
|
-
// A
|
|
20781
|
-
//
|
|
20782
|
-
//
|
|
20783
|
-
//
|
|
20784
|
-
//
|
|
20813
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20814
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20815
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20816
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20817
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20818
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20819
|
+
// and so is not bounded by what any page happens to hold.
|
|
20785
20820
|
includeId: external_exports.string().optional(),
|
|
20786
|
-
|
|
20787
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20821
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20788
20822
|
cursor: external_exports.string().optional()
|
|
20789
20823
|
});
|
|
20790
|
-
var
|
|
20824
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20791
20825
|
totals: external_exports.object({
|
|
20826
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20827
|
+
// the filters. The filters here select types, so a type that survives
|
|
20828
|
+
// contributes its whole instanceCount.
|
|
20829
|
+
//
|
|
20830
|
+
// `status` is the one exception, narrowed per finding via
|
|
20831
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20832
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20833
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20834
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20835
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20792
20836
|
findings: external_exports.number().int().nonnegative(),
|
|
20793
|
-
|
|
20837
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20838
|
+
// own totals count findings; the two deliberately answer different
|
|
20839
|
+
// questions and are never summed.
|
|
20840
|
+
types: external_exports.number().int().nonnegative()
|
|
20794
20841
|
}),
|
|
20795
20842
|
facets: FindingFacets,
|
|
20796
|
-
items: external_exports.array(
|
|
20843
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20797
20844
|
nextCursor: external_exports.string().nullable(),
|
|
20798
20845
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20799
20846
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20801,7 +20848,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20801
20848
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20802
20849
|
// session-scoped view show both.
|
|
20803
20850
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20804
|
-
}).meta({ id: "
|
|
20851
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20805
20852
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20806
20853
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20807
20854
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20831,12 +20878,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20831
20878
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20832
20879
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20833
20880
|
severity: external_exports.array(Severity).optional(),
|
|
20834
|
-
// Rule ids, the same vocabulary the
|
|
20881
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
20882
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20835
20883
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20836
20884
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20837
20885
|
action: external_exports.array(FindingAction).optional(),
|
|
20838
20886
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20839
|
-
// the
|
|
20887
|
+
// the types query's type-level fold.
|
|
20840
20888
|
status: external_exports.array(FindingStatus).optional(),
|
|
20841
20889
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20842
20890
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
@@ -20853,37 +20901,47 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20853
20901
|
});
|
|
20854
20902
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20855
20903
|
// Instances matching the filters across the whole scope, not just this
|
|
20856
|
-
// page — cursor-independent, like the
|
|
20904
|
+
// page — cursor-independent, like the types list's totals.
|
|
20857
20905
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20858
|
-
// Counts in INSTANCES here, where the
|
|
20906
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20859
20907
|
// dimension still excludes its own filter.
|
|
20860
20908
|
facets: FindingFacets,
|
|
20861
20909
|
items: external_exports.array(FindingInstanceDetail),
|
|
20862
20910
|
nextCursor: external_exports.string().nullable()
|
|
20863
20911
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20864
|
-
var
|
|
20865
|
-
//
|
|
20866
|
-
//
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
// Folded from the instances' derived statuses with the same
|
|
20872
|
-
// open-dominates precedence a group uses.
|
|
20873
|
-
status: FindingStatus.optional(),
|
|
20874
|
-
// Distinct rules seen at this location, capped — the row shows them as
|
|
20875
|
-
// chips, and the count is what conveys scale.
|
|
20876
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20877
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20878
|
-
var FindingLocationRepo = external_exports.object({
|
|
20912
|
+
var FindingLocationSummary = external_exports.object({
|
|
20913
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
20914
|
+
// because a location's identity is two values and a URL param carries one:
|
|
20915
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
20916
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
20917
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
20918
|
+
id: external_exports.string(),
|
|
20879
20919
|
/** Empty when the instances carried no repo attribute. */
|
|
20880
20920
|
repo: external_exports.string(),
|
|
20921
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
20922
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
20923
|
+
// the largest one in a store — and is selectable like any other.
|
|
20924
|
+
file: external_exports.string(),
|
|
20881
20925
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
20926
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
20927
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
20928
|
+
// how a reader decides what to open without opening everything.
|
|
20882
20929
|
maxSeverity: Severity,
|
|
20883
20930
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20931
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
20932
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
20933
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
20934
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
20935
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
20936
|
+
// badge.
|
|
20884
20937
|
status: FindingStatus.optional(),
|
|
20885
|
-
|
|
20886
|
-
|
|
20938
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
20939
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
20940
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
20941
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
20942
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
20943
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
20944
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20887
20945
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20888
20946
|
severity: external_exports.array(Severity).optional(),
|
|
20889
20947
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20896,18 +20954,42 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20896
20954
|
q: external_exports.string().optional(),
|
|
20897
20955
|
sessionId: external_exports.string().optional(),
|
|
20898
20956
|
from: external_exports.iso.datetime().optional(),
|
|
20899
|
-
|
|
20957
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
20958
|
+
// even when the cursor has already advanced past its sort position — the
|
|
20959
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
20960
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
20961
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
20962
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
20963
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
20964
|
+
includeId: external_exports.string().optional(),
|
|
20965
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
20966
|
+
cursor: external_exports.string().optional()
|
|
20900
20967
|
});
|
|
20901
20968
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20902
20969
|
totals: external_exports.object({
|
|
20970
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
20971
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
20972
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20903
20973
|
findings: external_exports.number().int().nonnegative(),
|
|
20904
|
-
|
|
20905
|
-
|
|
20974
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
20975
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
20976
|
+
// showing both says which is which.
|
|
20977
|
+
locations: external_exports.number().int().nonnegative()
|
|
20906
20978
|
}),
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
20979
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
20980
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
20981
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
20982
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
20983
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
20984
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
20985
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
20986
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
20987
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
20988
|
+
// tally and the panel it sits above.
|
|
20989
|
+
facets: FindingFacets,
|
|
20990
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
20991
|
+
items: external_exports.array(FindingLocationSummary),
|
|
20992
|
+
nextCursor: external_exports.string().nullable()
|
|
20911
20993
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20912
20994
|
|
|
20913
20995
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -22075,6 +22157,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22075
22157
|
message: external_exports.string().optional()
|
|
22076
22158
|
}).optional()
|
|
22077
22159
|
});
|
|
22160
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22161
|
+
"unauthorized",
|
|
22162
|
+
"forbidden",
|
|
22163
|
+
"route-absent",
|
|
22164
|
+
"invalid-request",
|
|
22165
|
+
"rejected",
|
|
22166
|
+
"unreachable"
|
|
22167
|
+
]);
|
|
22078
22168
|
var AttachDeviceRequest = external_exports.object({
|
|
22079
22169
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22080
22170
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22136,6 +22226,26 @@ var AttachTokenResponse = external_exports.union([
|
|
|
22136
22226
|
AttachTokenExpired,
|
|
22137
22227
|
external_exports.object({ status: printable(64) })
|
|
22138
22228
|
]);
|
|
22229
|
+
var DeviceCommandKind = external_exports.enum(["shares_rescan"]);
|
|
22230
|
+
var DeviceCommand = external_exports.object({
|
|
22231
|
+
id: printable(128).min(1),
|
|
22232
|
+
kind: DeviceCommandKind,
|
|
22233
|
+
issuedAt: printable(64).min(1),
|
|
22234
|
+
expiresAt: printable(64).min(1)
|
|
22235
|
+
}).strict();
|
|
22236
|
+
var DeviceCommandPollResponse = external_exports.object({ command: DeviceCommand.nullable() });
|
|
22237
|
+
var DeviceCommandFailureReason = external_exports.enum(["scan_failed", "no_projects", "expired"]);
|
|
22238
|
+
var DeviceCommandAckBody = external_exports.discriminatedUnion("outcome", [
|
|
22239
|
+
external_exports.object({
|
|
22240
|
+
outcome: external_exports.literal("reported"),
|
|
22241
|
+
projectsScanned: external_exports.number().int().nonnegative()
|
|
22242
|
+
}).strict(),
|
|
22243
|
+
external_exports.object({
|
|
22244
|
+
outcome: external_exports.literal("failed"),
|
|
22245
|
+
reason: DeviceCommandFailureReason,
|
|
22246
|
+
projectsScanned: external_exports.number().int().nonnegative()
|
|
22247
|
+
}).strict()
|
|
22248
|
+
]);
|
|
22139
22249
|
|
|
22140
22250
|
// ../../packages/schema/src/zod/registry.ts
|
|
22141
22251
|
var Namespace = external_exports.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
@@ -22302,7 +22412,7 @@ var PackManifest = external_exports.object({
|
|
|
22302
22412
|
}).meta({ id: "PackManifest" });
|
|
22303
22413
|
|
|
22304
22414
|
// ../../packages/schema/src/zod/detection.ts
|
|
22305
|
-
var OriginEnum = external_exports.enum(["library"]).meta({ id: "OriginEnum" });
|
|
22415
|
+
var OriginEnum = external_exports.enum(["library", "custom"]).meta({ id: "OriginEnum" });
|
|
22306
22416
|
var DetectionFilterEnum = external_exports.enum(["all", "library", "custom", "customized", "updates"]);
|
|
22307
22417
|
var LibraryStateEnum = external_exports.enum(["new", "imported", "update"]).meta({ id: "LibraryStateEnum" });
|
|
22308
22418
|
var DetectionCounts = external_exports.object({
|
|
@@ -22439,14 +22549,17 @@ function optional2(key, parsed, raw) {
|
|
|
22439
22549
|
function isStringArray(value) {
|
|
22440
22550
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
22441
22551
|
}
|
|
22552
|
+
var ORIGIN_VALUES = { library: true, custom: true };
|
|
22553
|
+
function resolveOrigin(origin) {
|
|
22554
|
+
return origin != null && Object.hasOwn(ORIGIN_VALUES, origin) ? origin : "library";
|
|
22555
|
+
}
|
|
22442
22556
|
function summaryToDetectionListItem(s) {
|
|
22443
22557
|
return {
|
|
22444
22558
|
id: `${s.namespace}/${s.packId}`,
|
|
22445
22559
|
name: s.name,
|
|
22446
22560
|
version: s.version,
|
|
22447
22561
|
enabled: s.enabled,
|
|
22448
|
-
origin:
|
|
22449
|
-
// v1: every installed pack is library origin
|
|
22562
|
+
origin: resolveOrigin(s.origin),
|
|
22450
22563
|
namespace: s.namespace,
|
|
22451
22564
|
packId: s.packId,
|
|
22452
22565
|
ruleCount: s.ruleCount,
|
|
@@ -22498,7 +22611,7 @@ function rowToDetectionDetail(row, findingsLast30d, update) {
|
|
|
22498
22611
|
name: row.name,
|
|
22499
22612
|
version: row.version,
|
|
22500
22613
|
enabled: row.enabled,
|
|
22501
|
-
origin:
|
|
22614
|
+
origin: resolveOrigin(row.origin),
|
|
22502
22615
|
namespace: row.namespace,
|
|
22503
22616
|
packId: row.packId,
|
|
22504
22617
|
ruleCount: row.rules.length,
|
|
@@ -22518,16 +22631,20 @@ function splitDetectionId(id) {
|
|
|
22518
22631
|
}
|
|
22519
22632
|
function buildDetectionsList(summaries, query) {
|
|
22520
22633
|
const withUpdate = summaries.filter((s) => s.latestVersion != null);
|
|
22634
|
+
const originOf = (s) => resolveOrigin(s.origin);
|
|
22521
22635
|
const counts = {
|
|
22522
22636
|
all: summaries.length,
|
|
22523
|
-
library: summaries.length,
|
|
22524
|
-
|
|
22525
|
-
|
|
22637
|
+
library: summaries.filter((s) => originOf(s) === "library").length,
|
|
22638
|
+
custom: summaries.filter((s) => originOf(s) === "custom").length,
|
|
22639
|
+
// No origin member produces this, so it is 0 BY CONSTRUCTION rather than by
|
|
22640
|
+
// omission: `customized` would mean a LIBRARY pack whose rules were edited in
|
|
22641
|
+
// place, and that state does not exist — editing a library pack forks it. See
|
|
22642
|
+
// OriginEnum.
|
|
22526
22643
|
customized: 0,
|
|
22527
22644
|
updates: withUpdate.length
|
|
22528
22645
|
};
|
|
22529
22646
|
const filter = query.filter;
|
|
22530
|
-
let filtered = filter === "custom"
|
|
22647
|
+
let filtered = filter === "customized" ? [] : filter === "custom" ? summaries.filter((s) => originOf(s) === "custom") : filter === "library" ? summaries.filter((s) => originOf(s) === "library") : filter === "updates" ? [...withUpdate] : [...summaries];
|
|
22531
22648
|
if (query.q) {
|
|
22532
22649
|
const q = query.q.toLowerCase();
|
|
22533
22650
|
filtered = filtered.filter(
|
|
@@ -22607,8 +22724,9 @@ var Event = external_exports.object({
|
|
|
22607
22724
|
metadata: EventMetadata.optional()
|
|
22608
22725
|
}).meta({ id: "Event" });
|
|
22609
22726
|
var IngestEvent = Event.meta({ id: "IngestEvent" });
|
|
22727
|
+
var INGEST_BATCH_MAX = 100;
|
|
22610
22728
|
var IngestBatch = external_exports.object({
|
|
22611
|
-
events: external_exports.array(IngestEvent).min(1).max(
|
|
22729
|
+
events: external_exports.array(IngestEvent).min(1).max(INGEST_BATCH_MAX),
|
|
22612
22730
|
// Dedup policy for this batch. Id-dedup ALWAYS applies. 'content-hash'
|
|
22613
22731
|
// additionally rejects any event whose contentHash the store has already
|
|
22614
22732
|
// recorded — for re-runnable bulk ingest (worktree scan, transcript
|
|
@@ -22740,139 +22858,62 @@ function deriveFindingStatus(row) {
|
|
|
22740
22858
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22741
22859
|
return "open";
|
|
22742
22860
|
}
|
|
22743
|
-
function distinctUsers(instances) {
|
|
22744
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22745
|
-
const users = [];
|
|
22746
|
-
for (const i of instances) {
|
|
22747
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22748
|
-
seen.add(i.user.id);
|
|
22749
|
-
users.push(i.user);
|
|
22750
|
-
}
|
|
22751
|
-
return users;
|
|
22752
|
-
}
|
|
22753
22861
|
function sortUsers(users) {
|
|
22754
22862
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22755
22863
|
}
|
|
22756
|
-
function
|
|
22757
|
-
const overrides = opts.overrides;
|
|
22864
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22758
22865
|
const packNames = opts.packNames;
|
|
22759
|
-
const
|
|
22760
|
-
const
|
|
22761
|
-
|
|
22762
|
-
const
|
|
22763
|
-
|
|
22764
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22765
|
-
}
|
|
22766
|
-
const groups = [];
|
|
22767
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22768
|
-
const instances = ruleRows.map((r) => {
|
|
22769
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22770
|
-
return {
|
|
22771
|
-
id: r.id,
|
|
22772
|
-
provider: toApiProvider(r.sourceTool),
|
|
22773
|
-
repo: r.repo,
|
|
22774
|
-
file: r.file,
|
|
22775
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22776
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22777
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22778
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22779
|
-
action: toApiAction(effectiveDbAction),
|
|
22780
|
-
detectedAt: r.occurredAt,
|
|
22781
|
-
confidence: r.confidence,
|
|
22782
|
-
status: r.status
|
|
22783
|
-
};
|
|
22784
|
-
});
|
|
22785
|
-
const agg = aggregates?.get(ruleId);
|
|
22786
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22787
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22788
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22789
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22790
|
-
);
|
|
22791
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22792
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22793
|
-
if (seenProviders.has(p)) return false;
|
|
22794
|
-
seenProviders.add(p);
|
|
22795
|
-
return true;
|
|
22796
|
-
});
|
|
22797
|
-
const actionSet = new Set(
|
|
22798
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22799
|
-
);
|
|
22866
|
+
const types = [];
|
|
22867
|
+
for (const [ruleId, agg] of aggregates) {
|
|
22868
|
+
const users = sortUsers(agg.users ?? []);
|
|
22869
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
22870
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22800
22871
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22801
|
-
const
|
|
22802
|
-
const
|
|
22803
|
-
id: ruleId,
|
|
22804
|
-
name: packNames?.get(ruleId) ?? null
|
|
22805
|
-
};
|
|
22806
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22807
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22808
|
-
const match = {
|
|
22809
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22810
|
-
contextPrefix: ""
|
|
22811
|
-
// empty (pending privacy review)
|
|
22812
|
-
};
|
|
22813
|
-
const status = foldGroupStatus(
|
|
22814
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22815
|
-
);
|
|
22816
|
-
const group = {
|
|
22872
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
22873
|
+
const type = {
|
|
22817
22874
|
id: ruleId,
|
|
22818
22875
|
category: apiCategory,
|
|
22819
22876
|
subtype: ruleId,
|
|
22820
22877
|
// human label comes with pack metadata later
|
|
22821
|
-
severity,
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
22878
|
+
severity: agg.severity ?? "low",
|
|
22879
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
22880
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
22881
|
+
instanceCount: agg.instanceCount,
|
|
22826
22882
|
providers,
|
|
22827
22883
|
aggregateAction,
|
|
22828
|
-
latestDetectedAt,
|
|
22829
|
-
|
|
22830
|
-
status,
|
|
22884
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
22885
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22831
22886
|
...users.length > 0 ? { users } : {}
|
|
22832
22887
|
};
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22837
|
-
}
|
|
22888
|
+
actionsCache.set(type, [...actionSet]);
|
|
22889
|
+
if (agg.searchText !== void 0) {
|
|
22890
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22838
22891
|
}
|
|
22839
|
-
|
|
22892
|
+
types.push(type);
|
|
22840
22893
|
}
|
|
22841
|
-
return
|
|
22894
|
+
return types;
|
|
22842
22895
|
}
|
|
22843
22896
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22844
|
-
function buildHaystack(
|
|
22897
|
+
function buildHaystack(t, extra) {
|
|
22845
22898
|
return [
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
...g.instances.map((i) => i.repo),
|
|
22852
|
-
...g.instances.map((i) => i.file),
|
|
22853
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22854
|
-
...g.instances.map((i) => i.id),
|
|
22855
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22856
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22857
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22858
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
22899
|
+
t.subtype,
|
|
22900
|
+
t.category,
|
|
22901
|
+
t.policy.name,
|
|
22902
|
+
t.id,
|
|
22903
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22859
22904
|
...extra === void 0 ? [] : [extra]
|
|
22860
22905
|
].join(" ").toLowerCase();
|
|
22861
22906
|
}
|
|
22862
|
-
function
|
|
22863
|
-
const cached2 = haystackCache.get(
|
|
22907
|
+
function typeHaystack(t) {
|
|
22908
|
+
const cached2 = haystackCache.get(t);
|
|
22864
22909
|
if (cached2 !== void 0) return cached2;
|
|
22865
|
-
const haystack = buildHaystack(
|
|
22866
|
-
haystackCache.set(
|
|
22910
|
+
const haystack = buildHaystack(t);
|
|
22911
|
+
haystackCache.set(t, haystack);
|
|
22867
22912
|
return haystack;
|
|
22868
22913
|
}
|
|
22869
22914
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22870
|
-
function
|
|
22871
|
-
|
|
22872
|
-
if (cached2 !== void 0) return cached2;
|
|
22873
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22874
|
-
actionsCache.set(g, actions);
|
|
22875
|
-
return actions;
|
|
22915
|
+
function typeActions(t) {
|
|
22916
|
+
return actionsCache.get(t) ?? [];
|
|
22876
22917
|
}
|
|
22877
22918
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22878
22919
|
const statusSet = new Set(statuses);
|
|
@@ -22883,8 +22924,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22883
22924
|
}
|
|
22884
22925
|
return sum;
|
|
22885
22926
|
}
|
|
22886
|
-
function applyFindingFilters(
|
|
22887
|
-
let filtered =
|
|
22927
|
+
function applyFindingFilters(types, opts) {
|
|
22928
|
+
let filtered = types;
|
|
22888
22929
|
if (opts.severity && opts.severity.length > 0) {
|
|
22889
22930
|
const sevSet = new Set(opts.severity);
|
|
22890
22931
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -22895,7 +22936,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22895
22936
|
}
|
|
22896
22937
|
if (opts.actions && opts.actions.length > 0) {
|
|
22897
22938
|
const actionSet = new Set(opts.actions);
|
|
22898
|
-
filtered = filtered.filter((
|
|
22939
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
22899
22940
|
}
|
|
22900
22941
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
22901
22942
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -22907,7 +22948,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22907
22948
|
}
|
|
22908
22949
|
if (opts.q) {
|
|
22909
22950
|
const q = opts.q.toLowerCase();
|
|
22910
|
-
filtered = filtered.filter((
|
|
22951
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
22911
22952
|
}
|
|
22912
22953
|
return filtered;
|
|
22913
22954
|
}
|
|
@@ -22922,11 +22963,11 @@ function compareFindingGroupOrder(a, b) {
|
|
|
22922
22963
|
if (recencyDiff !== 0) return recencyDiff;
|
|
22923
22964
|
return a.id.localeCompare(b.id);
|
|
22924
22965
|
}
|
|
22925
|
-
function
|
|
22926
|
-
return [...
|
|
22966
|
+
function sortFindingTypes(types) {
|
|
22967
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
22927
22968
|
}
|
|
22928
|
-
function computeFindingFacets(
|
|
22929
|
-
const forSeverity = applyFindingFilters(
|
|
22969
|
+
function computeFindingFacets(allTypes, opts) {
|
|
22970
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
22930
22971
|
providers: opts.providers,
|
|
22931
22972
|
actions: opts.actions,
|
|
22932
22973
|
statuses: opts.statuses,
|
|
@@ -22937,7 +22978,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22937
22978
|
for (const g of forSeverity) {
|
|
22938
22979
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
22939
22980
|
}
|
|
22940
|
-
const forProvider = applyFindingFilters(
|
|
22981
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
22941
22982
|
actions: opts.actions,
|
|
22942
22983
|
statuses: opts.statuses,
|
|
22943
22984
|
q: opts.q,
|
|
@@ -22948,7 +22989,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22948
22989
|
for (const g of forProvider) {
|
|
22949
22990
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
22950
22991
|
}
|
|
22951
|
-
const forAction = applyFindingFilters(
|
|
22992
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
22952
22993
|
providers: opts.providers,
|
|
22953
22994
|
statuses: opts.statuses,
|
|
22954
22995
|
q: opts.q,
|
|
@@ -22957,9 +22998,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22957
22998
|
});
|
|
22958
22999
|
const actionMap = /* @__PURE__ */ new Map();
|
|
22959
23000
|
for (const g of forAction) {
|
|
22960
|
-
for (const a of
|
|
23001
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
22961
23002
|
}
|
|
22962
|
-
const forSubtype = applyFindingFilters(
|
|
23003
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
22963
23004
|
providers: opts.providers,
|
|
22964
23005
|
actions: opts.actions,
|
|
22965
23006
|
statuses: opts.statuses,
|
|
@@ -22968,7 +23009,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22968
23009
|
});
|
|
22969
23010
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
22970
23011
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
22971
|
-
const forStatus = applyFindingFilters(
|
|
23012
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
22972
23013
|
providers: opts.providers,
|
|
22973
23014
|
actions: opts.actions,
|
|
22974
23015
|
q: opts.q,
|
|
@@ -23016,10 +23057,20 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23016
23057
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23017
23058
|
case "tools":
|
|
23018
23059
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23060
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23061
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23062
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23063
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23064
|
+
// row omits the key, which every call site already does.
|
|
23065
|
+
//
|
|
23066
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23067
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23068
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23069
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23019
23070
|
case "repo":
|
|
23020
|
-
return opts.repo === void 0 ||
|
|
23071
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23021
23072
|
case "file":
|
|
23022
|
-
return opts.file === void 0 ||
|
|
23073
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23023
23074
|
case "q":
|
|
23024
23075
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23025
23076
|
}
|
|
@@ -23133,6 +23184,23 @@ function addToLocation(acc, row) {
|
|
|
23133
23184
|
acc.statuses.push(row.status);
|
|
23134
23185
|
acc.ruleIds.add(row.ruleId);
|
|
23135
23186
|
}
|
|
23187
|
+
function compareLocationOrder(a, b) {
|
|
23188
|
+
const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
|
|
23189
|
+
const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
|
|
23190
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23191
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23192
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23193
|
+
}
|
|
23194
|
+
if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
|
|
23195
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
23196
|
+
return 0;
|
|
23197
|
+
}
|
|
23198
|
+
function encodeLocationId(repo, file2) {
|
|
23199
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23200
|
+
}
|
|
23201
|
+
function encodePart(value) {
|
|
23202
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23203
|
+
}
|
|
23136
23204
|
|
|
23137
23205
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23138
23206
|
var InstalledPack = external_exports.object({
|
|
@@ -23164,6 +23232,257 @@ var PatchInstalledPackRequest = external_exports.object({
|
|
|
23164
23232
|
message: "At least one field must be provided"
|
|
23165
23233
|
}).meta({ id: "PatchInstalledPackRequest" });
|
|
23166
23234
|
|
|
23235
|
+
// ../../packages/schema/src/zod/policy.ts
|
|
23236
|
+
var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
|
|
23237
|
+
var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
|
|
23238
|
+
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
23239
|
+
var PolicyProvenance = external_exports.enum(["builtin", "authored"]).meta({ id: "PolicyProvenance" });
|
|
23240
|
+
var Policy = external_exports.object({
|
|
23241
|
+
id: external_exports.guid(),
|
|
23242
|
+
scope: PolicyScope,
|
|
23243
|
+
target: PolicyTarget,
|
|
23244
|
+
action: ActionTaken,
|
|
23245
|
+
enabled: external_exports.boolean().default(true),
|
|
23246
|
+
customKeywords: external_exports.array(external_exports.string()).optional(),
|
|
23247
|
+
// Display name — optional so older policy rows without name still parse.
|
|
23248
|
+
// Added for the findings API (policy.name column migration).
|
|
23249
|
+
name: external_exports.string().optional(),
|
|
23250
|
+
// Whether an AUTHORED policy governs this row's target — not a claim about
|
|
23251
|
+
// which row this is. A producer that collapses several rows onto one target
|
|
23252
|
+
// must carry the marker onto whichever row survives, or the collapse decides
|
|
23253
|
+
// the answer; a survivor may therefore be a built-in expansion still marked
|
|
23254
|
+
// 'authored' because an authored sibling targeted the same thing.
|
|
23255
|
+
// Optional so an older producer — and an older on-disk cache — still parses;
|
|
23256
|
+
// absent reads as 'builtin', which is the behaviour that predates the field.
|
|
23257
|
+
//
|
|
23258
|
+
// Deliberately NOT `kind`/PolicyKind: that name and that enum answer which
|
|
23259
|
+
// built-in archetype catalog entry a policy is, which every catalog surface
|
|
23260
|
+
// reads and which a caller may state. This one is a statement the PRODUCER
|
|
23261
|
+
// of a bundle makes about a row, and only the bundle builder ever stamps it
|
|
23262
|
+
// — the CRUD routes neither accept nor set it.
|
|
23263
|
+
//
|
|
23264
|
+
// A device consumes this in exactly one direction: an 'authored' policy
|
|
23265
|
+
// arriving from a control plane marks the rules it targets as not
|
|
23266
|
+
// locally re-assignable. That can only ever ADD a refusal, never relax one,
|
|
23267
|
+
// which is what makes it safe to honour from an unsigned cache — the same
|
|
23268
|
+
// test `prohibitedModels` passes and `reversibleRuleIds` fails.
|
|
23269
|
+
provenance: PolicyProvenance.optional()
|
|
23270
|
+
}).meta({ id: "Policy" });
|
|
23271
|
+
var PolicyBundle = external_exports.object({
|
|
23272
|
+
version: external_exports.string(),
|
|
23273
|
+
policies: external_exports.array(Policy),
|
|
23274
|
+
// Rules from the installed marketplace packs (snapshotted by the
|
|
23275
|
+
// control plane). The plugin registers these in addition to its bundled
|
|
23276
|
+
// packs. Optional so older backends — and older on-disk caches — that omit
|
|
23277
|
+
// the field still parse; consumers read `bundle.rules ?? []`.
|
|
23278
|
+
rules: external_exports.array(Rule).optional(),
|
|
23279
|
+
// When true, `rules` IS the complete effective ruleset and the runtime must
|
|
23280
|
+
// NOT merge its compiled-in bundled packs — the standalone gateway sets this
|
|
23281
|
+
// after reading the user's installed snapshot (installed_packs, enabled
|
|
23282
|
+
// packs only), which is how detection updates stay manual: new bundled
|
|
23283
|
+
// rules run only after the user applies the pack update. Absent/false keeps
|
|
23284
|
+
// the historical composition (bundled packs + rules) — older caches.
|
|
23285
|
+
rulesComplete: external_exports.boolean().optional(),
|
|
23286
|
+
// Active detection exceptions, evaluation subset only (see
|
|
23287
|
+
// ExceptionBundleEntry). Optional so older bundle producers — and older
|
|
23288
|
+
// on-disk caches — that omit the field still parse; consumers read
|
|
23289
|
+
// `bundle.exceptions ?? []`.
|
|
23290
|
+
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
23291
|
+
// Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
|
|
23292
|
+
// A second axis over the same `redact` action, carried beside the policies
|
|
23293
|
+
// rather than on them: nothing writes ruleId-targeted policies to disk, so
|
|
23294
|
+
// widening Policy itself would change a persisted shape to express something
|
|
23295
|
+
// only the in-memory bundle needs. Optional so an older producer — or an
|
|
23296
|
+
// older on-disk cache — still parses; consumers read `?? []` and get the
|
|
23297
|
+
// pre-existing one-way behaviour, which is the safe direction to default.
|
|
23298
|
+
reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
|
|
23299
|
+
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
23300
|
+
// from a versioned installed pack. Optional so older backends — and older
|
|
23301
|
+
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
23302
|
+
// the rule's own spec version. NOT the bundle version above — see
|
|
23303
|
+
// installedRuleset's ruleVersions for the source of truth.
|
|
23304
|
+
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
23305
|
+
// Model ids (the raw `model` string a harness reports, e.g.
|
|
23306
|
+
// `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
|
|
23307
|
+
// a session onto one (PreModelSwitch) and refuses a turn that would run on
|
|
23308
|
+
// one (UserPromptSubmit). Optional so an older backend — and an older
|
|
23309
|
+
// on-disk cache — still parses; consumers read `?? []`, which is the
|
|
23310
|
+
// unenforced behaviour that predates this field and the safe direction to
|
|
23311
|
+
// default.
|
|
23312
|
+
//
|
|
23313
|
+
// Ids, not display names: the governance decision is keyed on the exact
|
|
23314
|
+
// string the harness reports (`model_status_override.versionId` in the
|
|
23315
|
+
// control plane), so no name resolution stands between the decision and the
|
|
23316
|
+
// comparison.
|
|
23317
|
+
prohibitedModels: external_exports.array(external_exports.string()).optional(),
|
|
23318
|
+
customKeywords: external_exports.array(external_exports.string()),
|
|
23319
|
+
fetchedAt: external_exports.iso.datetime()
|
|
23320
|
+
}).meta({ id: "PolicyBundle" });
|
|
23321
|
+
var POLICY_BUNDLE_SHAPE_ID = [
|
|
23322
|
+
...Object.keys(PolicyBundle.shape),
|
|
23323
|
+
...Object.keys(Policy.shape).map((key) => `policies.${key}`),
|
|
23324
|
+
...PolicyTarget.options.flatMap((member) => "shape" in member ? Object.keys(member.shape) : []).map((key) => `policies.target.${key}`),
|
|
23325
|
+
...Object.keys(ExceptionBundleEntry.shape).map((key) => `exceptions.${key}`)
|
|
23326
|
+
].sort().join(",");
|
|
23327
|
+
var OBSERVE_ONLY_CATEGORIES = ["config"];
|
|
23328
|
+
var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
|
|
23329
|
+
var CATEGORY_PEAK_SEVERITY = {
|
|
23330
|
+
secret: "critical",
|
|
23331
|
+
financial: "critical",
|
|
23332
|
+
// core-financial/credit-card
|
|
23333
|
+
code_flaw: "critical",
|
|
23334
|
+
pii: "high",
|
|
23335
|
+
phi: "high",
|
|
23336
|
+
custom: "high",
|
|
23337
|
+
// user-defined; conservative
|
|
23338
|
+
code_context: "low",
|
|
23339
|
+
config: "low"
|
|
23340
|
+
// observe-only; floors to monitor regardless
|
|
23341
|
+
};
|
|
23342
|
+
function severityFloorPolicy(category) {
|
|
23343
|
+
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
23344
|
+
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
23345
|
+
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
23346
|
+
}
|
|
23347
|
+
function severityFloorPosture() {
|
|
23348
|
+
const out = {};
|
|
23349
|
+
for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
|
|
23350
|
+
return out;
|
|
23351
|
+
}
|
|
23352
|
+
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23353
|
+
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23354
|
+
var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
|
|
23355
|
+
id: "RedactFallback"
|
|
23356
|
+
});
|
|
23357
|
+
var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
|
|
23358
|
+
var BUILTIN_POLICY_SPECS = {
|
|
23359
|
+
monitor: {
|
|
23360
|
+
name: "Monitor",
|
|
23361
|
+
action: "log",
|
|
23362
|
+
reversible: false,
|
|
23363
|
+
description: "Log every match for audit. The request is allowed through untouched."
|
|
23364
|
+
},
|
|
23365
|
+
warn: {
|
|
23366
|
+
name: "Warn",
|
|
23367
|
+
action: "warn",
|
|
23368
|
+
reversible: false,
|
|
23369
|
+
description: "Allow the request, but warn the user inline before it is sent."
|
|
23370
|
+
},
|
|
23371
|
+
redact: {
|
|
23372
|
+
name: "Redact",
|
|
23373
|
+
action: "redact",
|
|
23374
|
+
reversible: false,
|
|
23375
|
+
description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
|
|
23376
|
+
},
|
|
23377
|
+
vault: {
|
|
23378
|
+
name: "Redact & Vault",
|
|
23379
|
+
action: "redact",
|
|
23380
|
+
reversible: true,
|
|
23381
|
+
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."
|
|
23382
|
+
},
|
|
23383
|
+
block: {
|
|
23384
|
+
name: "Block",
|
|
23385
|
+
action: "block",
|
|
23386
|
+
reversible: false,
|
|
23387
|
+
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
23388
|
+
}
|
|
23389
|
+
};
|
|
23390
|
+
function builtinPolicyToAction(id) {
|
|
23391
|
+
return BUILTIN_POLICY_SPECS[id].action;
|
|
23392
|
+
}
|
|
23393
|
+
var PALETTE_WEAKEST_FIRST = [
|
|
23394
|
+
...new Set(KNOWN_BUILTIN_IDS.map(builtinPolicyToAction))
|
|
23395
|
+
];
|
|
23396
|
+
var BELOW_PALETTE = ACTION_TAKEN_KEYS.filter(
|
|
23397
|
+
(action) => !PALETTE_WEAKEST_FIRST.includes(action)
|
|
23398
|
+
);
|
|
23399
|
+
var ACTION_STRENGTH_ORDER = [
|
|
23400
|
+
...BELOW_PALETTE,
|
|
23401
|
+
...PALETTE_WEAKEST_FIRST
|
|
23402
|
+
];
|
|
23403
|
+
function actionRank(action) {
|
|
23404
|
+
return ACTION_STRENGTH_ORDER.indexOf(action);
|
|
23405
|
+
}
|
|
23406
|
+
function isActionAtLeast(action, floor) {
|
|
23407
|
+
return actionRank(action) >= actionRank(floor);
|
|
23408
|
+
}
|
|
23409
|
+
function strongerAction(a, b) {
|
|
23410
|
+
return actionRank(a) >= actionRank(b) ? a : b;
|
|
23411
|
+
}
|
|
23412
|
+
function weakestBuiltinAtLeast(floor) {
|
|
23413
|
+
return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
|
|
23414
|
+
}
|
|
23415
|
+
var PackPolicyFloor = external_exports.object({
|
|
23416
|
+
/**
|
|
23417
|
+
* The weakest archetype the device may assign. Stated as a BuiltinPolicyId
|
|
23418
|
+
* rather than a raw ActionTaken because that is the vocabulary the user
|
|
23419
|
+
* picks from — a floor a UI cannot name is one it cannot explain.
|
|
23420
|
+
*/
|
|
23421
|
+
floor: BuiltinPolicyId,
|
|
23422
|
+
/**
|
|
23423
|
+
* True when the organization AUTHORED a policy governing this pack rather
|
|
23424
|
+
* than stating a minimum: it gave the answer, so the pack is not
|
|
23425
|
+
* re-assignable locally in either direction.
|
|
23426
|
+
*/
|
|
23427
|
+
locked: external_exports.boolean()
|
|
23428
|
+
}).describe("PackPolicyFloor");
|
|
23429
|
+
var CATEGORY_EXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23430
|
+
(id) => !BUILTIN_POLICY_SPECS[id].reversible
|
|
23431
|
+
);
|
|
23432
|
+
var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23433
|
+
(id) => BUILTIN_POLICY_SPECS[id].reversible
|
|
23434
|
+
);
|
|
23435
|
+
var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
|
|
23436
|
+
function builtinPolicyIsReversible(id) {
|
|
23437
|
+
return BUILTIN_POLICY_SPECS[id].reversible;
|
|
23438
|
+
}
|
|
23439
|
+
function policyIdIsReversible(policyId) {
|
|
23440
|
+
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23441
|
+
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23442
|
+
return builtinPolicyIsReversible(id);
|
|
23443
|
+
}
|
|
23444
|
+
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
23445
|
+
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
23446
|
+
);
|
|
23447
|
+
var BUILTIN_POLICIES = Object.fromEntries(
|
|
23448
|
+
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
23449
|
+
);
|
|
23450
|
+
var DEFAULT_PACK_POLICY_ID = "monitor";
|
|
23451
|
+
function policyIdToAction(policyId) {
|
|
23452
|
+
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23453
|
+
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23454
|
+
return BUILTIN_POLICIES[id].action;
|
|
23455
|
+
}
|
|
23456
|
+
var UsedByItem = external_exports.object({
|
|
23457
|
+
id: external_exports.string(),
|
|
23458
|
+
name: external_exports.string(),
|
|
23459
|
+
ruleCount: external_exports.number().int().nonnegative(),
|
|
23460
|
+
enabled: external_exports.boolean()
|
|
23461
|
+
}).meta({ id: "UsedByItem" });
|
|
23462
|
+
var PolicyListItem = external_exports.object({
|
|
23463
|
+
id: external_exports.string(),
|
|
23464
|
+
kind: PolicyKind,
|
|
23465
|
+
name: external_exports.string(),
|
|
23466
|
+
enabled: external_exports.boolean(),
|
|
23467
|
+
usedByCount: external_exports.number().int().nonnegative()
|
|
23468
|
+
}).meta({ id: "PolicyListItem" });
|
|
23469
|
+
var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
|
|
23470
|
+
var PolicyDetail = external_exports.object({
|
|
23471
|
+
specVersion: external_exports.literal(1),
|
|
23472
|
+
id: external_exports.string(),
|
|
23473
|
+
kind: PolicyKind,
|
|
23474
|
+
name: external_exports.string(),
|
|
23475
|
+
enabled: external_exports.boolean(),
|
|
23476
|
+
description: external_exports.string(),
|
|
23477
|
+
usedBy: external_exports.array(UsedByItem)
|
|
23478
|
+
}).meta({ id: "PolicyDetail" });
|
|
23479
|
+
var PolicyStatsResponse = external_exports.object({
|
|
23480
|
+
policies: external_exports.number().int().nonnegative(),
|
|
23481
|
+
builtin: external_exports.number().int().nonnegative(),
|
|
23482
|
+
custom: external_exports.number().int().nonnegative(),
|
|
23483
|
+
detectionsGoverned: external_exports.number().int().nonnegative()
|
|
23484
|
+
}).meta({ id: "PolicyStatsResponse" });
|
|
23485
|
+
|
|
23167
23486
|
// ../../packages/schema/src/zod/vault.ts
|
|
23168
23487
|
var POINTER_FORMAT_VERSION = 2;
|
|
23169
23488
|
var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
|
|
@@ -23204,6 +23523,14 @@ var VaultEntry = external_exports.object({
|
|
|
23204
23523
|
// How many times this value has been detected on this machine — the reuse
|
|
23205
23524
|
// signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
|
|
23206
23525
|
occurrenceCount: external_exports.number().int().nonnegative(),
|
|
23526
|
+
// True when a PERSON asked for this value to be replaced — the surfaced-
|
|
23527
|
+
// secrets strike — rather than a pack enforcing its assignment. One value is
|
|
23528
|
+
// one row however many paths vault it, so this is what tells a policy sweep
|
|
23529
|
+
// that the row carries somebody's own instruction and not just an assignment
|
|
23530
|
+
// that has since been lowered. STICKY and MONOTONIC: a later automatic
|
|
23531
|
+
// vaulting of the same value must never clear it — what the user said about
|
|
23532
|
+
// the value does not expire.
|
|
23533
|
+
userAuthorized: external_exports.boolean(),
|
|
23207
23534
|
firstSeen: external_exports.string(),
|
|
23208
23535
|
lastSeen: external_exports.string()
|
|
23209
23536
|
});
|
|
@@ -23322,7 +23649,7 @@ var VaultConsent = external_exports.object({
|
|
|
23322
23649
|
});
|
|
23323
23650
|
|
|
23324
23651
|
// ../../packages/schema/src/zod/local.ts
|
|
23325
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
23652
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
|
|
23326
23653
|
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
23327
23654
|
var RunMode = external_exports.enum(["standalone", "attached"]);
|
|
23328
23655
|
var ControlPlaneConnection = external_exports.object({
|
|
@@ -23367,6 +23694,19 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23367
23694
|
vaultKeyCustody: VaultKeyCustody.default("file"),
|
|
23368
23695
|
// How a pointer renders in assistant prose on screen (see VaultInlineReveal).
|
|
23369
23696
|
vaultInlineReveal: VaultInlineReveal.default("masked"),
|
|
23697
|
+
// What a `redact` policy degrades to on a FIELD the host cannot rewrite in
|
|
23698
|
+
// place. Not a handling policy: the policy has already resolved to redact,
|
|
23699
|
+
// and this only says what happens when the host offers no channel to carry it
|
|
23700
|
+
// out — Antigravity's PreToolUse has no updatedInput at all, and Codex and
|
|
23701
|
+
// Claude Code decline to mask a field that EXECUTES because masking would
|
|
23702
|
+
// change what runs. Per FIELD rather than per host, so a host that can
|
|
23703
|
+
// rewrite some inputs keeps true redaction on those.
|
|
23704
|
+
//
|
|
23705
|
+
// Spelled in the built-in policy vocabulary rather than as a fresh enum, so
|
|
23706
|
+
// an attached machine's merge is `strongerAction` over the one action ladder
|
|
23707
|
+
// and no second rank order exists to drift from it. 'deny' is a host wire
|
|
23708
|
+
// word and stays out of the stored value.
|
|
23709
|
+
redactFallback: RedactFallback.default("warn"),
|
|
23370
23710
|
// Absent until /aka:setup completes; its presence is what "onboarded" means.
|
|
23371
23711
|
onboardedAt: external_exports.iso.datetime().optional(),
|
|
23372
23712
|
// Records that the user consented to sending findings to the model API for
|
|
@@ -23374,15 +23714,20 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23374
23714
|
// Absent until granted; a stale payloadVersion means the consent no longer
|
|
23375
23715
|
// covers the current payload and must be re-granted.
|
|
23376
23716
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23377
|
-
// Records that the user consented to
|
|
23378
|
-
//
|
|
23379
|
-
//
|
|
23380
|
-
//
|
|
23717
|
+
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23718
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23719
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23720
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23721
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23722
|
+
// or an older payload no longer counts.
|
|
23381
23723
|
historySyncConsent: HistorySyncConsent.optional()
|
|
23382
23724
|
});
|
|
23383
23725
|
function defaultWorkspaceSettings() {
|
|
23384
23726
|
return WorkspaceSettings.parse({});
|
|
23385
23727
|
}
|
|
23728
|
+
function isAttached(settings) {
|
|
23729
|
+
return settings.runMode === "attached" && settings.controlPlane !== void 0;
|
|
23730
|
+
}
|
|
23386
23731
|
function toInventoryRow(input2, id, now) {
|
|
23387
23732
|
return {
|
|
23388
23733
|
id,
|
|
@@ -23502,8 +23847,12 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23502
23847
|
"vaultKeyCustody",
|
|
23503
23848
|
"vaultInlineReveal",
|
|
23504
23849
|
"modelJudgeConsent",
|
|
23505
|
-
"dataSharesInPlace"
|
|
23850
|
+
"dataSharesInPlace",
|
|
23851
|
+
"redactFallback"
|
|
23506
23852
|
]).meta({ id: "ManagedSettingKey" });
|
|
23853
|
+
function isManagedSettingKey(value) {
|
|
23854
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
23855
|
+
}
|
|
23507
23856
|
var ManagedSettingsValues = external_exports.object({
|
|
23508
23857
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
23509
23858
|
controlPlane: external_exports.object({
|
|
@@ -23515,7 +23864,8 @@ var ManagedSettingsValues = external_exports.object({
|
|
|
23515
23864
|
vaultKeyCustody: external_exports.enum(["file", "keychain"]).optional(),
|
|
23516
23865
|
vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
|
|
23517
23866
|
modelJudgeConsent: external_exports.boolean().optional(),
|
|
23518
|
-
dataSharesInPlace: external_exports.boolean().optional()
|
|
23867
|
+
dataSharesInPlace: external_exports.boolean().optional(),
|
|
23868
|
+
redactFallback: RedactFallback.optional()
|
|
23519
23869
|
}).meta({ id: "ManagedSettingsValues" });
|
|
23520
23870
|
var ManagedSettings = external_exports.object({
|
|
23521
23871
|
specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
|
|
@@ -23527,193 +23877,28 @@ var ManagedSettings = external_exports.object({
|
|
|
23527
23877
|
// Which of those the user may not change. A key here with no matching value
|
|
23528
23878
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23529
23879
|
// the user may still override. The two are separable on purpose.
|
|
23530
|
-
lockedFields: external_exports.array(ManagedSettingKey).default([])
|
|
23531
|
-
}).meta({ id: "ManagedSettings" });
|
|
23532
|
-
|
|
23533
|
-
// ../../packages/schema/src/zod/policy.ts
|
|
23534
|
-
var PolicyScope = external_exports.enum(["global", "repo", "user"]).meta({ id: "PolicyScope" });
|
|
23535
|
-
var PolicyTarget = external_exports.union([external_exports.object({ ruleId: external_exports.string() }), external_exports.object({ category: DetectionCategory })]).meta({ id: "PolicyTarget" });
|
|
23536
|
-
var Policy = external_exports.object({
|
|
23537
|
-
id: external_exports.guid(),
|
|
23538
|
-
scope: PolicyScope,
|
|
23539
|
-
target: PolicyTarget,
|
|
23540
|
-
action: ActionTaken,
|
|
23541
|
-
enabled: external_exports.boolean().default(true),
|
|
23542
|
-
customKeywords: external_exports.array(external_exports.string()).optional(),
|
|
23543
|
-
// Display name — optional so older policy rows without name still parse.
|
|
23544
|
-
// Added for the findings API (policy.name column migration).
|
|
23545
|
-
name: external_exports.string().optional()
|
|
23546
|
-
}).meta({ id: "Policy" });
|
|
23547
|
-
var PolicyBundle = external_exports.object({
|
|
23548
|
-
version: external_exports.string(),
|
|
23549
|
-
policies: external_exports.array(Policy),
|
|
23550
|
-
// Rules from the installed marketplace packs (snapshotted by the
|
|
23551
|
-
// control plane). The plugin registers these in addition to its bundled
|
|
23552
|
-
// packs. Optional so older backends — and older on-disk caches — that omit
|
|
23553
|
-
// the field still parse; consumers read `bundle.rules ?? []`.
|
|
23554
|
-
rules: external_exports.array(Rule).optional(),
|
|
23555
|
-
// When true, `rules` IS the complete effective ruleset and the runtime must
|
|
23556
|
-
// NOT merge its compiled-in bundled packs — the standalone gateway sets this
|
|
23557
|
-
// after reading the user's installed snapshot (installed_packs, enabled
|
|
23558
|
-
// packs only), which is how detection updates stay manual: new bundled
|
|
23559
|
-
// rules run only after the user applies the pack update. Absent/false keeps
|
|
23560
|
-
// the historical composition (bundled packs + rules) — older caches.
|
|
23561
|
-
rulesComplete: external_exports.boolean().optional(),
|
|
23562
|
-
// Active detection exceptions, evaluation subset only (see
|
|
23563
|
-
// ExceptionBundleEntry). Optional so older bundle producers — and older
|
|
23564
|
-
// on-disk caches — that omit the field still parse; consumers read
|
|
23565
|
-
// `bundle.exceptions ?? []`.
|
|
23566
|
-
exceptions: external_exports.array(ExceptionBundleEntry).optional(),
|
|
23567
|
-
// Rule ids whose pack is assigned a REVERSIBLE archetype (Redact & Vault).
|
|
23568
|
-
// A second axis over the same `redact` action, carried beside the policies
|
|
23569
|
-
// rather than on them: nothing writes ruleId-targeted policies to disk, so
|
|
23570
|
-
// widening Policy itself would change a persisted shape to express something
|
|
23571
|
-
// only the in-memory bundle needs. Optional so an older producer — or an
|
|
23572
|
-
// older on-disk cache — still parses; consumers read `?? []` and get the
|
|
23573
|
-
// pre-existing one-way behaviour, which is the safe direction to default.
|
|
23574
|
-
reversibleRuleIds: external_exports.array(external_exports.string()).optional(),
|
|
23575
|
-
// Installed pack version, keyed by ruleId, for rules in `rules` that came
|
|
23576
|
-
// from a versioned installed pack. Optional so older backends — and older
|
|
23577
|
-
// on-disk caches — that omit the field still parse; consumers fall back to
|
|
23578
|
-
// the rule's own spec version. NOT the bundle version above — see
|
|
23579
|
-
// installedRuleset's ruleVersions for the source of truth.
|
|
23580
|
-
ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
23581
|
-
// Model ids (the raw `model` string a harness reports, e.g.
|
|
23582
|
-
// `claude-opus-4-1`) the tenant has PROHIBITED. The plugin refuses to switch
|
|
23583
|
-
// a session onto one (PreModelSwitch) and refuses a turn that would run on
|
|
23584
|
-
// one (UserPromptSubmit). Optional so an older backend — and an older
|
|
23585
|
-
// on-disk cache — still parses; consumers read `?? []`, which is the
|
|
23586
|
-
// unenforced behaviour that predates this field and the safe direction to
|
|
23587
|
-
// default.
|
|
23588
23880
|
//
|
|
23589
|
-
//
|
|
23590
|
-
//
|
|
23591
|
-
//
|
|
23592
|
-
//
|
|
23593
|
-
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23600
|
-
|
|
23601
|
-
|
|
23602
|
-
|
|
23603
|
-
code_flaw: "critical",
|
|
23604
|
-
pii: "high",
|
|
23605
|
-
phi: "high",
|
|
23606
|
-
custom: "high",
|
|
23607
|
-
// user-defined; conservative
|
|
23608
|
-
code_context: "low",
|
|
23609
|
-
config: "low"
|
|
23610
|
-
// observe-only; floors to monitor regardless
|
|
23611
|
-
};
|
|
23612
|
-
function severityFloorPolicy(category) {
|
|
23613
|
-
if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
|
|
23614
|
-
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
23615
|
-
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
23616
|
-
}
|
|
23617
|
-
function severityFloorPosture() {
|
|
23618
|
-
const out = {};
|
|
23619
|
-
for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
|
|
23620
|
-
return out;
|
|
23621
|
-
}
|
|
23622
|
-
var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
|
|
23623
|
-
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23624
|
-
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23625
|
-
var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
|
|
23626
|
-
var BUILTIN_POLICY_SPECS = {
|
|
23627
|
-
monitor: {
|
|
23628
|
-
name: "Monitor",
|
|
23629
|
-
action: "log",
|
|
23630
|
-
reversible: false,
|
|
23631
|
-
description: "Log every match for audit. The request is allowed through untouched."
|
|
23632
|
-
},
|
|
23633
|
-
warn: {
|
|
23634
|
-
name: "Warn",
|
|
23635
|
-
action: "warn",
|
|
23636
|
-
reversible: false,
|
|
23637
|
-
description: "Allow the request, but warn the user inline before it is sent."
|
|
23638
|
-
},
|
|
23639
|
-
redact: {
|
|
23640
|
-
name: "Redact",
|
|
23641
|
-
action: "redact",
|
|
23642
|
-
reversible: false,
|
|
23643
|
-
description: "Strip the matched value from the request and destroy it, then continue. What was removed cannot be recovered."
|
|
23644
|
-
},
|
|
23645
|
-
vault: {
|
|
23646
|
-
name: "Redact & Vault",
|
|
23647
|
-
action: "redact",
|
|
23648
|
-
reversible: true,
|
|
23649
|
-
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."
|
|
23650
|
-
},
|
|
23651
|
-
block: {
|
|
23652
|
-
name: "Block",
|
|
23653
|
-
action: "block",
|
|
23654
|
-
reversible: false,
|
|
23655
|
-
description: "Refuse the request entirely whenever any rule in this detection matches."
|
|
23881
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
23882
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
23883
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
23884
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
23885
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
23886
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
23887
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
23888
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
23889
|
+
}).transform(({ lockedFields, ...rest }) => {
|
|
23890
|
+
const known = [];
|
|
23891
|
+
const unknown2 = [];
|
|
23892
|
+
for (const name of lockedFields) {
|
|
23893
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
23894
|
+
else unknown2.push(name);
|
|
23656
23895
|
}
|
|
23657
|
-
|
|
23658
|
-
|
|
23659
|
-
|
|
23660
|
-
}
|
|
23661
|
-
|
|
23662
|
-
|
|
23663
|
-
);
|
|
23664
|
-
var CATEGORY_INEXPRESSIBLE_IDS = KNOWN_BUILTIN_IDS.filter(
|
|
23665
|
-
(id) => BUILTIN_POLICY_SPECS[id].reversible
|
|
23666
|
-
);
|
|
23667
|
-
var CategoryPolicyId = external_exports.enum(CATEGORY_EXPRESSIBLE_IDS).meta({ id: "CategoryPolicyId" });
|
|
23668
|
-
function builtinPolicyIsReversible(id) {
|
|
23669
|
-
return BUILTIN_POLICY_SPECS[id].reversible;
|
|
23670
|
-
}
|
|
23671
|
-
function policyIdIsReversible(policyId) {
|
|
23672
|
-
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23673
|
-
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23674
|
-
return builtinPolicyIsReversible(id);
|
|
23675
|
-
}
|
|
23676
|
-
var DEFAULT_ACTIONS = Object.fromEntries(
|
|
23677
|
-
DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
|
|
23678
|
-
);
|
|
23679
|
-
var BUILTIN_POLICIES = Object.fromEntries(
|
|
23680
|
-
KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
|
|
23681
|
-
);
|
|
23682
|
-
var DEFAULT_PACK_POLICY_ID = "monitor";
|
|
23683
|
-
function policyIdToAction(policyId) {
|
|
23684
|
-
const parsed = BuiltinPolicyId.safeParse(policyId ?? DEFAULT_PACK_POLICY_ID);
|
|
23685
|
-
const id = parsed.success ? parsed.data : DEFAULT_PACK_POLICY_ID;
|
|
23686
|
-
return BUILTIN_POLICIES[id].action;
|
|
23687
|
-
}
|
|
23688
|
-
var UsedByItem = external_exports.object({
|
|
23689
|
-
id: external_exports.string(),
|
|
23690
|
-
name: external_exports.string(),
|
|
23691
|
-
ruleCount: external_exports.number().int().nonnegative(),
|
|
23692
|
-
enabled: external_exports.boolean()
|
|
23693
|
-
}).meta({ id: "UsedByItem" });
|
|
23694
|
-
var PolicyListItem = external_exports.object({
|
|
23695
|
-
id: external_exports.string(),
|
|
23696
|
-
kind: PolicyKind,
|
|
23697
|
-
name: external_exports.string(),
|
|
23698
|
-
enabled: external_exports.boolean(),
|
|
23699
|
-
usedByCount: external_exports.number().int().nonnegative()
|
|
23700
|
-
}).meta({ id: "PolicyListItem" });
|
|
23701
|
-
var ListPoliciesResponse = external_exports.object({ items: external_exports.array(PolicyListItem) }).meta({ id: "ListPoliciesResponse" });
|
|
23702
|
-
var PolicyDetail = external_exports.object({
|
|
23703
|
-
specVersion: external_exports.literal(1),
|
|
23704
|
-
id: external_exports.string(),
|
|
23705
|
-
kind: PolicyKind,
|
|
23706
|
-
name: external_exports.string(),
|
|
23707
|
-
enabled: external_exports.boolean(),
|
|
23708
|
-
description: external_exports.string(),
|
|
23709
|
-
usedBy: external_exports.array(UsedByItem)
|
|
23710
|
-
}).meta({ id: "PolicyDetail" });
|
|
23711
|
-
var PolicyStatsResponse = external_exports.object({
|
|
23712
|
-
policies: external_exports.number().int().nonnegative(),
|
|
23713
|
-
builtin: external_exports.number().int().nonnegative(),
|
|
23714
|
-
custom: external_exports.number().int().nonnegative(),
|
|
23715
|
-
detectionsGoverned: external_exports.number().int().nonnegative()
|
|
23716
|
-
}).meta({ id: "PolicyStatsResponse" });
|
|
23896
|
+
return {
|
|
23897
|
+
...rest,
|
|
23898
|
+
lockedFields: known,
|
|
23899
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
|
|
23900
|
+
};
|
|
23901
|
+
}).meta({ id: "ManagedSettings" });
|
|
23717
23902
|
|
|
23718
23903
|
// ../../packages/schema/src/zod/project-files.ts
|
|
23719
23904
|
var ProjectFileInput = external_exports.object({
|
|
@@ -23836,7 +24021,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
23836
24021
|
timestamp: external_exports.iso.date(),
|
|
23837
24022
|
critical: external_exports.number().int().nonnegative(),
|
|
23838
24023
|
high: external_exports.number().int().nonnegative(),
|
|
23839
|
-
medium: external_exports.number().int().nonnegative()
|
|
24024
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24025
|
+
// Optional and additive, so a producer written against the earlier
|
|
24026
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24027
|
+
// absent case itself — the chart point requires a number.
|
|
24028
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
23840
24029
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
23841
24030
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
23842
24031
|
range: TimeRange,
|
|
@@ -23862,6 +24051,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
23862
24051
|
findingKey: external_exports.string(),
|
|
23863
24052
|
ruleId: external_exports.string(),
|
|
23864
24053
|
severity: Severity,
|
|
24054
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24055
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24056
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24057
|
+
repo: external_exports.string().optional(),
|
|
23865
24058
|
path: external_exports.string(),
|
|
23866
24059
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
23867
24060
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -23960,10 +24153,12 @@ var DismissRecommendedActionResponse = external_exports.object({ id: external_ex
|
|
|
23960
24153
|
var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
|
|
23961
24154
|
|
|
23962
24155
|
// ../../packages/schema/src/zod/settings-action.ts
|
|
24156
|
+
var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
|
|
24157
|
+
var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
|
|
23963
24158
|
var SaveSettingsInput = external_exports.object({
|
|
23964
24159
|
historicalAccess: external_exports.string(),
|
|
23965
|
-
modelJudgeConsent:
|
|
23966
|
-
historySyncConsent:
|
|
24160
|
+
modelJudgeConsent: ModelJudgeConsentChoice,
|
|
24161
|
+
historySyncConsent: HistorySyncConsentChoice,
|
|
23967
24162
|
vaultConsent: external_exports.string(),
|
|
23968
24163
|
vaultInlineReveal: external_exports.string()
|
|
23969
24164
|
});
|
|
@@ -24113,9 +24308,9 @@ function deriveReviewReasons(trust, transports) {
|
|
|
24113
24308
|
if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
|
|
24114
24309
|
return reasons;
|
|
24115
24310
|
}
|
|
24116
|
-
function buildReviewInfo(trust, transports) {
|
|
24311
|
+
function buildReviewInfo(trust, transports, decided) {
|
|
24117
24312
|
const reasons = deriveReviewReasons(trust, transports);
|
|
24118
|
-
return { needsReview: reasons.length > 0, reasons };
|
|
24313
|
+
return { needsReview: reasons.length > 0 && !decided, reasons };
|
|
24119
24314
|
}
|
|
24120
24315
|
function distinctTransports(transports) {
|
|
24121
24316
|
return Array.from(new Set(transports));
|
|
@@ -24186,8 +24381,8 @@ function tightenPerms(file2) {
|
|
|
24186
24381
|
}
|
|
24187
24382
|
|
|
24188
24383
|
// ../../packages/persistence/src/database.ts
|
|
24189
|
-
import { randomUUID as
|
|
24190
|
-
import { join as
|
|
24384
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
24385
|
+
import { dirname as dirname2, join as join7, sep } from "path";
|
|
24191
24386
|
import { DatabaseSync } from "node:sqlite";
|
|
24192
24387
|
|
|
24193
24388
|
// ../../packages/persistence/src/ids.ts
|
|
@@ -24431,6 +24626,10 @@ function allRows(stmt, params) {
|
|
|
24431
24626
|
if (Array.isArray(params)) return stmt.all(...params);
|
|
24432
24627
|
return stmt.all(params);
|
|
24433
24628
|
}
|
|
24629
|
+
function* iterateRows(stmt, params) {
|
|
24630
|
+
const rows = params === void 0 ? stmt.iterate() : Array.isArray(params) ? stmt.iterate(...params) : stmt.iterate(params);
|
|
24631
|
+
for (const row of rows) yield row;
|
|
24632
|
+
}
|
|
24434
24633
|
function getRow(stmt, params) {
|
|
24435
24634
|
if (params === void 0) return stmt.get();
|
|
24436
24635
|
if (Array.isArray(params)) return stmt.get(...params);
|
|
@@ -24899,10 +25098,17 @@ function ensureSyncedAtColumn(db, table2) {
|
|
|
24899
25098
|
if (!columns.includes("sync_claimed_at")) {
|
|
24900
25099
|
db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_claimed_at integer`);
|
|
24901
25100
|
}
|
|
25101
|
+
if (!columns.includes("outbox_owed")) {
|
|
25102
|
+
db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
|
|
25103
|
+
}
|
|
24902
25104
|
db.exec(
|
|
24903
25105
|
`CREATE INDEX IF NOT EXISTS idx_audit_events_sync
|
|
24904
25106
|
ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
|
|
24905
25107
|
);
|
|
25108
|
+
db.exec(
|
|
25109
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
|
|
25110
|
+
ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
|
|
25111
|
+
);
|
|
24906
25112
|
db.exec(
|
|
24907
25113
|
`CREATE INDEX IF NOT EXISTS idx_audit_claimed
|
|
24908
25114
|
ON audit_events (sync_claimed_at) WHERE sync_claimed_at IS NOT NULL`
|
|
@@ -25007,7 +25213,6 @@ function decodeKeysetCursor(cursor) {
|
|
|
25007
25213
|
// ../../packages/persistence/src/repositories/activity.ts
|
|
25008
25214
|
var DAY_MS = 864e5;
|
|
25009
25215
|
var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
|
|
25010
|
-
var LAST_ACTIVITY_EXPR = `max(started_at, coalesce(ended_at, started_at))`;
|
|
25011
25216
|
function defaultTimeZone() {
|
|
25012
25217
|
try {
|
|
25013
25218
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -25062,6 +25267,7 @@ var DB_EVENT_TYPE_TO_KIND = {
|
|
|
25062
25267
|
error: "error",
|
|
25063
25268
|
active: "active"
|
|
25064
25269
|
};
|
|
25270
|
+
var TIMELINE_EVENT_TYPES = Object.keys(DB_EVENT_TYPE_TO_KIND).map((kind) => `'${kind}'`).join(", ");
|
|
25065
25271
|
function safeParseStringArray(raw) {
|
|
25066
25272
|
if (!raw) return [];
|
|
25067
25273
|
const parsed = safeJson(raw, null);
|
|
@@ -25135,6 +25341,37 @@ var TIMELINE_COLUMNS = `
|
|
|
25135
25341
|
json_extract(attributes, '$.targetId') AS target_id,
|
|
25136
25342
|
json_extract(attributes, '$.internal') AS internal,
|
|
25137
25343
|
json_extract(attributes, '$.flagged') AS flagged`;
|
|
25344
|
+
var LLM_USAGE_SELECT = `
|
|
25345
|
+
SELECT root_session_id AS sessionId,
|
|
25346
|
+
provider,
|
|
25347
|
+
model,
|
|
25348
|
+
service_tier AS serviceTier,
|
|
25349
|
+
coalesce(sum(input_tokens), 0) AS inputTokens,
|
|
25350
|
+
coalesce(sum(output_tokens), 0) AS outputTokens,
|
|
25351
|
+
coalesce(sum(cache_creation_input_tokens), 0) AS cacheCreationTokens,
|
|
25352
|
+
coalesce(sum(cache_read_input_tokens), 0) AS cacheReadTokens,
|
|
25353
|
+
coalesce(sum(ephemeral_1h_input_tokens), 0) AS ephemeral1hTokens,
|
|
25354
|
+
coalesce(sum(ephemeral_5m_input_tokens), 0) AS ephemeral5mTokens,
|
|
25355
|
+
coalesce(sum(web_search_requests), 0) AS webSearchRequests`;
|
|
25356
|
+
var LLM_USAGE_SCOPE = `event_type = 'llm_call' AND attributes IS NOT NULL AND root_session_id IS NOT NULL`;
|
|
25357
|
+
var LLM_USAGE_GROUP = `GROUP BY root_session_id, provider, model, service_tier`;
|
|
25358
|
+
function usageLeaves(rows) {
|
|
25359
|
+
return rows.map((row) => {
|
|
25360
|
+
const attributes = {
|
|
25361
|
+
input_tokens: row.inputTokens,
|
|
25362
|
+
output_tokens: row.outputTokens,
|
|
25363
|
+
cache_creation_input_tokens: row.cacheCreationTokens,
|
|
25364
|
+
cache_read_input_tokens: row.cacheReadTokens,
|
|
25365
|
+
ephemeral_1h_input_tokens: row.ephemeral1hTokens,
|
|
25366
|
+
ephemeral_5m_input_tokens: row.ephemeral5mTokens,
|
|
25367
|
+
web_search_requests: row.webSearchRequests
|
|
25368
|
+
};
|
|
25369
|
+
if (row.provider !== null) attributes.provider = row.provider;
|
|
25370
|
+
if (row.model !== null) attributes.model = row.model;
|
|
25371
|
+
if (row.serviceTier !== null) attributes.service_tier = row.serviceTier;
|
|
25372
|
+
return { sessionId: row.sessionId, attributes };
|
|
25373
|
+
});
|
|
25374
|
+
}
|
|
25138
25375
|
var SESSION_ROOT = `event_type = 'session'`;
|
|
25139
25376
|
var HAS_ACTIVITY = `EXISTS (
|
|
25140
25377
|
SELECT 1 FROM audit_events c
|
|
@@ -25160,16 +25397,17 @@ var SqliteActivityRepository = class {
|
|
|
25160
25397
|
const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
|
|
25161
25398
|
const liveNow = countScalar(
|
|
25162
25399
|
this.db,
|
|
25163
|
-
`SELECT count(*) AS n FROM audit_events s
|
|
25400
|
+
`SELECT count(*) AS n FROM audit_events s INDEXED BY sqlite_autoindex_audit_events_1
|
|
25164
25401
|
WHERE s.event_type = 'session' AND s.ended_at IS NULL
|
|
25165
|
-
AND
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
|
|
25170
|
-
|
|
25171
|
-
|
|
25172
|
-
|
|
25402
|
+
AND s.id IN (
|
|
25403
|
+
SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
|
|
25404
|
+
UNION
|
|
25405
|
+
SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
|
|
25406
|
+
WHERE started_at >= ?
|
|
25407
|
+
UNION
|
|
25408
|
+
SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
|
|
25409
|
+
WHERE ended_at >= ?)`,
|
|
25410
|
+
[liveThreshold, liveThreshold, liveThreshold]
|
|
25173
25411
|
);
|
|
25174
25412
|
const toolCallsToday = countScalar(
|
|
25175
25413
|
this.db,
|
|
@@ -25222,7 +25460,8 @@ var SqliteActivityRepository = class {
|
|
|
25222
25460
|
SELECT 1 FROM audit_events d
|
|
25223
25461
|
WHERE d.root_session_id = audit_events.id
|
|
25224
25462
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25225
|
-
OR json_extract(d.attributes, '$.detail')
|
|
25463
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
25464
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25226
25465
|
);
|
|
25227
25466
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25228
25467
|
}
|
|
@@ -25299,7 +25538,7 @@ var SqliteActivityRepository = class {
|
|
|
25299
25538
|
this.db.prepare(
|
|
25300
25539
|
`SELECT ${TIMELINE_COLUMNS}
|
|
25301
25540
|
FROM audit_events
|
|
25302
|
-
WHERE id = ? OR root_session_id = ?
|
|
25541
|
+
WHERE (id = ? OR root_session_id = ?) AND event_type IN (${TIMELINE_EVENT_TYPES})
|
|
25303
25542
|
ORDER BY started_at ASC, id ASC`
|
|
25304
25543
|
),
|
|
25305
25544
|
[sessionId, sessionId]
|
|
@@ -25312,14 +25551,14 @@ var SqliteActivityRepository = class {
|
|
|
25312
25551
|
coalesce(sum(output_tokens), 0) AS output,
|
|
25313
25552
|
coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
|
|
25314
25553
|
coalesce(sum(cache_read_input_tokens), 0) AS cache_read
|
|
25315
|
-
FROM audit_events
|
|
25554
|
+
FROM audit_events INDEXED BY idx_audit_session_type
|
|
25316
25555
|
WHERE root_session_id = ? AND event_type = 'llm_call'`
|
|
25317
25556
|
),
|
|
25318
25557
|
[sessionId]
|
|
25319
25558
|
) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
|
|
25320
25559
|
const primaryModel = getRow(
|
|
25321
25560
|
this.db.prepare(
|
|
25322
|
-
`SELECT model, provider FROM audit_events
|
|
25561
|
+
`SELECT model, provider FROM audit_events INDEXED BY idx_audit_session_type
|
|
25323
25562
|
WHERE root_session_id = ? AND event_type = 'llm_call'
|
|
25324
25563
|
ORDER BY started_at ASC, id ASC
|
|
25325
25564
|
LIMIT 1`
|
|
@@ -25330,7 +25569,7 @@ var SqliteActivityRepository = class {
|
|
|
25330
25569
|
this.db.prepare(
|
|
25331
25570
|
`SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
25332
25571
|
count(*) AS n
|
|
25333
|
-
FROM audit_events
|
|
25572
|
+
FROM audit_events INDEXED BY idx_audit_session
|
|
25334
25573
|
WHERE root_session_id = ? AND event_type = 'tool_call'
|
|
25335
25574
|
GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
|
|
25336
25575
|
),
|
|
@@ -25338,7 +25577,7 @@ var SqliteActivityRepository = class {
|
|
|
25338
25577
|
);
|
|
25339
25578
|
const modelRows = allRows(
|
|
25340
25579
|
this.db.prepare(
|
|
25341
|
-
`SELECT DISTINCT model FROM audit_events
|
|
25580
|
+
`SELECT DISTINCT model FROM audit_events INDEXED BY idx_audit_session_type
|
|
25342
25581
|
WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
|
|
25343
25582
|
ORDER BY model`
|
|
25344
25583
|
),
|
|
@@ -25347,7 +25586,7 @@ var SqliteActivityRepository = class {
|
|
|
25347
25586
|
const derivedModels = modelRows.map((r) => r.model);
|
|
25348
25587
|
const commits = countScalar(
|
|
25349
25588
|
this.db,
|
|
25350
|
-
`SELECT count(*) AS n FROM audit_events
|
|
25589
|
+
`SELECT count(*) AS n FROM audit_events INDEXED BY idx_audit_session
|
|
25351
25590
|
WHERE root_session_id = ? AND event_type = 'commit'`,
|
|
25352
25591
|
[sessionId]
|
|
25353
25592
|
);
|
|
@@ -25383,25 +25622,57 @@ var SqliteActivityRepository = class {
|
|
|
25383
25622
|
return Promise.resolve(session);
|
|
25384
25623
|
}
|
|
25385
25624
|
/**
|
|
25386
|
-
* Cross-session token report — every `llm_call`
|
|
25387
|
-
* `started_at >= fromMs`
|
|
25388
|
-
* USD cost DERIVED at read time via the shared
|
|
25389
|
-
*
|
|
25390
|
-
*
|
|
25391
|
-
*
|
|
25625
|
+
* Cross-session token report — every `llm_call` in the store (or in a
|
|
25626
|
+
* `started_at >= fromMs` window, the Activity page's range) grouped per
|
|
25627
|
+
* session, with USD cost DERIVED at read time via the shared
|
|
25628
|
+
* `defaultCostModel` (never stored). The caller collapses these onto
|
|
25629
|
+
* per-model rows with `aggregateTokenUsage`.
|
|
25630
|
+
*
|
|
25631
|
+
* Grouped in SQL over `idx_audit_llm_usage` — one entry per call carrying
|
|
25632
|
+
* the members the rollup sums — and priced once per group, which is exact
|
|
25633
|
+
* (see LlmUsageRow). Reading the bags and folding them in JS measured 31 ms
|
|
25634
|
+
* for a seven-day window at 50k calls, and naming the VIRTUAL columns
|
|
25635
|
+
* against the table 40 ms, since each is a json_extract recomputed per row;
|
|
25636
|
+
* the index stores the values once, at write, and answers the same window in
|
|
25637
|
+
* 8.5 ms. `INDEXED BY` is deliberate: with or without ANALYZE statistics the
|
|
25638
|
+
* planner prefers the general event-type index and fetches every row to
|
|
25639
|
+
* recompute the columns it could have read. The index is one every open
|
|
25640
|
+
* store carries, since opening runs the migrations, so the hard requirement
|
|
25641
|
+
* `INDEXED BY` introduces is already met; token-rollup-plans.test.ts pins
|
|
25642
|
+
* the plan. All-time is a scan of the whole index — still one narrow entry
|
|
25643
|
+
* per call, no bag parsed.
|
|
25392
25644
|
*/
|
|
25393
25645
|
tokenReports(fromMs) {
|
|
25394
|
-
const
|
|
25395
|
-
|
|
25646
|
+
const rows = allRows(
|
|
25647
|
+
this.db.prepare(
|
|
25648
|
+
`${LLM_USAGE_SELECT}
|
|
25649
|
+
FROM audit_events INDEXED BY idx_audit_llm_usage
|
|
25650
|
+
WHERE ${LLM_USAGE_SCOPE}${fromMs === void 0 ? "" : " AND started_at >= ?"}
|
|
25651
|
+
${LLM_USAGE_GROUP}`
|
|
25652
|
+
),
|
|
25653
|
+
fromMs === void 0 ? void 0 : [fromMs]
|
|
25654
|
+
);
|
|
25655
|
+
return Promise.resolve(buildTokenReports(usageLeaves(rows), defaultCostModel));
|
|
25396
25656
|
}
|
|
25397
25657
|
/**
|
|
25398
|
-
* One session's token report — its `llm_call`
|
|
25399
|
-
* model) with derived cost, or `null` when the session made no
|
|
25400
|
-
* (an empty/tool-only session). Feeds the session-detail pane's
|
|
25401
|
-
* breakdown + estimated cost.
|
|
25658
|
+
* One session's token report — its `llm_call`s grouped per (provider,
|
|
25659
|
+
* model, tier) with derived cost, or `null` when the session made no
|
|
25660
|
+
* `llm_call`s (an empty/tool-only session). Feeds the session-detail pane's
|
|
25661
|
+
* per-model breakdown + estimated cost. The same rollup as `tokenReports`,
|
|
25662
|
+
* seeking one root through a root-led `llm_call` index; the bag-reading fold
|
|
25663
|
+
* it replaces walked every `llm_call` in the store to find one session's.
|
|
25402
25664
|
*/
|
|
25403
25665
|
tokenReportForSession(sessionId) {
|
|
25404
|
-
const
|
|
25666
|
+
const rows = allRows(
|
|
25667
|
+
this.db.prepare(
|
|
25668
|
+
`${LLM_USAGE_SELECT}
|
|
25669
|
+
FROM audit_events
|
|
25670
|
+
WHERE ${LLM_USAGE_SCOPE} AND root_session_id = ?
|
|
25671
|
+
${LLM_USAGE_GROUP}`
|
|
25672
|
+
),
|
|
25673
|
+
[sessionId]
|
|
25674
|
+
);
|
|
25675
|
+
const reports = buildTokenReports(usageLeaves(rows), defaultCostModel);
|
|
25405
25676
|
return Promise.resolve(reports[0] ?? null);
|
|
25406
25677
|
}
|
|
25407
25678
|
/**
|
|
@@ -25425,42 +25696,6 @@ var SqliteActivityRepository = class {
|
|
|
25425
25696
|
for (const row of rows) seen.add(toHarness(row.harness));
|
|
25426
25697
|
return Promise.resolve([...seen]);
|
|
25427
25698
|
}
|
|
25428
|
-
/**
|
|
25429
|
-
* The raw `llm_call` leaves (session id + parsed attribute bag) for the token
|
|
25430
|
-
* rollups, optionally narrowed to one session and/or a `started_at >= fromMs`
|
|
25431
|
-
* window. A leaf whose attributes blob is NULL or unparseable is skipped
|
|
25432
|
-
* (best-effort read — a corrupt bag never breaks the report). `root_session_id`
|
|
25433
|
-
* is the leaf's session (the reconciler sets parent_id = root_session_id).
|
|
25434
|
-
*/
|
|
25435
|
-
readLlmCallLeaves(opts = {}) {
|
|
25436
|
-
const conditions = ["event_type = 'llm_call'", "attributes IS NOT NULL"];
|
|
25437
|
-
const params = [];
|
|
25438
|
-
if (opts.sessionId !== void 0) {
|
|
25439
|
-
conditions.push("root_session_id = ?");
|
|
25440
|
-
params.push(opts.sessionId);
|
|
25441
|
-
}
|
|
25442
|
-
if (opts.fromMs !== void 0) {
|
|
25443
|
-
conditions.push("started_at >= ?");
|
|
25444
|
-
params.push(opts.fromMs);
|
|
25445
|
-
}
|
|
25446
|
-
const rows = allRows(
|
|
25447
|
-
this.db.prepare(
|
|
25448
|
-
`SELECT root_session_id AS sessionId, attributes
|
|
25449
|
-
FROM audit_events
|
|
25450
|
-
WHERE ${conditions.join(" AND ")}`
|
|
25451
|
-
),
|
|
25452
|
-
params
|
|
25453
|
-
);
|
|
25454
|
-
return mapRowsTolerant(
|
|
25455
|
-
rows.filter(
|
|
25456
|
-
(row) => row.sessionId !== null
|
|
25457
|
-
),
|
|
25458
|
-
(row) => ({
|
|
25459
|
-
sessionId: row.sessionId,
|
|
25460
|
-
attributes: JSON.parse(row.attributes)
|
|
25461
|
-
})
|
|
25462
|
-
);
|
|
25463
|
-
}
|
|
25464
25699
|
/**
|
|
25465
25700
|
* Per-session turns/findings/shares + last-activity for a page of session ids,
|
|
25466
25701
|
* in grouped queries (not one per row). An id with no matching rows still
|
|
@@ -25475,20 +25710,23 @@ var SqliteActivityRepository = class {
|
|
|
25475
25710
|
const inClause = placeholders(sessionIds.length);
|
|
25476
25711
|
const lastActivityRows = allRows(
|
|
25477
25712
|
this.db.prepare(
|
|
25478
|
-
`SELECT
|
|
25479
|
-
|
|
25480
|
-
|
|
25713
|
+
`SELECT ids.value AS id,
|
|
25714
|
+
(SELECT max(started_at) FROM audit_events e WHERE e.root_session_id = ids.value) AS ms,
|
|
25715
|
+
(SELECT max(ended_at) FROM audit_events e
|
|
25716
|
+
WHERE e.root_session_id = ids.value AND e.ended_at IS NOT NULL) AS me
|
|
25717
|
+
FROM json_each(?) AS ids`
|
|
25481
25718
|
),
|
|
25482
|
-
sessionIds
|
|
25719
|
+
[JSON.stringify(sessionIds)]
|
|
25483
25720
|
);
|
|
25484
25721
|
for (const row of lastActivityRows) {
|
|
25485
|
-
if (row.id === null) continue;
|
|
25486
25722
|
const entry = result.get(row.id);
|
|
25487
|
-
|
|
25723
|
+
const last = Math.max(row.ms ?? 0, row.me ?? 0);
|
|
25724
|
+
if (entry && last > 0) entry.lastActivityMs = last;
|
|
25488
25725
|
}
|
|
25489
25726
|
const turnsRows = allRows(
|
|
25490
25727
|
this.db.prepare(
|
|
25491
|
-
`SELECT root_session_id AS id, count(*) AS n
|
|
25728
|
+
`SELECT root_session_id AS id, count(*) AS n
|
|
25729
|
+
FROM audit_events INDEXED BY idx_audit_session_prompt
|
|
25492
25730
|
WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
|
|
25493
25731
|
GROUP BY root_session_id`
|
|
25494
25732
|
),
|
|
@@ -25503,7 +25741,7 @@ var SqliteActivityRepository = class {
|
|
|
25503
25741
|
this.db.prepare(
|
|
25504
25742
|
`SELECT root_session_id AS id,
|
|
25505
25743
|
count(DISTINCT json_extract(attributes, '$.run_key')) AS n
|
|
25506
|
-
FROM audit_events
|
|
25744
|
+
FROM audit_events INDEXED BY idx_audit_session_run_key
|
|
25507
25745
|
WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
|
|
25508
25746
|
AND json_extract(attributes, '$.run_key') IS NOT NULL
|
|
25509
25747
|
GROUP BY root_session_id`
|
|
@@ -25533,7 +25771,7 @@ var SqliteActivityRepository = class {
|
|
|
25533
25771
|
this.db.prepare(
|
|
25534
25772
|
`SELECT root_session_id AS id,
|
|
25535
25773
|
count(DISTINCT json_extract(attributes, '$.destination')) AS n
|
|
25536
|
-
FROM audit_events
|
|
25774
|
+
FROM audit_events INDEXED BY idx_audit_session_share
|
|
25537
25775
|
WHERE root_session_id IN (${inClause}) AND event_type = 'share'
|
|
25538
25776
|
GROUP BY root_session_id`
|
|
25539
25777
|
),
|
|
@@ -26561,24 +26799,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26561
26799
|
)`;
|
|
26562
26800
|
|
|
26563
26801
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26564
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26565
|
-
var SCAN_BATCH_ROWS = 1e3;
|
|
26566
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26567
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26568
|
-
function compareLocationOrder(a, b) {
|
|
26569
|
-
return compareFindingGroupOrder(
|
|
26570
|
-
{
|
|
26571
|
-
severity: a.maxSeverity,
|
|
26572
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26573
|
-
id: ""
|
|
26574
|
-
},
|
|
26575
|
-
{
|
|
26576
|
-
severity: b.maxSeverity,
|
|
26577
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26578
|
-
id: ""
|
|
26579
|
-
}
|
|
26580
|
-
);
|
|
26581
|
-
}
|
|
26582
26802
|
var CONCAT_SEP = ",";
|
|
26583
26803
|
var TUPLE_SEP = "|";
|
|
26584
26804
|
function splitConcat(value) {
|
|
@@ -26591,6 +26811,25 @@ function deriveInstanceStatus(row) {
|
|
|
26591
26811
|
latestResolutionStatus: row.latest_status
|
|
26592
26812
|
});
|
|
26593
26813
|
}
|
|
26814
|
+
function toFlatFindingRow(r) {
|
|
26815
|
+
return {
|
|
26816
|
+
id: r.id,
|
|
26817
|
+
ruleId: r.rule_id,
|
|
26818
|
+
category: r.category,
|
|
26819
|
+
severity: r.severity,
|
|
26820
|
+
maskedMatch: r.masked_match,
|
|
26821
|
+
actionTaken: r.action_taken,
|
|
26822
|
+
confidence: r.confidence,
|
|
26823
|
+
occurredAt: epochMillisToIso(r.occurred_at),
|
|
26824
|
+
sourceTool: r.source_tool,
|
|
26825
|
+
repo: r.repo ?? "",
|
|
26826
|
+
file: r.file ?? "",
|
|
26827
|
+
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
26828
|
+
eventId: r.event_id,
|
|
26829
|
+
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
26830
|
+
status: deriveInstanceStatus(r)
|
|
26831
|
+
};
|
|
26832
|
+
}
|
|
26594
26833
|
function encodeGroupCursor(group) {
|
|
26595
26834
|
const payload = {
|
|
26596
26835
|
sev: group.severity,
|
|
@@ -26611,13 +26850,48 @@ function decodeGroupCursor(cursor) {
|
|
|
26611
26850
|
return null;
|
|
26612
26851
|
}
|
|
26613
26852
|
function firstAfter(sorted, cursor) {
|
|
26614
|
-
const index = sorted.findIndex((
|
|
26853
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
26615
26854
|
return index === -1 ? sorted.length : index;
|
|
26616
26855
|
}
|
|
26617
26856
|
function findDeepLinked(sorted, page, id) {
|
|
26618
|
-
if (page.some((
|
|
26619
|
-
return sorted.find((
|
|
26857
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
26858
|
+
return sorted.find((t) => t.id === id);
|
|
26620
26859
|
}
|
|
26860
|
+
function encodeLocationCursor(location) {
|
|
26861
|
+
const payload = {
|
|
26862
|
+
sev: location.maxSeverity,
|
|
26863
|
+
t: location.latestDetectedAt,
|
|
26864
|
+
r: location.repo,
|
|
26865
|
+
f: location.file
|
|
26866
|
+
};
|
|
26867
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
26868
|
+
}
|
|
26869
|
+
function decodeLocationCursor(cursor) {
|
|
26870
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
26871
|
+
if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
|
|
26872
|
+
return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
|
|
26873
|
+
}
|
|
26874
|
+
return null;
|
|
26875
|
+
}
|
|
26876
|
+
function firstLocationAfter(sorted, cursor) {
|
|
26877
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
26878
|
+
return index === -1 ? sorted.length : index;
|
|
26879
|
+
}
|
|
26880
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
26881
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
26882
|
+
return sorted.find((l) => l.id === id);
|
|
26883
|
+
}
|
|
26884
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
26885
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
26886
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
26887
|
+
e.started_at AS occurred_at,
|
|
26888
|
+
e.source_tool AS source_tool,
|
|
26889
|
+
e.repo AS repo,
|
|
26890
|
+
e.file_path AS file,
|
|
26891
|
+
e.tool_name AS tool_name,
|
|
26892
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
26893
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
26894
|
+
${latestResolutionStatusSql("f")} AS latest_status`;
|
|
26621
26895
|
var DAY_MS3 = 864e5;
|
|
26622
26896
|
var SqliteFindingsRepository = class {
|
|
26623
26897
|
constructor(db) {
|
|
@@ -26666,7 +26940,7 @@ var SqliteFindingsRepository = class {
|
|
|
26666
26940
|
this.db.prepare(
|
|
26667
26941
|
`SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
|
|
26668
26942
|
f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
|
|
26669
|
-
|
|
26943
|
+
e.source_tool AS source_tool,
|
|
26670
26944
|
e.event_type AS kind
|
|
26671
26945
|
FROM audit_events e
|
|
26672
26946
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
@@ -26738,30 +27012,26 @@ var SqliteFindingsRepository = class {
|
|
|
26738
27012
|
);
|
|
26739
27013
|
}
|
|
26740
27014
|
/**
|
|
26741
|
-
*
|
|
26742
|
-
*
|
|
26743
|
-
*
|
|
26744
|
-
*
|
|
26745
|
-
*
|
|
26746
|
-
*
|
|
26747
|
-
*
|
|
26748
|
-
*
|
|
26749
|
-
*
|
|
26750
|
-
*
|
|
26751
|
-
*
|
|
26752
|
-
*
|
|
27015
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
27016
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
27017
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
27018
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
27019
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
27020
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
27021
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
27022
|
+
* derived status was requested.
|
|
27023
|
+
*
|
|
27024
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
27025
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
27026
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
27027
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
27028
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
26753
27029
|
*
|
|
26754
|
-
* Two reads, neither of which materializes a row per finding:
|
|
26755
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
26756
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
26757
|
-
* latest, search text);
|
|
26758
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
26759
|
-
* populate `instances` for the table's expanded rows.
|
|
26760
27030
|
* The aggregates carry raw DB values and are translated by the same
|
|
26761
|
-
* @akasecurity/schema mappers
|
|
26762
|
-
* rule is ever restated in SQL.
|
|
27031
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
27032
|
+
* status rule is ever restated in SQL.
|
|
26763
27033
|
*/
|
|
26764
|
-
|
|
27034
|
+
listFindingTypes(query) {
|
|
26765
27035
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
26766
27036
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
26767
27037
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -26774,57 +27044,7 @@ var SqliteFindingsRepository = class {
|
|
|
26774
27044
|
predicate,
|
|
26775
27045
|
params: sessionParams
|
|
26776
27046
|
});
|
|
26777
|
-
const
|
|
26778
|
-
this.db.prepare(
|
|
26779
|
-
`SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
|
|
26780
|
-
occurred_at, source_tool, repo, file, tool_name, event_id, session_id,
|
|
26781
|
-
kind, finding_key, latest_status
|
|
26782
|
-
FROM (
|
|
26783
|
-
SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
26784
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
26785
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
26786
|
-
e.started_at AS occurred_at,
|
|
26787
|
-
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
26788
|
-
json_extract(e.attributes, '$.repo') AS repo,
|
|
26789
|
-
json_extract(e.attributes, '$.file_path') AS file,
|
|
26790
|
-
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
26791
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
26792
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
26793
|
-
latest.status AS latest_status,
|
|
26794
|
-
ROW_NUMBER() OVER (
|
|
26795
|
-
PARTITION BY d.rule_id
|
|
26796
|
-
ORDER BY e.started_at DESC, f.id DESC
|
|
26797
|
-
) AS rn
|
|
26798
|
-
FROM inspection_findings f
|
|
26799
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
26800
|
-
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
26801
|
-
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
26802
|
-
ON latest.finding_key = f.finding_key
|
|
26803
|
-
${predicate}
|
|
26804
|
-
)
|
|
26805
|
-
WHERE rn <= :cap
|
|
26806
|
-
ORDER BY occurred_at DESC, id DESC`
|
|
26807
|
-
),
|
|
26808
|
-
{ cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
|
|
26809
|
-
);
|
|
26810
|
-
const groupable = rows.map((r) => ({
|
|
26811
|
-
id: r.id,
|
|
26812
|
-
ruleId: r.rule_id,
|
|
26813
|
-
category: r.category,
|
|
26814
|
-
severity: r.severity,
|
|
26815
|
-
maskedMatch: r.masked_match,
|
|
26816
|
-
actionTaken: r.action_taken,
|
|
26817
|
-
confidence: r.confidence,
|
|
26818
|
-
occurredAt: epochMillisToIso(r.occurred_at),
|
|
26819
|
-
sourceTool: r.source_tool,
|
|
26820
|
-
repo: r.repo ?? "",
|
|
26821
|
-
file: r.file ?? "",
|
|
26822
|
-
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
26823
|
-
eventId: r.event_id,
|
|
26824
|
-
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
26825
|
-
status: deriveInstanceStatus(r)
|
|
26826
|
-
}));
|
|
26827
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
27047
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
26828
27048
|
const filterOpts = {
|
|
26829
27049
|
severity: query.severity,
|
|
26830
27050
|
providers: query.provider,
|
|
@@ -26833,30 +27053,25 @@ var SqliteFindingsRepository = class {
|
|
|
26833
27053
|
subtype: query.subtype,
|
|
26834
27054
|
q: query.q
|
|
26835
27055
|
};
|
|
26836
|
-
const facets = computeFindingFacets(
|
|
26837
|
-
const sorted =
|
|
27056
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
27057
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
26838
27058
|
const statusFilter = query.status ?? [];
|
|
26839
27059
|
const totals = {
|
|
26840
|
-
findings: sorted.reduce((acc,
|
|
26841
|
-
if (statusFilter.length === 0) return acc +
|
|
26842
|
-
const agg = aggregates.get(
|
|
26843
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
27060
|
+
findings: sorted.reduce((acc, t) => {
|
|
27061
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
27062
|
+
const agg = aggregates.get(t.id);
|
|
27063
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
26844
27064
|
}, 0),
|
|
26845
|
-
|
|
27065
|
+
types: sorted.length
|
|
26846
27066
|
};
|
|
26847
|
-
const limit = query.limit ??
|
|
27067
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
26848
27068
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
26849
27069
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
26850
27070
|
const page = sorted.slice(start, start + limit);
|
|
26851
27071
|
const lastOnPage = page.at(-1);
|
|
26852
27072
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
26853
27073
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
26854
|
-
const
|
|
26855
|
-
const narrow = (g) => statusSet ? {
|
|
26856
|
-
...g,
|
|
26857
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
26858
|
-
} : g;
|
|
26859
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
27074
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
26860
27075
|
return Promise.resolve({
|
|
26861
27076
|
totals,
|
|
26862
27077
|
facets,
|
|
@@ -26867,7 +27082,7 @@ var SqliteFindingsRepository = class {
|
|
|
26867
27082
|
}
|
|
26868
27083
|
/**
|
|
26869
27084
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
26870
|
-
*
|
|
27085
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
26871
27086
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
26872
27087
|
*
|
|
26873
27088
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -26909,8 +27124,10 @@ var SqliteFindingsRepository = class {
|
|
|
26909
27124
|
*
|
|
26910
27125
|
* The scan runs from the top of the scope on every request, not from the
|
|
26911
27126
|
* cursor: `totals` and `facets` describe the whole filtered scope and must not
|
|
26912
|
-
* move as the caller pages. Rows
|
|
26913
|
-
*
|
|
27127
|
+
* move as the caller pages. Rows come off ONE statement, iterated rather
|
|
27128
|
+
* than materialized (`scanFindingRows`), so memory stays flat while the
|
|
27129
|
+
* counting runs — a generator streaming the index order, not a sequence of
|
|
27130
|
+
* fetched batches; only the page itself is retained.
|
|
26914
27131
|
*/
|
|
26915
27132
|
listFindingInstances(query) {
|
|
26916
27133
|
const opts = {
|
|
@@ -26926,6 +27143,10 @@ var SqliteFindingsRepository = class {
|
|
|
26926
27143
|
};
|
|
26927
27144
|
const limit = query.limit ?? DEFAULT_FLAT_FINDINGS_LIMIT;
|
|
26928
27145
|
const cursor = query.cursor === void 0 ? null : decodeKeysetCursor(query.cursor);
|
|
27146
|
+
const isPastCursor = cursor === null ? () => true : (row) => {
|
|
27147
|
+
const rowMs = isoToEpochMillis(row.occurredAt);
|
|
27148
|
+
return rowMs <= cursor.startedAtMs && (rowMs < cursor.startedAtMs || row.id < cursor.id);
|
|
27149
|
+
};
|
|
26929
27150
|
const accumulator = createInstanceFacetAccumulator(opts);
|
|
26930
27151
|
const items = [];
|
|
26931
27152
|
let total = 0;
|
|
@@ -26938,6 +27159,7 @@ var SqliteFindingsRepository = class {
|
|
|
26938
27159
|
accumulator.add(row);
|
|
26939
27160
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
26940
27161
|
total += 1;
|
|
27162
|
+
if (!isPastCursor(row)) continue;
|
|
26941
27163
|
if (items.length < limit) {
|
|
26942
27164
|
items.push(toInstanceDetail(row));
|
|
26943
27165
|
last = row;
|
|
@@ -26946,15 +27168,6 @@ var SqliteFindingsRepository = class {
|
|
|
26946
27168
|
}
|
|
26947
27169
|
}
|
|
26948
27170
|
const nextCursor = hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null;
|
|
26949
|
-
if (cursor !== null) {
|
|
26950
|
-
const resumed = this.pageAfter(cursor, opts, limit, query);
|
|
26951
|
-
return Promise.resolve({
|
|
26952
|
-
totals: { findings: total },
|
|
26953
|
-
facets: accumulator.facets(),
|
|
26954
|
-
items: resumed.items,
|
|
26955
|
-
nextCursor: resumed.nextCursor
|
|
26956
|
-
});
|
|
26957
|
-
}
|
|
26958
27171
|
return Promise.resolve({
|
|
26959
27172
|
totals: { findings: total },
|
|
26960
27173
|
facets: accumulator.facets(),
|
|
@@ -26963,42 +27176,25 @@ var SqliteFindingsRepository = class {
|
|
|
26963
27176
|
});
|
|
26964
27177
|
}
|
|
26965
27178
|
/**
|
|
26966
|
-
* The
|
|
26967
|
-
* counting pass because that one starts at the top of the scope by design;
|
|
26968
|
-
* this one narrows the scan with the same keyset predicate the activity list
|
|
26969
|
-
* uses, so a later page costs less than the first rather than more.
|
|
26970
|
-
*/
|
|
26971
|
-
pageAfter(cursor, opts, limit, query) {
|
|
26972
|
-
const items = [];
|
|
26973
|
-
let last;
|
|
26974
|
-
let hasMore = false;
|
|
26975
|
-
for (const row of this.scanFindingRows({
|
|
26976
|
-
sessionId: query.sessionId,
|
|
26977
|
-
from: query.from,
|
|
26978
|
-
after: cursor
|
|
26979
|
-
})) {
|
|
26980
|
-
if (!matchesInstanceFilters(row, opts)) continue;
|
|
26981
|
-
if (items.length < limit) {
|
|
26982
|
-
items.push(toInstanceDetail(row));
|
|
26983
|
-
last = row;
|
|
26984
|
-
} else {
|
|
26985
|
-
hasMore = true;
|
|
26986
|
-
break;
|
|
26987
|
-
}
|
|
26988
|
-
}
|
|
26989
|
-
return {
|
|
26990
|
-
items,
|
|
26991
|
-
nextCursor: hasMore && last ? encodeKeysetCursor({ startedAtMs: isoToEpochMillis(last.occurredAt), id: last.id }) : null
|
|
26992
|
-
};
|
|
26993
|
-
}
|
|
26994
|
-
/**
|
|
26995
|
-
* The same findings folded by location: repository, then file within it.
|
|
27179
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
26996
27180
|
*
|
|
26997
27181
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
26998
|
-
* the local store relates a finding to
|
|
26999
|
-
*
|
|
27000
|
-
* empty-string bucket, which
|
|
27001
|
-
*
|
|
27182
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
27183
|
+
* by instead. A repo or file the event did not record folds into the
|
|
27184
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
27185
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
27186
|
+
*
|
|
27187
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
27188
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
27189
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
27190
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
27191
|
+
* place.
|
|
27192
|
+
*
|
|
27193
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
27194
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
27195
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
27196
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
27197
|
+
* its fields.
|
|
27002
27198
|
*/
|
|
27003
27199
|
listFindingLocations(query) {
|
|
27004
27200
|
const opts = {
|
|
@@ -27010,13 +27206,16 @@ var SqliteFindingsRepository = class {
|
|
|
27010
27206
|
tools: query.tool,
|
|
27011
27207
|
q: query.q
|
|
27012
27208
|
};
|
|
27013
|
-
const limit = query.limit ??
|
|
27209
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
27210
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27014
27211
|
const byRepo = /* @__PURE__ */ new Map();
|
|
27212
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27015
27213
|
let total = 0;
|
|
27016
27214
|
for (const row of this.scanFindingRows({
|
|
27017
27215
|
sessionId: query.sessionId,
|
|
27018
27216
|
from: query.from
|
|
27019
27217
|
})) {
|
|
27218
|
+
accumulator.add(row);
|
|
27020
27219
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27021
27220
|
total += 1;
|
|
27022
27221
|
let files = byRepo.get(row.repo);
|
|
@@ -27031,67 +27230,112 @@ var SqliteFindingsRepository = class {
|
|
|
27031
27230
|
}
|
|
27032
27231
|
addToLocation(acc, row);
|
|
27033
27232
|
}
|
|
27034
|
-
|
|
27035
|
-
const
|
|
27036
|
-
|
|
27037
|
-
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
|
|
27045
|
-
|
|
27046
|
-
|
|
27047
|
-
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
27053
|
-
|
|
27054
|
-
|
|
27055
|
-
|
|
27056
|
-
);
|
|
27057
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27058
|
-
const folded = foldGroupStatus(statuses);
|
|
27059
|
-
return {
|
|
27060
|
-
repo,
|
|
27061
|
-
instanceCount: rollup.instanceCount,
|
|
27062
|
-
maxSeverity: rollup.maxSeverity,
|
|
27063
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27064
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27065
|
-
files: fileRows
|
|
27066
|
-
};
|
|
27067
|
-
});
|
|
27068
|
-
repos.sort(compareLocationOrder);
|
|
27233
|
+
const sorted = [];
|
|
27234
|
+
for (const [repo, files] of byRepo) {
|
|
27235
|
+
for (const [file2, acc] of files) {
|
|
27236
|
+
const status = foldGroupStatus(acc.statuses);
|
|
27237
|
+
sorted.push({
|
|
27238
|
+
id: encodeLocationId(repo, file2),
|
|
27239
|
+
repo,
|
|
27240
|
+
file: file2,
|
|
27241
|
+
instanceCount: acc.instanceCount,
|
|
27242
|
+
maxSeverity: acc.maxSeverity,
|
|
27243
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
27244
|
+
...status === void 0 ? {} : { status },
|
|
27245
|
+
ruleIds: [...acc.ruleIds]
|
|
27246
|
+
});
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
27249
|
+
sorted.sort(compareLocationOrder);
|
|
27250
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
27251
|
+
const page = sorted.slice(start, start + limit);
|
|
27252
|
+
const lastOnPage = page.at(-1);
|
|
27253
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
27254
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27069
27255
|
return Promise.resolve({
|
|
27070
|
-
totals: { findings: total,
|
|
27071
|
-
|
|
27072
|
-
|
|
27256
|
+
totals: { findings: total, locations: sorted.length },
|
|
27257
|
+
facets: accumulator.facets(),
|
|
27258
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
27259
|
+
nextCursor
|
|
27073
27260
|
});
|
|
27074
27261
|
}
|
|
27075
27262
|
/**
|
|
27076
|
-
* Every finding in scope as a FlatFindingRow, newest first,
|
|
27263
|
+
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27077
27264
|
*
|
|
27078
27265
|
* A generator so a caller streams the scope without it ever being an array:
|
|
27079
27266
|
* the flat list counts and facets the whole filtered scope, which on a large
|
|
27080
|
-
* store is far more rows than any page.
|
|
27081
|
-
*
|
|
27082
|
-
*
|
|
27267
|
+
* store is far more rows than any page. The rows come off ONE statement,
|
|
27268
|
+
* iterated rather than materialized, in the index order `findingScanSql`
|
|
27269
|
+
* arranges — so the scan is a single pass with a block sort of the id
|
|
27270
|
+
* tie-break only, never a sort of the scope, where a sequence of
|
|
27271
|
+
* keyset-bounded batches re-sorted everything below the cursor on every
|
|
27272
|
+
* batch and cost the square of the scope.
|
|
27083
27273
|
*
|
|
27084
|
-
*
|
|
27085
|
-
*
|
|
27086
|
-
*
|
|
27087
|
-
*
|
|
27088
|
-
*
|
|
27089
|
-
*
|
|
27090
|
-
*
|
|
27091
|
-
* dimension — see listFindingInstances.
|
|
27274
|
+
* `sessionId` and `from` carry ONLY what no facet counts — a filter
|
|
27275
|
+
* dimension narrowed here would be missing from its own facet, which is
|
|
27276
|
+
* computed by excluding that dimension (see listFindingInstances). There is
|
|
27277
|
+
* no `after`/cursor parameter: a keyset page is collected inline from this
|
|
27278
|
+
* same pass (`listFindingInstances`' `isPastCursor`) rather than by a second,
|
|
27279
|
+
* narrower statement, since the counting pass already visits every row a
|
|
27280
|
+
* page-2+ request would otherwise re-seek for.
|
|
27092
27281
|
*/
|
|
27093
27282
|
*scanFindingRows(scope) {
|
|
27094
|
-
const
|
|
27283
|
+
const { sql, params } = this.findingScanSql(scope);
|
|
27284
|
+
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27285
|
+
yield toFlatFindingRow(r);
|
|
27286
|
+
}
|
|
27287
|
+
}
|
|
27288
|
+
/**
|
|
27289
|
+
* One finding by its own id, or null when no such row exists.
|
|
27290
|
+
*
|
|
27291
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
27292
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
27293
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
27294
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
27295
|
+
* older than anything a first page holds.
|
|
27296
|
+
*
|
|
27297
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
27298
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
27299
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
27300
|
+
* worse than showing it.
|
|
27301
|
+
*
|
|
27302
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
27303
|
+
* type should the list select?" and "what does the drawer show?".
|
|
27304
|
+
*/
|
|
27305
|
+
findingInstance(id) {
|
|
27306
|
+
const row = this.db.prepare(
|
|
27307
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27308
|
+
FROM inspection_findings f
|
|
27309
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27310
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27311
|
+
WHERE f.id = ?`
|
|
27312
|
+
).get(id);
|
|
27313
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
27314
|
+
}
|
|
27315
|
+
/**
|
|
27316
|
+
* The one statement both instance-level scans run: every finding in scope,
|
|
27317
|
+
* joined to its event and definition, newest first.
|
|
27318
|
+
*
|
|
27319
|
+
* THE PLAN IS THE POINT, and two things in the SQL exist only to pin it —
|
|
27320
|
+
* the same two `recentFindings` documents at length, for the same reason:
|
|
27321
|
+
*
|
|
27322
|
+
* - **`+e.event_type`** makes the capture-kind predicate non-indexable, so
|
|
27323
|
+
* the planner cannot pick `idx_audit_type_t` and then sort. That index
|
|
27324
|
+
* yields `started_at` order per event type, not across the four, so
|
|
27325
|
+
* satisfying the ORDER BY from it would need a merge SQLite does not do.
|
|
27326
|
+
* Freed of it, the planner walks `idx_audit_started_at` backwards — or
|
|
27327
|
+
* `idx_audit_session` for a session scope, which is also `started_at`
|
|
27328
|
+
* ordered within the session — and the order falls out of the index.
|
|
27329
|
+
* - **`CROSS JOIN`** pins `audit_events` as the driving table. With plain
|
|
27330
|
+
* JOINs the planner drives from the findings and sorts everything.
|
|
27331
|
+
*
|
|
27332
|
+
* The latest-resolution lookup is the CORRELATED form: only `status` is
|
|
27333
|
+
* needed, `idx_finding_resolution_key_created` answers it with one backward
|
|
27334
|
+
* index probe per keyed row, and a derived table over the whole resolution
|
|
27335
|
+
* table would be materialized before the first row streamed.
|
|
27336
|
+
*/
|
|
27337
|
+
findingScanSql(scope) {
|
|
27338
|
+
const conditions = [`+e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`];
|
|
27095
27339
|
const params = [];
|
|
27096
27340
|
if (scope.sessionId !== void 0 && scope.sessionId !== "") {
|
|
27097
27341
|
conditions.push("e.root_session_id = ?");
|
|
@@ -27101,64 +27345,40 @@ var SqliteFindingsRepository = class {
|
|
|
27101
27345
|
conditions.push("e.started_at >= ?");
|
|
27102
27346
|
params.push(isoToEpochMillis(scope.from));
|
|
27103
27347
|
}
|
|
27104
|
-
const sql = `SELECT
|
|
27105
|
-
|
|
27106
|
-
|
|
27107
|
-
|
|
27108
|
-
json_extract(e.attributes, '$.source_tool') AS source_tool,
|
|
27109
|
-
json_extract(e.attributes, '$.repo') AS repo,
|
|
27110
|
-
json_extract(e.attributes, '$.file_path') AS file,
|
|
27111
|
-
json_extract(e.attributes, '$.tool_name') AS tool_name,
|
|
27112
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27113
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27114
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
27115
|
-
FROM inspection_findings f
|
|
27116
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
27117
|
-
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27348
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27349
|
+
FROM audit_events e
|
|
27350
|
+
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27351
|
+
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27118
27352
|
WHERE ${conditions.join(" AND ")}
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
LIMIT ?`;
|
|
27122
|
-
let after = scope.after ?? { startedAtMs: Number.MAX_SAFE_INTEGER, id: "\uFFFF" };
|
|
27123
|
-
for (; ; ) {
|
|
27124
|
-
const rows = allRows(this.db.prepare(sql), [
|
|
27125
|
-
...params,
|
|
27126
|
-
after.startedAtMs,
|
|
27127
|
-
after.startedAtMs,
|
|
27128
|
-
after.id,
|
|
27129
|
-
SCAN_BATCH_ROWS
|
|
27130
|
-
]);
|
|
27131
|
-
for (const r of rows) {
|
|
27132
|
-
yield {
|
|
27133
|
-
id: r.id,
|
|
27134
|
-
ruleId: r.rule_id,
|
|
27135
|
-
category: r.category,
|
|
27136
|
-
severity: r.severity,
|
|
27137
|
-
maskedMatch: r.masked_match,
|
|
27138
|
-
actionTaken: r.action_taken,
|
|
27139
|
-
confidence: r.confidence,
|
|
27140
|
-
occurredAt: epochMillisToIso(r.occurred_at),
|
|
27141
|
-
sourceTool: r.source_tool,
|
|
27142
|
-
repo: r.repo ?? "",
|
|
27143
|
-
file: r.file ?? "",
|
|
27144
|
-
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
27145
|
-
eventId: r.event_id,
|
|
27146
|
-
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
27147
|
-
status: deriveInstanceStatus(r)
|
|
27148
|
-
};
|
|
27149
|
-
}
|
|
27150
|
-
if (rows.length < SCAN_BATCH_ROWS) return;
|
|
27151
|
-
const lastRow = rows[rows.length - 1];
|
|
27152
|
-
if (lastRow === void 0) return;
|
|
27153
|
-
after = { startedAtMs: lastRow.occurred_at, id: lastRow.id };
|
|
27154
|
-
}
|
|
27353
|
+
ORDER BY e.started_at DESC, f.id DESC`;
|
|
27354
|
+
return { sql, params };
|
|
27155
27355
|
}
|
|
27156
27356
|
groupAggregates(withSearchText, scope) {
|
|
27157
|
-
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT
|
|
27158
|
-
group_concat(DISTINCT
|
|
27159
|
-
group_concat(DISTINCT 'via ' ||
|
|
27357
|
+
const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT e.repo) AS repos,
|
|
27358
|
+
group_concat(DISTINCT e.file_path) AS files,
|
|
27359
|
+
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27160
27360
|
const rows = this.db.prepare(
|
|
27161
27361
|
`SELECT rule_id,
|
|
27362
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
27363
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
27364
|
+
-- in an aggregate query, every bare column takes its value from
|
|
27365
|
+
-- the row that produced the extremum. So these are the severity
|
|
27366
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
27367
|
+
-- is what the row-based build they replaced read off its first
|
|
27368
|
+
-- (newest-first) row.
|
|
27369
|
+
--
|
|
27370
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
27371
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
27372
|
+
-- mints a new row), so a rule whose severity moved between
|
|
27373
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
27374
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
27375
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
27376
|
+
-- filter, the facet counts and the primary sort key.
|
|
27377
|
+
--
|
|
27378
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
27379
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
27380
|
+
severity,
|
|
27381
|
+
category,
|
|
27162
27382
|
sum(tuple_count) AS instance_count,
|
|
27163
27383
|
max(latest_at) AS latest_at,
|
|
27164
27384
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27169,12 +27389,20 @@ var SqliteFindingsRepository = class {
|
|
|
27169
27389
|
group_concat(tool_names) AS tool_names
|
|
27170
27390
|
FROM (
|
|
27171
27391
|
SELECT d.rule_id AS rule_id,
|
|
27392
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
27393
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
27394
|
+
-- these are grouped on below and resolved to the newest
|
|
27395
|
+
-- firing version by the outer query's bare-column select.
|
|
27396
|
+
-- They ride the aggregate because the type build has no rows
|
|
27397
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
27398
|
+
d.severity AS severity,
|
|
27399
|
+
d.category AS category,
|
|
27172
27400
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27173
27401
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27174
27402
|
coalesce(latest.status, '') AS status_tuple,
|
|
27175
27403
|
count(*) AS tuple_count,
|
|
27176
27404
|
max(e.started_at) AS latest_at,
|
|
27177
|
-
group_concat(DISTINCT
|
|
27405
|
+
group_concat(DISTINCT e.source_tool) AS source_tools,
|
|
27178
27406
|
group_concat(DISTINCT f.action_taken) AS actions_taken
|
|
27179
27407
|
${innerSearchColumns}
|
|
27180
27408
|
FROM inspection_findings f
|
|
@@ -27183,7 +27411,7 @@ var SqliteFindingsRepository = class {
|
|
|
27183
27411
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27184
27412
|
ON latest.finding_key = f.finding_key
|
|
27185
27413
|
${scope.predicate}
|
|
27186
|
-
GROUP BY d.rule_id, status_tuple
|
|
27414
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27187
27415
|
)
|
|
27188
27416
|
GROUP BY rule_id`
|
|
27189
27417
|
).all(scope.params);
|
|
@@ -27192,6 +27420,8 @@ var SqliteFindingsRepository = class {
|
|
|
27192
27420
|
r.rule_id,
|
|
27193
27421
|
{
|
|
27194
27422
|
instanceCount: r.instance_count,
|
|
27423
|
+
severity: r.severity,
|
|
27424
|
+
category: r.category,
|
|
27195
27425
|
sourceTools: splitConcat(r.source_tools),
|
|
27196
27426
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27197
27427
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27208,7 +27438,7 @@ var SqliteFindingsRepository = class {
|
|
|
27208
27438
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27209
27439
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27210
27440
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27211
|
-
// Left undefined (not '') when unfetched, so
|
|
27441
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27212
27442
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27213
27443
|
// and skip priming a haystack nothing will read.
|
|
27214
27444
|
...withSearchText ? {
|
|
@@ -27305,6 +27535,8 @@ function isoDay(ms) {
|
|
|
27305
27535
|
// ../../packages/persistence/src/repositories/history-sync.ts
|
|
27306
27536
|
var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
|
|
27307
27537
|
var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27538
|
+
var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
|
|
27539
|
+
var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27308
27540
|
var SKIPPED = -1;
|
|
27309
27541
|
var ROW_COLUMNS = `id,
|
|
27310
27542
|
parent_id AS parentId,
|
|
@@ -27344,6 +27576,26 @@ var SqliteHistorySyncRepository = class {
|
|
|
27344
27576
|
ORDER BY (event_type = 'session') DESC, started_at
|
|
27345
27577
|
LIMIT :limit`
|
|
27346
27578
|
);
|
|
27579
|
+
this.captureRowsStmt = db.prepare(
|
|
27580
|
+
`SELECT ${ROW_COLUMNS}
|
|
27581
|
+
FROM audit_events
|
|
27582
|
+
WHERE synced_at IS NULL
|
|
27583
|
+
AND sync_claimed_at IS NULL
|
|
27584
|
+
AND outbox_owed = 1
|
|
27585
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27586
|
+
AND started_at < :before
|
|
27587
|
+
ORDER BY started_at
|
|
27588
|
+
LIMIT :limit`
|
|
27589
|
+
);
|
|
27590
|
+
this.markOwedStmt = db.prepare(
|
|
27591
|
+
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27592
|
+
);
|
|
27593
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
27594
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
27595
|
+
WHERE synced_at IS NULL
|
|
27596
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27597
|
+
AND started_at < :before`
|
|
27598
|
+
);
|
|
27347
27599
|
this.stampStmt = db.prepare(
|
|
27348
27600
|
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27349
27601
|
);
|
|
@@ -27375,6 +27627,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27375
27627
|
FROM audit_events
|
|
27376
27628
|
WHERE event_type IN (${TYPE_LIST})`
|
|
27377
27629
|
);
|
|
27630
|
+
this.captureSkipCountStmt = db.prepare(
|
|
27631
|
+
`SELECT COUNT(*) AS skipped
|
|
27632
|
+
FROM audit_events
|
|
27633
|
+
WHERE synced_at = ${String(SKIPPED)}
|
|
27634
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})`
|
|
27635
|
+
);
|
|
27378
27636
|
this.fingerprintStmt = db.prepare(
|
|
27379
27637
|
`SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
|
|
27380
27638
|
FROM history_sync WHERE id = 1`
|
|
@@ -27384,6 +27642,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27384
27642
|
SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
|
|
27385
27643
|
WHERE id = 1`
|
|
27386
27644
|
);
|
|
27645
|
+
this.disownCapturesStmt = db.prepare(
|
|
27646
|
+
`UPDATE audit_events SET outbox_owed = NULL
|
|
27647
|
+
WHERE outbox_owed IS NOT NULL
|
|
27648
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27649
|
+
AND started_at < :attachedAt`
|
|
27650
|
+
);
|
|
27387
27651
|
this.rearmStmt = db.prepare(
|
|
27388
27652
|
`UPDATE audit_events SET synced_at = NULL
|
|
27389
27653
|
WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
|
|
@@ -27456,6 +27720,11 @@ var SqliteHistorySyncRepository = class {
|
|
|
27456
27720
|
closeWindowStmt;
|
|
27457
27721
|
releaseBoundaryStmt;
|
|
27458
27722
|
freezeBoundaryStmt;
|
|
27723
|
+
captureRowsStmt;
|
|
27724
|
+
markOwedStmt;
|
|
27725
|
+
markCaptureBacklogOwedStmt;
|
|
27726
|
+
captureSkipCountStmt;
|
|
27727
|
+
disownCapturesStmt;
|
|
27459
27728
|
partitionStmt;
|
|
27460
27729
|
claimRowStmt;
|
|
27461
27730
|
releaseRowStmt;
|
|
@@ -27489,6 +27758,51 @@ var SqliteHistorySyncRepository = class {
|
|
|
27489
27758
|
pendingRows(sessionId, limit, before) {
|
|
27490
27759
|
return allRows(this.rowsStmt, { sessionId, limit, before });
|
|
27491
27760
|
}
|
|
27761
|
+
/**
|
|
27762
|
+
* Captures this machine still owes the deployment, oldest first.
|
|
27763
|
+
*
|
|
27764
|
+
* Selected by the `outbox_owed` marker the attached forward path writes, not
|
|
27765
|
+
* by a time window — see captureRowsStmt for why a window could not express
|
|
27766
|
+
* this. `before` is the grace window that leaves a just-recorded capture to
|
|
27767
|
+
* the live path.
|
|
27768
|
+
*/
|
|
27769
|
+
pendingCaptureRows(limit, before) {
|
|
27770
|
+
return allRows(this.captureRowsStmt, { limit, before });
|
|
27771
|
+
}
|
|
27772
|
+
/**
|
|
27773
|
+
* Record that a capture is OWED to the deployment.
|
|
27774
|
+
*
|
|
27775
|
+
* Written by the attached forward path when a live send did not confirm
|
|
27776
|
+
* delivery, and read by the drain as the whole of its eligibility test. It is
|
|
27777
|
+
* a fact rather than an inference: the machine was attached, the send did not
|
|
27778
|
+
* land, so the row is owed — which no time window can state, because the same
|
|
27779
|
+
* window that holds the rows a past attachment left owed also holds every
|
|
27780
|
+
* capture recorded while the machine was DETACHED, and those were never
|
|
27781
|
+
* offered to anyone.
|
|
27782
|
+
*
|
|
27783
|
+
* Idempotent, and never un-set: `markSynced` settling the row is what takes it
|
|
27784
|
+
* out of the drain's read.
|
|
27785
|
+
*/
|
|
27786
|
+
markCaptureOwed(id) {
|
|
27787
|
+
this.markOwedStmt.run({ id });
|
|
27788
|
+
}
|
|
27789
|
+
/**
|
|
27790
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
27791
|
+
*
|
|
27792
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
27793
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
27794
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
27795
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
27796
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
27797
|
+
* later re-attach or key rotation might widen it to.
|
|
27798
|
+
*
|
|
27799
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
27800
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
27801
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
27802
|
+
*/
|
|
27803
|
+
markCaptureBacklogOwed(before) {
|
|
27804
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
27805
|
+
}
|
|
27492
27806
|
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27493
27807
|
markSynced(ids, atMs) {
|
|
27494
27808
|
this.stampAll(ids, atMs);
|
|
@@ -27572,10 +27886,12 @@ var SqliteHistorySyncRepository = class {
|
|
|
27572
27886
|
this.countsStmt,
|
|
27573
27887
|
{ before }
|
|
27574
27888
|
);
|
|
27889
|
+
const captures = getRow(this.captureSkipCountStmt);
|
|
27575
27890
|
return {
|
|
27576
27891
|
pending: row?.pending ?? 0,
|
|
27577
27892
|
sent: row?.sent ?? 0,
|
|
27578
|
-
skipped: row?.skipped ?? 0
|
|
27893
|
+
skipped: row?.skipped ?? 0,
|
|
27894
|
+
capturesSkipped: captures?.skipped ?? 0
|
|
27579
27895
|
};
|
|
27580
27896
|
}
|
|
27581
27897
|
/**
|
|
@@ -27603,20 +27919,54 @@ var SqliteHistorySyncRepository = class {
|
|
|
27603
27919
|
*
|
|
27604
27920
|
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
27605
27921
|
* machine has just left are undelivered as far as the new one is concerned.
|
|
27606
|
-
* All
|
|
27607
|
-
* attributed to the wrong deployment,
|
|
27922
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
27923
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
27924
|
+
* a disown with no re-mark to follow it.
|
|
27608
27925
|
*
|
|
27609
27926
|
* The boundary is written HERE and only here, which is what freezes it: a
|
|
27610
27927
|
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
27611
27928
|
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
27612
27929
|
* the live path has since delivered.
|
|
27930
|
+
*
|
|
27931
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
27932
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
27933
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
27934
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
27935
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
27936
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
27937
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
27938
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
27939
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
27940
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
27941
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
27942
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
27943
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
27944
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
27945
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
27946
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
27947
|
+
*
|
|
27948
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
27949
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
27950
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
27951
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
27952
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
27953
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
27954
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
27955
|
+
* transaction above.
|
|
27613
27956
|
*/
|
|
27614
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
27957
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
27615
27958
|
this.ensureRowStmt.run();
|
|
27616
27959
|
withTransaction(
|
|
27617
27960
|
this.db,
|
|
27618
27961
|
() => {
|
|
27962
|
+
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
27619
27963
|
this.rearmStmt.run();
|
|
27964
|
+
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
27965
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
27966
|
+
}
|
|
27967
|
+
if (backfillCapturesBefore !== void 0) {
|
|
27968
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
27969
|
+
}
|
|
27620
27970
|
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
27621
27971
|
},
|
|
27622
27972
|
"IMMEDIATE"
|
|
@@ -27813,7 +28163,253 @@ var SqliteInspectionFindingsRepository = class {
|
|
|
27813
28163
|
};
|
|
27814
28164
|
|
|
27815
28165
|
// ../../packages/persistence/src/repositories/installed-packs.ts
|
|
27816
|
-
import { createHash as createHash2, randomUUID as
|
|
28166
|
+
import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
|
|
28167
|
+
|
|
28168
|
+
// ../../packages/persistence/src/policy-floor.ts
|
|
28169
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
28170
|
+
import { join as join6 } from "path";
|
|
28171
|
+
|
|
28172
|
+
// ../../packages/persistence/src/local-layout.ts
|
|
28173
|
+
import { renameSync as renameSync3 } from "fs";
|
|
28174
|
+
import { mkdir } from "fs/promises";
|
|
28175
|
+
import { homedir } from "os";
|
|
28176
|
+
import { join as join4 } from "path";
|
|
28177
|
+
function defaultDataDir() {
|
|
28178
|
+
return join4(homedir(), ".aka");
|
|
28179
|
+
}
|
|
28180
|
+
function settingsDir(base = defaultDataDir()) {
|
|
28181
|
+
return join4(base, "settings");
|
|
28182
|
+
}
|
|
28183
|
+
function dataDir(base = defaultDataDir()) {
|
|
28184
|
+
return join4(base, "data");
|
|
28185
|
+
}
|
|
28186
|
+
function dbPath(base = defaultDataDir()) {
|
|
28187
|
+
return join4(dataDir(base), "aka.db");
|
|
28188
|
+
}
|
|
28189
|
+
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
28190
|
+
ensureDataDirSync(dir);
|
|
28191
|
+
}
|
|
28192
|
+
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
28193
|
+
const moves = [
|
|
28194
|
+
{ name: "config.json", dest: settingsDir(base) },
|
|
28195
|
+
{ name: "policy-cache.json", dest: dataDir(base) }
|
|
28196
|
+
];
|
|
28197
|
+
for (const { name, dest } of moves) {
|
|
28198
|
+
try {
|
|
28199
|
+
ensureDataDirSync(dest);
|
|
28200
|
+
const moved = join4(dest, name);
|
|
28201
|
+
renameSync3(join4(base, name), moved);
|
|
28202
|
+
tightenFile(moved);
|
|
28203
|
+
} catch {
|
|
28204
|
+
}
|
|
28205
|
+
}
|
|
28206
|
+
}
|
|
28207
|
+
|
|
28208
|
+
// ../../packages/persistence/src/settings.ts
|
|
28209
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
28210
|
+
import { join as join5 } from "path";
|
|
28211
|
+
|
|
28212
|
+
// ../../packages/persistence/src/file-lock.ts
|
|
28213
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
28214
|
+
import {
|
|
28215
|
+
closeSync,
|
|
28216
|
+
existsSync as existsSync2,
|
|
28217
|
+
openSync,
|
|
28218
|
+
readFileSync as readFileSync2,
|
|
28219
|
+
rmSync as rmSync5,
|
|
28220
|
+
statSync as statSync3,
|
|
28221
|
+
writeFileSync as writeFileSync2
|
|
28222
|
+
} from "fs";
|
|
28223
|
+
import { hostname as hostname3 } from "os";
|
|
28224
|
+
var PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
28225
|
+
|
|
28226
|
+
// ../../packages/persistence/src/managed-settings.ts
|
|
28227
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
28228
|
+
import { posix, win32 } from "path";
|
|
28229
|
+
function managedSettingsPaths(platform2 = process.platform) {
|
|
28230
|
+
if (platform2 === "darwin") {
|
|
28231
|
+
return [
|
|
28232
|
+
posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
|
|
28233
|
+
posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
|
|
28234
|
+
];
|
|
28235
|
+
}
|
|
28236
|
+
if (platform2 === "win32") {
|
|
28237
|
+
return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
|
|
28238
|
+
}
|
|
28239
|
+
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28240
|
+
}
|
|
28241
|
+
var testOnlyManagedPaths = null;
|
|
28242
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28243
|
+
for (const path of paths) {
|
|
28244
|
+
let text;
|
|
28245
|
+
try {
|
|
28246
|
+
text = readFileSync3(path, "utf8");
|
|
28247
|
+
} catch {
|
|
28248
|
+
continue;
|
|
28249
|
+
}
|
|
28250
|
+
const record2 = parseJsonObject(text);
|
|
28251
|
+
if (!record2) continue;
|
|
28252
|
+
const parsed = ManagedSettings.safeParse(record2);
|
|
28253
|
+
if (parsed.success) return parsed.data;
|
|
28254
|
+
}
|
|
28255
|
+
return null;
|
|
28256
|
+
}
|
|
28257
|
+
function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
|
|
28258
|
+
if (!managed) return settings;
|
|
28259
|
+
const { values } = managed;
|
|
28260
|
+
const merged = { ...settings };
|
|
28261
|
+
if (values.runMode !== void 0) merged.runMode = values.runMode;
|
|
28262
|
+
if (values.controlPlane !== void 0) {
|
|
28263
|
+
merged.controlPlane = {
|
|
28264
|
+
...values.controlPlane,
|
|
28265
|
+
// The administrator pinned WHICH deployment, not WHEN this machine
|
|
28266
|
+
// joined it. Keep the user's own attach time when the endpoint is
|
|
28267
|
+
// unchanged, so a managed machine does not appear to re-attach on every
|
|
28268
|
+
// read; stamp a fresh one when the administrator moved it.
|
|
28269
|
+
attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
|
|
28270
|
+
};
|
|
28271
|
+
}
|
|
28272
|
+
if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
|
|
28273
|
+
if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
|
|
28274
|
+
if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
|
|
28275
|
+
if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
|
|
28276
|
+
if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
|
|
28277
|
+
if (values.vaultConsent !== void 0) {
|
|
28278
|
+
merged.vaultConsent = values.vaultConsent ? (
|
|
28279
|
+
// Keep an existing valid grant so its acknowledgedAt survives; mint one
|
|
28280
|
+
// at the current version otherwise.
|
|
28281
|
+
settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
|
|
28282
|
+
) : void 0;
|
|
28283
|
+
}
|
|
28284
|
+
if (values.modelJudgeConsent !== void 0) {
|
|
28285
|
+
merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
|
|
28286
|
+
acknowledgedAt: now().toISOString(),
|
|
28287
|
+
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
28288
|
+
} : void 0;
|
|
28289
|
+
}
|
|
28290
|
+
return merged;
|
|
28291
|
+
}
|
|
28292
|
+
|
|
28293
|
+
// ../../packages/persistence/src/settings.ts
|
|
28294
|
+
var SETTINGS_FILENAME = "settings.json";
|
|
28295
|
+
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
28296
|
+
return overlayManagedSettings(readUserSettings(base), readManagedSettings());
|
|
28297
|
+
}
|
|
28298
|
+
function readUserSettings(base) {
|
|
28299
|
+
const record2 = readJson(join5(settingsDir(base), SETTINGS_FILENAME));
|
|
28300
|
+
if (!record2) return defaultWorkspaceSettings();
|
|
28301
|
+
try {
|
|
28302
|
+
return WorkspaceSettings.parse(record2);
|
|
28303
|
+
} catch {
|
|
28304
|
+
return defaultWorkspaceSettings();
|
|
28305
|
+
}
|
|
28306
|
+
}
|
|
28307
|
+
function readJson(file2) {
|
|
28308
|
+
let text;
|
|
28309
|
+
try {
|
|
28310
|
+
text = readFileSync4(file2, "utf8");
|
|
28311
|
+
} catch {
|
|
28312
|
+
return null;
|
|
28313
|
+
}
|
|
28314
|
+
return parseJsonObject(text) ?? null;
|
|
28315
|
+
}
|
|
28316
|
+
|
|
28317
|
+
// ../../packages/persistence/src/policy-floor.ts
|
|
28318
|
+
function refusalMessage(pack, attempted, floor, refusal) {
|
|
28319
|
+
switch (refusal) {
|
|
28320
|
+
case "lock":
|
|
28321
|
+
return `refusing to re-assign '${pack}': its policy is set by the connected control plane`;
|
|
28322
|
+
case "disable":
|
|
28323
|
+
return `refusing to disable '${pack}': it is governed by the connected control plane`;
|
|
28324
|
+
case "floor":
|
|
28325
|
+
return `refusing to set '${pack}' to '${attempted ?? "unassigned"}': the connected control plane requires at least '${floor}'`;
|
|
28326
|
+
}
|
|
28327
|
+
}
|
|
28328
|
+
var PolicyFloorError = class extends Error {
|
|
28329
|
+
/** `namespace/packId` of the detection whose write was refused. */
|
|
28330
|
+
pack;
|
|
28331
|
+
/**
|
|
28332
|
+
* The archetype the caller asked for, or null when the write named none —
|
|
28333
|
+
* clearing the assignment, or switching the detection off.
|
|
28334
|
+
*/
|
|
28335
|
+
attempted;
|
|
28336
|
+
/** The weakest archetype the control plane permits for this pack. */
|
|
28337
|
+
floor;
|
|
28338
|
+
refusal;
|
|
28339
|
+
constructor(pack, attempted, floor, refusal) {
|
|
28340
|
+
super(refusalMessage(pack, attempted, floor, refusal));
|
|
28341
|
+
this.name = "PolicyFloorError";
|
|
28342
|
+
this.pack = pack;
|
|
28343
|
+
this.attempted = attempted;
|
|
28344
|
+
this.floor = floor;
|
|
28345
|
+
this.refusal = refusal;
|
|
28346
|
+
}
|
|
28347
|
+
};
|
|
28348
|
+
function readCachedPolicyBundle(base = defaultDataDir()) {
|
|
28349
|
+
try {
|
|
28350
|
+
const raw = readFileSync5(join6(dataDir(base), POLICY_CACHE_FILENAME), "utf8");
|
|
28351
|
+
const parsed = JSON.parse(raw);
|
|
28352
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
28353
|
+
return PolicyBundle.parse(parsed.bundle);
|
|
28354
|
+
} catch {
|
|
28355
|
+
return null;
|
|
28356
|
+
}
|
|
28357
|
+
}
|
|
28358
|
+
function indexEnabled(policies) {
|
|
28359
|
+
const byRuleId = /* @__PURE__ */ new Map();
|
|
28360
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
28361
|
+
for (const policy of policies) {
|
|
28362
|
+
if (!policy.enabled) continue;
|
|
28363
|
+
if ("ruleId" in policy.target) {
|
|
28364
|
+
if (!byRuleId.has(policy.target.ruleId)) byRuleId.set(policy.target.ruleId, policy.action);
|
|
28365
|
+
} else if (!byCategory.has(policy.target.category)) {
|
|
28366
|
+
byCategory.set(policy.target.category, policy.action);
|
|
28367
|
+
}
|
|
28368
|
+
}
|
|
28369
|
+
return { byRuleId, byCategory };
|
|
28370
|
+
}
|
|
28371
|
+
function hasAuthoredPolicy(policies, rules, byRuleId) {
|
|
28372
|
+
const ruleIds = new Set(rules.map((rule) => rule.id));
|
|
28373
|
+
const categories = new Set(rules.map((rule) => rule.category));
|
|
28374
|
+
const bundleNamesPack = rules.some((rule) => byRuleId.has(rule.id));
|
|
28375
|
+
return policies.some((policy) => {
|
|
28376
|
+
if (!policy.enabled || policy.provenance !== "authored") return false;
|
|
28377
|
+
return "ruleId" in policy.target ? ruleIds.has(policy.target.ruleId) : bundleNamesPack && categories.has(policy.target.category);
|
|
28378
|
+
});
|
|
28379
|
+
}
|
|
28380
|
+
function controlPlanePolicyFloor(rules, base = defaultDataDir()) {
|
|
28381
|
+
const floors = openControlPlaneFloors(base);
|
|
28382
|
+
return floors === null ? null : floors.floorFor(rules);
|
|
28383
|
+
}
|
|
28384
|
+
function openControlPlaneFloors(base = defaultDataDir()) {
|
|
28385
|
+
if (!isAttached(readWorkspaceSettings(base))) return null;
|
|
28386
|
+
const bundle = readCachedPolicyBundle(base);
|
|
28387
|
+
if (bundle === null) return null;
|
|
28388
|
+
const indexes = indexEnabled(bundle.policies);
|
|
28389
|
+
return { floorFor: (rules) => resolveFloor(rules, bundle.policies, indexes) };
|
|
28390
|
+
}
|
|
28391
|
+
function resolveFloor(rules, policies, { byRuleId, byCategory }) {
|
|
28392
|
+
let action = null;
|
|
28393
|
+
for (const rule of rules) {
|
|
28394
|
+
const resolved = byRuleId.get(rule.id) ?? byCategory.get(rule.category);
|
|
28395
|
+
if (resolved === void 0) continue;
|
|
28396
|
+
action = action === null ? resolved : strongerAction(action, resolved);
|
|
28397
|
+
}
|
|
28398
|
+
if (action === null) return null;
|
|
28399
|
+
return {
|
|
28400
|
+
floor: weakestBuiltinAtLeast(action),
|
|
28401
|
+
locked: hasAuthoredPolicy(policies, rules, byRuleId)
|
|
28402
|
+
};
|
|
28403
|
+
}
|
|
28404
|
+
function policyAssignmentRefusal(policyId, floor) {
|
|
28405
|
+
if (floor.locked) return "lock";
|
|
28406
|
+
const effective = policyId ?? DEFAULT_PACK_POLICY_ID;
|
|
28407
|
+
return isActionAtLeast(builtinPolicyToAction(effective), builtinPolicyToAction(floor.floor)) ? null : "floor";
|
|
28408
|
+
}
|
|
28409
|
+
function packEnablementRefusal(enabled, floor) {
|
|
28410
|
+
if (floor === null || enabled) return null;
|
|
28411
|
+
return "disable";
|
|
28412
|
+
}
|
|
27817
28413
|
|
|
27818
28414
|
// ../../packages/persistence/src/semver.ts
|
|
27819
28415
|
function parse3(version2) {
|
|
@@ -27907,8 +28503,19 @@ function ruleIdsOf(rulesJson) {
|
|
|
27907
28503
|
return ids;
|
|
27908
28504
|
}
|
|
27909
28505
|
var SqliteInstalledPacksRepository = class {
|
|
27910
|
-
|
|
28506
|
+
/**
|
|
28507
|
+
* `baseDir` is the `~/.aka` LAYOUT BASE, not the data dir — the control-plane
|
|
28508
|
+
* floor needs both halves of it (settings/ says whether this machine is
|
|
28509
|
+
* attached, data/ holds the cached bundle). It is optional because a caller
|
|
28510
|
+
* holding only a DatabaseSync — every test construction site, and any embedder
|
|
28511
|
+
* that opens the store itself — has no layout to point at, and such a caller
|
|
28512
|
+
* gets the pre-existing behaviour: no floor, no lock. Production threads it in
|
|
28513
|
+
* from `openLocalDatabase`, which is the single construction site that owns a
|
|
28514
|
+
* real `~/.aka`.
|
|
28515
|
+
*/
|
|
28516
|
+
constructor(db, baseDir) {
|
|
27911
28517
|
this.db = db;
|
|
28518
|
+
this.baseDir = baseDir;
|
|
27912
28519
|
this.insertMissingStmt = db.prepare(
|
|
27913
28520
|
`INSERT INTO installed_packs (id, namespace, pack_id, version, name, rules_json, enabled, created_at, updated_at)
|
|
27914
28521
|
VALUES (:id, :namespace, :packId, :version, :name, :rulesJson, 1, :now, :now)
|
|
@@ -27930,11 +28537,17 @@ var SqliteInstalledPacksRepository = class {
|
|
|
27930
28537
|
this.signatureStmt = db.prepare(
|
|
27931
28538
|
`SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
|
|
27932
28539
|
);
|
|
28540
|
+
this.packRulesStmt = db.prepare(
|
|
28541
|
+
`SELECT rules_json AS rulesJson FROM installed_packs
|
|
28542
|
+
WHERE namespace = ? AND pack_id = ?`
|
|
28543
|
+
);
|
|
27933
28544
|
}
|
|
27934
28545
|
db;
|
|
28546
|
+
baseDir;
|
|
27935
28547
|
insertMissingStmt;
|
|
27936
28548
|
upsertAvailableStmt;
|
|
27937
28549
|
signatureStmt;
|
|
28550
|
+
packRulesStmt;
|
|
27938
28551
|
/**
|
|
27939
28552
|
* Record the running binary's detection inventory. Refreshes the
|
|
27940
28553
|
* available_packs mirror (pruning packs the binary no longer ships) and
|
|
@@ -27976,7 +28589,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
27976
28589
|
let behind = false;
|
|
27977
28590
|
for (const row of rows) {
|
|
27978
28591
|
const params = {
|
|
27979
|
-
id:
|
|
28592
|
+
id: randomUUID4(),
|
|
27980
28593
|
namespace: row.namespace,
|
|
27981
28594
|
packId: row.packId,
|
|
27982
28595
|
version: row.version,
|
|
@@ -27988,7 +28601,7 @@ var SqliteInstalledPacksRepository = class {
|
|
|
27988
28601
|
if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
|
|
27989
28602
|
this.upsertAvailableStmt.run({
|
|
27990
28603
|
...params,
|
|
27991
|
-
id:
|
|
28604
|
+
id: randomUUID4(),
|
|
27992
28605
|
recordedBy: meta4?.recordedBy ?? null
|
|
27993
28606
|
});
|
|
27994
28607
|
} else {
|
|
@@ -28234,9 +28847,65 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28234
28847
|
// NOT on the hook path — so, unlike recordInventory, these surface errors to the
|
|
28235
28848
|
// caller rather than swallowing them. Each returns whether a row matched, so the
|
|
28236
28849
|
// caller can tell an edit from a no-such-detection.
|
|
28850
|
+
/**
|
|
28851
|
+
* The rules one installed pack owns, reduced to what a floor computation
|
|
28852
|
+
* reads. Display-tolerant parsing on purpose: a pack whose snapshot is
|
|
28853
|
+
* unreadable contributes no rules to a scan either, so it is not a detection
|
|
28854
|
+
* the control plane can be governing, and an empty list correctly imposes no
|
|
28855
|
+
* floor. Enabled state is deliberately not filtered — a disabled pack is one
|
|
28856
|
+
* the user can re-enable, and its assignment stays governed meanwhile.
|
|
28857
|
+
*/
|
|
28858
|
+
packFloorRules(namespace, packId) {
|
|
28859
|
+
const row = getRow(this.packRulesStmt, [namespace, packId]);
|
|
28860
|
+
if (!row) return [];
|
|
28861
|
+
return parseRules(row.rulesJson).map((rule) => ({ id: rule.id, category: rule.category }));
|
|
28862
|
+
}
|
|
28863
|
+
/**
|
|
28864
|
+
* What the connected control plane imposes on one installed pack, or null on a
|
|
28865
|
+
* machine that is its own authority (standalone, no cached bundle, or a
|
|
28866
|
+
* repository constructed without a layout base).
|
|
28867
|
+
*
|
|
28868
|
+
* Exposed as a READ so a surface can render the constraint — grey out the
|
|
28869
|
+
* choices below the floor, mark a locked detection as locked — rather than
|
|
28870
|
+
* offer the user a picker whose selections it will then be told it may not
|
|
28871
|
+
* make. The refusal in `setPolicy` does not depend on any surface calling this.
|
|
28872
|
+
*/
|
|
28873
|
+
policyFloor(namespace, packId) {
|
|
28874
|
+
if (this.baseDir === void 0) return null;
|
|
28875
|
+
return controlPlanePolicyFloor(this.packFloorRules(namespace, packId), this.baseDir);
|
|
28876
|
+
}
|
|
28877
|
+
/**
|
|
28878
|
+
* The same answer for several packs, keyed `namespace/packId` and carrying an
|
|
28879
|
+
* entry only for a pack the control plane actually governs.
|
|
28880
|
+
*
|
|
28881
|
+
* A surface listing every detection asks per pack, and asking through
|
|
28882
|
+
* `policyFloor` re-reads the settings, re-reads and re-parses the whole cached
|
|
28883
|
+
* bundle and rebuilds its indexes once per pack — the entire cost of one
|
|
28884
|
+
* answer, repeated for each row, on every render. This reads all of that once.
|
|
28885
|
+
* Packs whose rules the snapshot cannot produce simply contribute no entry,
|
|
28886
|
+
* exactly as the single-pack read returns null for them.
|
|
28887
|
+
*/
|
|
28888
|
+
policyFloors(packs2) {
|
|
28889
|
+
const floors = /* @__PURE__ */ new Map();
|
|
28890
|
+
if (this.baseDir === void 0) return floors;
|
|
28891
|
+
const source = openControlPlaneFloors(this.baseDir);
|
|
28892
|
+
if (source === null) return floors;
|
|
28893
|
+
for (const pack of packs2) {
|
|
28894
|
+
const floor = source.floorFor(this.packFloorRules(pack.namespace, pack.packId));
|
|
28895
|
+
if (floor !== null) floors.set(`${pack.namespace}/${pack.packId}`, floor);
|
|
28896
|
+
}
|
|
28897
|
+
return floors;
|
|
28898
|
+
}
|
|
28237
28899
|
/**
|
|
28238
28900
|
* Assign (or clear, with null) the enforcement policy for one installed pack.
|
|
28239
|
-
* `policyId` must be a known built-in id (monitor/warn/redact/block).
|
|
28901
|
+
* `policyId` must be a known built-in id (monitor/warn/redact/block/vault).
|
|
28902
|
+
*
|
|
28903
|
+
* On an ATTACHED machine the organization's bundle is a floor this refuses to
|
|
28904
|
+
* write below, and a detection the organization has authored a policy for is
|
|
28905
|
+
* refused outright — see policy-floor.ts for both, and for why the refusal is
|
|
28906
|
+
* a throw rather than a silently substituted value. This is the one device-local
|
|
28907
|
+
* write path for the assignment, so the check belongs here rather than on any
|
|
28908
|
+
* surface that offers the choice.
|
|
28240
28909
|
*/
|
|
28241
28910
|
setPolicy(namespace, packId, policyId) {
|
|
28242
28911
|
if (policyId !== null && !BuiltinPolicyId.safeParse(policyId).success) {
|
|
@@ -28244,14 +28913,38 @@ var SqliteInstalledPacksRepository = class {
|
|
|
28244
28913
|
`Unknown policy '${policyId}'. Must be one of: ${KNOWN_BUILTIN_IDS.join(", ")}.`
|
|
28245
28914
|
);
|
|
28246
28915
|
}
|
|
28916
|
+
const requested = policyId;
|
|
28917
|
+
const floor = this.policyFloor(namespace, packId);
|
|
28918
|
+
if (floor !== null) {
|
|
28919
|
+
const refusal = policyAssignmentRefusal(requested, floor);
|
|
28920
|
+
if (refusal !== null) {
|
|
28921
|
+
throw new PolicyFloorError(`${namespace}/${packId}`, requested, floor.floor, refusal);
|
|
28922
|
+
}
|
|
28923
|
+
}
|
|
28247
28924
|
const res = this.db.prepare(
|
|
28248
28925
|
`UPDATE installed_packs SET policy_id = :policyId, updated_at = :now
|
|
28249
28926
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
28250
28927
|
).run({ policyId, now: Date.now(), namespace, packId });
|
|
28251
28928
|
return Number(res.changes) > 0;
|
|
28252
28929
|
}
|
|
28253
|
-
/**
|
|
28930
|
+
/**
|
|
28931
|
+
* Enable or disable one installed pack.
|
|
28932
|
+
*
|
|
28933
|
+
* On an ATTACHED machine a detection the organization's bundle governs at all
|
|
28934
|
+
* may not be switched OFF here — see packEnablementRefusal for why that is not
|
|
28935
|
+
* merely another point below the floor, and why re-enabling stays open. Like
|
|
28936
|
+
* the assignment above, the check belongs at this write path rather than on a
|
|
28937
|
+
* surface: this is the one device-local writer of the column, and a refusal
|
|
28938
|
+
* that lived in a page would leave the CLI free.
|
|
28939
|
+
*/
|
|
28254
28940
|
setEnabled(namespace, packId, enabled) {
|
|
28941
|
+
const floor = this.policyFloor(namespace, packId);
|
|
28942
|
+
if (floor !== null) {
|
|
28943
|
+
const refusal = packEnablementRefusal(enabled, floor);
|
|
28944
|
+
if (refusal !== null) {
|
|
28945
|
+
throw new PolicyFloorError(`${namespace}/${packId}`, null, floor.floor, refusal);
|
|
28946
|
+
}
|
|
28947
|
+
}
|
|
28255
28948
|
const res = this.db.prepare(
|
|
28256
28949
|
`UPDATE installed_packs SET enabled = :enabled, updated_at = :now
|
|
28257
28950
|
WHERE namespace = :namespace AND pack_id = :packId`
|
|
@@ -28337,7 +29030,7 @@ var SqliteInventoryRepository = class {
|
|
|
28337
29030
|
};
|
|
28338
29031
|
|
|
28339
29032
|
// ../../packages/persistence/src/repositories/inventory-assets.ts
|
|
28340
|
-
import { randomUUID as
|
|
29033
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
28341
29034
|
var CATEGORY_ORDER = ["config", "skill", "mcp", "hook"];
|
|
28342
29035
|
var VALID_HARNESS_IDS = new Set(HarnessId.options);
|
|
28343
29036
|
var WORKTREE_CHECKOUT_FILTER = "(url IS NULL OR (url NOT LIKE '%/.claude/worktrees/%' AND url NOT LIKE '%\\.claude\\worktrees\\%'))";
|
|
@@ -28826,7 +29519,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
28826
29519
|
`INSERT INTO file_access_override (id, project_id, path, access, created_at, updated_at)
|
|
28827
29520
|
VALUES (:id, :projectId, :path, :access, :now, :now)
|
|
28828
29521
|
ON CONFLICT (project_id, path) DO UPDATE SET access = excluded.access, updated_at = excluded.updated_at`
|
|
28829
|
-
).run({ id:
|
|
29522
|
+
).run({ id: randomUUID5(), projectId, path, access, now: Date.now() });
|
|
28830
29523
|
}
|
|
28831
29524
|
return true;
|
|
28832
29525
|
}
|
|
@@ -28847,7 +29540,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
28847
29540
|
`INSERT INTO mcp_trust_override (id, asset_id, trust, created_at, updated_at)
|
|
28848
29541
|
VALUES (:id, :assetId, :trust, :now, :now)
|
|
28849
29542
|
ON CONFLICT (asset_id) DO UPDATE SET trust = excluded.trust, updated_at = excluded.updated_at`
|
|
28850
|
-
).run({ id:
|
|
29543
|
+
).run({ id: randomUUID5(), assetId, trust, now: Date.now() });
|
|
28851
29544
|
}
|
|
28852
29545
|
this.configRowsCache = void 0;
|
|
28853
29546
|
return "ok";
|
|
@@ -29144,7 +29837,7 @@ var SqliteInventoryAssetsRepository = class {
|
|
|
29144
29837
|
};
|
|
29145
29838
|
|
|
29146
29839
|
// ../../packages/persistence/src/repositories/policies.ts
|
|
29147
|
-
import { randomUUID as
|
|
29840
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
29148
29841
|
var SqlitePoliciesRepository = class {
|
|
29149
29842
|
constructor(db) {
|
|
29150
29843
|
this.db = db;
|
|
@@ -29179,7 +29872,7 @@ var SqlitePoliciesRepository = class {
|
|
|
29179
29872
|
failOpenTransaction(this.db, () => {
|
|
29180
29873
|
for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
|
|
29181
29874
|
stmt.run({
|
|
29182
|
-
id:
|
|
29875
|
+
id: randomUUID6(),
|
|
29183
29876
|
target: JSON.stringify({ category }),
|
|
29184
29877
|
action,
|
|
29185
29878
|
now: Date.now()
|
|
@@ -29199,7 +29892,7 @@ var SqlitePoliciesRepository = class {
|
|
|
29199
29892
|
`INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
|
|
29200
29893
|
VALUES (:id, 'global', :target, :action, 1, :now, :now)
|
|
29201
29894
|
ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
|
|
29202
|
-
).run({ id:
|
|
29895
|
+
).run({ id: randomUUID6(), target: JSON.stringify({ category }), action, now });
|
|
29203
29896
|
}
|
|
29204
29897
|
// Caps every global per-category policy currently set to block/redact down
|
|
29205
29898
|
// to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
|
|
@@ -29267,7 +29960,7 @@ var SqlitePolicyCatalogRepository = class {
|
|
|
29267
29960
|
};
|
|
29268
29961
|
|
|
29269
29962
|
// ../../packages/persistence/src/repositories/project-files.ts
|
|
29270
|
-
import { randomUUID as
|
|
29963
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
29271
29964
|
var SqliteProjectFilesRepository = class {
|
|
29272
29965
|
constructor(db) {
|
|
29273
29966
|
this.db = db;
|
|
@@ -29299,7 +29992,7 @@ var SqliteProjectFilesRepository = class {
|
|
|
29299
29992
|
const stamp = Math.max(now, maxStamp + 1);
|
|
29300
29993
|
for (const file2 of scan2.files) {
|
|
29301
29994
|
this.upsertStmt.run({
|
|
29302
|
-
id:
|
|
29995
|
+
id: randomUUID7(),
|
|
29303
29996
|
projectId,
|
|
29304
29997
|
path: file2.path,
|
|
29305
29998
|
name: file2.name,
|
|
@@ -29313,9 +30006,9 @@ var SqliteProjectFilesRepository = class {
|
|
|
29313
30006
|
};
|
|
29314
30007
|
|
|
29315
30008
|
// ../../packages/persistence/src/repositories/resolutions.ts
|
|
29316
|
-
import { randomUUID as
|
|
30009
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
29317
30010
|
var SqliteResolutionsRepository = class {
|
|
29318
|
-
constructor(db, now = () => Date.now(), newId = () =>
|
|
30011
|
+
constructor(db, now = () => Date.now(), newId = () => randomUUID8()) {
|
|
29319
30012
|
this.db = db;
|
|
29320
30013
|
this.now = now;
|
|
29321
30014
|
this.newId = newId;
|
|
@@ -29528,7 +30221,7 @@ var SqliteScanLedgerRepository = class {
|
|
|
29528
30221
|
};
|
|
29529
30222
|
|
|
29530
30223
|
// ../../packages/persistence/src/repositories/secret-vault.ts
|
|
29531
|
-
import { randomUUID as
|
|
30224
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
29532
30225
|
function pageLimit(requested, fallback) {
|
|
29533
30226
|
if (requested === void 0) return fallback;
|
|
29534
30227
|
return Math.min(Math.max(1, Math.trunc(requested)), MAX_VAULT_PAGE_LIMIT);
|
|
@@ -29574,12 +30267,14 @@ var SELECT_COLUMNS = `
|
|
|
29574
30267
|
ciphertext,
|
|
29575
30268
|
nonce,
|
|
29576
30269
|
auth_tag AS authTag,
|
|
30270
|
+
user_authorized AS userAuthorized,
|
|
29577
30271
|
occurrence_count AS occurrenceCount,
|
|
29578
30272
|
first_seen AS firstSeen,
|
|
29579
30273
|
last_seen AS lastSeen`;
|
|
29580
30274
|
function toRow(raw) {
|
|
29581
|
-
const { provider, ...rest } = raw;
|
|
29582
|
-
|
|
30275
|
+
const { provider, userAuthorized, ...rest } = raw;
|
|
30276
|
+
const row = { ...rest, userAuthorized: userAuthorized !== 0 };
|
|
30277
|
+
return provider === null ? row : { ...row, provider };
|
|
29583
30278
|
}
|
|
29584
30279
|
var SqliteSecretVaultRepository = class {
|
|
29585
30280
|
constructor(db) {
|
|
@@ -29589,17 +30284,18 @@ var SqliteSecretVaultRepository = class {
|
|
|
29589
30284
|
pointer_id, value_fingerprint, fingerprint_key_version, key_version,
|
|
29590
30285
|
format_version, category, rule_id, masked_match, provider,
|
|
29591
30286
|
ciphertext, nonce, auth_tag,
|
|
29592
|
-
occurrence_count, first_seen, last_seen
|
|
30287
|
+
user_authorized, occurrence_count, first_seen, last_seen
|
|
29593
30288
|
) VALUES (
|
|
29594
30289
|
:pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
|
|
29595
30290
|
:formatVersion, :category, :ruleId, :maskedMatch, :provider,
|
|
29596
30291
|
:ciphertext, :nonce, :authTag,
|
|
29597
|
-
1, :now, :now
|
|
30292
|
+
:userAuthorized, 1, :now, :now
|
|
29598
30293
|
)`
|
|
29599
30294
|
);
|
|
29600
30295
|
this.bumpStmt = db.prepare(
|
|
29601
30296
|
`UPDATE secret_vault
|
|
29602
|
-
SET occurrence_count = occurrence_count + 1, last_seen = :now
|
|
30297
|
+
SET occurrence_count = occurrence_count + 1, last_seen = :now,
|
|
30298
|
+
user_authorized = max(user_authorized, :userAuthorized)
|
|
29603
30299
|
WHERE value_fingerprint = :valueFingerprint`
|
|
29604
30300
|
);
|
|
29605
30301
|
this.byPointerStmt = db.prepare(
|
|
@@ -29619,6 +30315,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29619
30315
|
SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
|
|
29620
30316
|
WHERE pointer_id = :pointerId`
|
|
29621
30317
|
);
|
|
30318
|
+
this.deleteByPointerStmt = db.prepare(`DELETE FROM secret_vault WHERE pointer_id = :pointerId`);
|
|
29622
30319
|
this.derefStmt = db.prepare(
|
|
29623
30320
|
`INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
|
|
29624
30321
|
VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
|
|
@@ -29632,6 +30329,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29632
30329
|
listStmt;
|
|
29633
30330
|
replaceCiphertextStmt;
|
|
29634
30331
|
refreshFingerprintStmt;
|
|
30332
|
+
deleteByPointerStmt;
|
|
29635
30333
|
derefStmt;
|
|
29636
30334
|
/**
|
|
29637
30335
|
* Vault a value, or record another sighting of one already vaulted. Keyed on
|
|
@@ -29640,6 +30338,11 @@ var SqliteSecretVaultRepository = class {
|
|
|
29640
30338
|
* pointer, category and ciphertext, so the same secret always resolves to one
|
|
29641
30339
|
* wire token. `minted` is true only when this call created the row.
|
|
29642
30340
|
*
|
|
30341
|
+
* `userAuthorized` is the one field a repeat call may still change, and only
|
|
30342
|
+
* upwards: it records that a PERSON asked for this value to be replaced, and
|
|
30343
|
+
* the row is shared with every automatic path that vaults the same value. See
|
|
30344
|
+
* `bumpStmt` for why clearing it is the defect this shape exists to refuse.
|
|
30345
|
+
*
|
|
29643
30346
|
* The read-then-write runs in one IMMEDIATE transaction so two concurrent
|
|
29644
30347
|
* writers cannot both decide they are minting.
|
|
29645
30348
|
*/
|
|
@@ -29666,13 +30369,18 @@ var SqliteSecretVaultRepository = class {
|
|
|
29666
30369
|
ciphertext: input2.ciphertext,
|
|
29667
30370
|
nonce: input2.nonce,
|
|
29668
30371
|
authTag: input2.authTag,
|
|
30372
|
+
userAuthorized: input2.userAuthorized === true ? 1 : 0,
|
|
29669
30373
|
now
|
|
29670
30374
|
})
|
|
29671
30375
|
);
|
|
29672
30376
|
minted = true;
|
|
29673
30377
|
return;
|
|
29674
30378
|
}
|
|
29675
|
-
this.bumpStmt.run({
|
|
30379
|
+
this.bumpStmt.run({
|
|
30380
|
+
valueFingerprint: input2.valueFingerprint,
|
|
30381
|
+
userAuthorized: input2.userAuthorized === true ? 1 : 0,
|
|
30382
|
+
now
|
|
30383
|
+
});
|
|
29676
30384
|
},
|
|
29677
30385
|
"IMMEDIATE"
|
|
29678
30386
|
);
|
|
@@ -29732,6 +30440,42 @@ var SqliteSecretVaultRepository = class {
|
|
|
29732
30440
|
);
|
|
29733
30441
|
return destroyed;
|
|
29734
30442
|
}
|
|
30443
|
+
/**
|
|
30444
|
+
* Destroy the named entries and report WHICH ones went — the scoped
|
|
30445
|
+
* counterpart to `purgeAll`, for a caller that has already put those specific
|
|
30446
|
+
* values back where they came from. Ids the store does not hold are absent
|
|
30447
|
+
* from the answer rather than an error, so a set assembled from a stale read
|
|
30448
|
+
* is not a fault. The deref audit is left alone, exactly as the purge leaves
|
|
30449
|
+
* it.
|
|
30450
|
+
*
|
|
30451
|
+
* The ids come back rather than a count because the caller's next act is to
|
|
30452
|
+
* write a purge row per destroyed entry, and a record of destruction has to
|
|
30453
|
+
* be a record of what was really destroyed: a selection is a claim about a
|
|
30454
|
+
* read that has since gone stale, and auditing from it invents a purge for an
|
|
30455
|
+
* entry still sitting in the vault.
|
|
30456
|
+
*
|
|
30457
|
+
* One transaction over the whole set rather than a statement per id: the
|
|
30458
|
+
* caller hands this the result of a restore pass it has completed, and a
|
|
30459
|
+
* fault partway through must leave the vault as it was found rather than
|
|
30460
|
+
* destroying a prefix of it. The vault holds the only copy of what a pointer
|
|
30461
|
+
* stands for, so half a delete is not a state anything can recover from.
|
|
30462
|
+
*/
|
|
30463
|
+
deleteByPointerIds(pointerIds) {
|
|
30464
|
+
if (pointerIds.length === 0) return [];
|
|
30465
|
+
const deleted = [];
|
|
30466
|
+
withTransaction(
|
|
30467
|
+
this.db,
|
|
30468
|
+
() => {
|
|
30469
|
+
for (const pointerId of pointerIds) {
|
|
30470
|
+
if (Number(this.deleteByPointerStmt.run({ pointerId }).changes) > 0) {
|
|
30471
|
+
deleted.push(pointerId);
|
|
30472
|
+
}
|
|
30473
|
+
}
|
|
30474
|
+
},
|
|
30475
|
+
"IMMEDIATE"
|
|
30476
|
+
);
|
|
30477
|
+
return deleted;
|
|
30478
|
+
}
|
|
29735
30479
|
/**
|
|
29736
30480
|
* Record (or re-stamp) one place a pointer has been written. One row per
|
|
29737
30481
|
* (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
|
|
@@ -29744,7 +30488,7 @@ var SqliteSecretVaultRepository = class {
|
|
|
29744
30488
|
VALUES (:id, :pointerId, :location, :kind, :now, :now)
|
|
29745
30489
|
ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
|
|
29746
30490
|
).run({
|
|
29747
|
-
id:
|
|
30491
|
+
id: randomUUID9(),
|
|
29748
30492
|
pointerId: entry.pointerId,
|
|
29749
30493
|
location: entry.location,
|
|
29750
30494
|
kind: entry.kind,
|
|
@@ -29975,7 +30719,7 @@ function toUtcDateString(ms) {
|
|
|
29975
30719
|
return new Date(ms).toISOString().slice(0, 10);
|
|
29976
30720
|
}
|
|
29977
30721
|
function isTimeseriesSeverity(s) {
|
|
29978
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
30722
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
29979
30723
|
}
|
|
29980
30724
|
var SqliteSecurityRepository = class {
|
|
29981
30725
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30104,12 +30848,16 @@ var SqliteSecurityRepository = class {
|
|
|
30104
30848
|
const now = this.now();
|
|
30105
30849
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30106
30850
|
const rows = this.findingsInRange(windowStart, now);
|
|
30107
|
-
const points = Array.from(
|
|
30108
|
-
|
|
30109
|
-
|
|
30110
|
-
|
|
30111
|
-
|
|
30112
|
-
|
|
30851
|
+
const points = Array.from(
|
|
30852
|
+
{ length: numBuckets },
|
|
30853
|
+
(_, i) => ({
|
|
30854
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
30855
|
+
critical: 0,
|
|
30856
|
+
high: 0,
|
|
30857
|
+
medium: 0,
|
|
30858
|
+
low: 0
|
|
30859
|
+
})
|
|
30860
|
+
);
|
|
30113
30861
|
for (const r of rows) {
|
|
30114
30862
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30115
30863
|
const bucket = points[idx];
|
|
@@ -30257,15 +31005,15 @@ var SqliteSecurityRepository = class {
|
|
|
30257
31005
|
const from = now - RANGE_DAYS[range] * DAY_MS4;
|
|
30258
31006
|
const rows = allRows(
|
|
30259
31007
|
this.db.prepare(
|
|
30260
|
-
`SELECT
|
|
31008
|
+
`SELECT e.repo AS repo, count(*) AS c
|
|
30261
31009
|
FROM inspection_findings f
|
|
30262
31010
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
30263
31011
|
WHERE e.started_at >= :from AND e.started_at < :to
|
|
30264
31012
|
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
30265
|
-
AND
|
|
30266
|
-
AND
|
|
30267
|
-
GROUP BY repo
|
|
30268
|
-
ORDER BY c DESC, repo
|
|
31013
|
+
AND e.repo IS NOT NULL
|
|
31014
|
+
AND e.repo != ''
|
|
31015
|
+
GROUP BY e.repo
|
|
31016
|
+
ORDER BY c DESC, e.repo
|
|
30269
31017
|
LIMIT :limit`
|
|
30270
31018
|
),
|
|
30271
31019
|
{ from, to: now, limit }
|
|
@@ -30327,7 +31075,8 @@ var SqliteSecurityRepository = class {
|
|
|
30327
31075
|
`SELECT f.finding_key AS finding_key,
|
|
30328
31076
|
d.rule_id AS rule_id,
|
|
30329
31077
|
d.severity AS severity,
|
|
30330
|
-
|
|
31078
|
+
e.repo AS repo,
|
|
31079
|
+
e.file_path AS path,
|
|
30331
31080
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
30332
31081
|
latest.resolved_at AS latest_resolved_at
|
|
30333
31082
|
FROM ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
@@ -30346,6 +31095,7 @@ var SqliteSecurityRepository = class {
|
|
|
30346
31095
|
const items = rows.map((r) => ({
|
|
30347
31096
|
findingKey: r.finding_key,
|
|
30348
31097
|
ruleId: r.rule_id,
|
|
31098
|
+
repo: r.repo ?? "",
|
|
30349
31099
|
severity: r.severity,
|
|
30350
31100
|
path: r.path ?? "",
|
|
30351
31101
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -30355,13 +31105,66 @@ var SqliteSecurityRepository = class {
|
|
|
30355
31105
|
}));
|
|
30356
31106
|
return Promise.resolve({ items });
|
|
30357
31107
|
}
|
|
31108
|
+
/**
|
|
31109
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31110
|
+
*
|
|
31111
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31112
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31113
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31114
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31115
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31116
|
+
*
|
|
31117
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31118
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31119
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31120
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31121
|
+
* The two answer different questions and only this one has to match a link.
|
|
31122
|
+
*
|
|
31123
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31124
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31125
|
+
*/
|
|
31126
|
+
recommendationInputs() {
|
|
31127
|
+
const rows = allRows(
|
|
31128
|
+
this.db.prepare(
|
|
31129
|
+
`SELECT d.rule_id AS rule_id,
|
|
31130
|
+
d.category AS category,
|
|
31131
|
+
d.severity AS severity,
|
|
31132
|
+
COUNT(*) AS count
|
|
31133
|
+
FROM inspection_findings f
|
|
31134
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31135
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31136
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31137
|
+
ON latest.finding_key = f.finding_key
|
|
31138
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31139
|
+
AND e.event_type = 'code_change'
|
|
31140
|
+
AND (
|
|
31141
|
+
f.finding_key IS NULL
|
|
31142
|
+
OR latest.status IS NULL
|
|
31143
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31144
|
+
)
|
|
31145
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31146
|
+
)
|
|
31147
|
+
);
|
|
31148
|
+
return Promise.resolve(
|
|
31149
|
+
rows.map((r) => ({
|
|
31150
|
+
ruleId: r.rule_id,
|
|
31151
|
+
category: r.category,
|
|
31152
|
+
severity: r.severity,
|
|
31153
|
+
count: r.count
|
|
31154
|
+
}))
|
|
31155
|
+
);
|
|
31156
|
+
}
|
|
30358
31157
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
30359
31158
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
30360
31159
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
30361
31160
|
findingsInRange(fromMs, toMs) {
|
|
30362
31161
|
const rows = allRows(
|
|
30363
31162
|
this.db.prepare(
|
|
30364
|
-
`
|
|
31163
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31164
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31165
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31166
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31167
|
+
d.rule_id AS rule_id, d.category AS category
|
|
30365
31168
|
FROM inspection_findings f
|
|
30366
31169
|
JOIN audit_events e ON e.id = f.audit_event_id
|
|
30367
31170
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -30374,13 +31177,15 @@ var SqliteSecurityRepository = class {
|
|
|
30374
31177
|
return rows.map((r) => ({
|
|
30375
31178
|
occurredAt: r.occurred_at,
|
|
30376
31179
|
severity: r.severity,
|
|
30377
|
-
actionTaken: r.action_taken
|
|
31180
|
+
actionTaken: r.action_taken,
|
|
31181
|
+
ruleId: r.rule_id,
|
|
31182
|
+
category: r.category
|
|
30378
31183
|
}));
|
|
30379
31184
|
}
|
|
30380
31185
|
};
|
|
30381
31186
|
|
|
30382
31187
|
// ../../packages/persistence/src/repositories/shares.ts
|
|
30383
|
-
import { randomUUID as
|
|
31188
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
30384
31189
|
var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
|
|
30385
31190
|
var IN_CHUNK = 500;
|
|
30386
31191
|
var KIND_ORDER = ["provider", "internal", "external", "ip"];
|
|
@@ -30468,7 +31273,7 @@ function buildSummary(dest, endpoints) {
|
|
|
30468
31273
|
callSiteCount,
|
|
30469
31274
|
transports: distinctTransports(transports),
|
|
30470
31275
|
dataClasses: distinctDataClasses(dataClasses),
|
|
30471
|
-
review: buildReviewInfo(dest.trust, transports),
|
|
31276
|
+
review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
|
|
30472
31277
|
network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
|
|
30473
31278
|
endpoints: endpoints.map(toEndpointSummary)
|
|
30474
31279
|
};
|
|
@@ -30495,7 +31300,7 @@ function buildDetail(dest, endpoints, callSites) {
|
|
|
30495
31300
|
lastSeen: new Date(lastSeenMs).toISOString(),
|
|
30496
31301
|
transports: distinctTransports(transports),
|
|
30497
31302
|
dataClasses: distinctDataClasses(endpoints.map((e) => e.dataClass)),
|
|
30498
|
-
review: buildReviewInfo(dest.trust, transports),
|
|
31303
|
+
review: buildReviewInfo(dest.trust, transports, dest.overrideDecision !== null),
|
|
30499
31304
|
network: dest.kind === "provider" ? null : parseNetwork(dest.networkJson),
|
|
30500
31305
|
note: dest.note,
|
|
30501
31306
|
endpoints: endpoints.map((ep) => ({
|
|
@@ -30524,7 +31329,11 @@ var SqliteSharesRepository = class {
|
|
|
30524
31329
|
FROM share_destination d
|
|
30525
31330
|
LEFT JOIN share_endpoint e ON e.destination_id = d.id
|
|
30526
31331
|
AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
|
|
30527
|
-
WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL
|
|
31332
|
+
WHERE (d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL)
|
|
31333
|
+
AND NOT EXISTS (
|
|
31334
|
+
SELECT 1 FROM egress_decision_override o
|
|
31335
|
+
WHERE o.host = d.host OR (o.destination_id = d.id AND o.host IS NULL)
|
|
31336
|
+
)`
|
|
30528
31337
|
);
|
|
30529
31338
|
const kindCounts = countBy(
|
|
30530
31339
|
this.db,
|
|
@@ -30636,7 +31445,7 @@ var SqliteSharesRepository = class {
|
|
|
30636
31445
|
(id, destination_id, host, decision, created_at, updated_at)
|
|
30637
31446
|
VALUES (:id, :destinationId, :host, :decision, :now, :now)`
|
|
30638
31447
|
).run({
|
|
30639
|
-
id:
|
|
31448
|
+
id: randomUUID10(),
|
|
30640
31449
|
destinationId,
|
|
30641
31450
|
host: dest.host,
|
|
30642
31451
|
decision,
|
|
@@ -30785,7 +31594,7 @@ var SqliteSharesRepository = class {
|
|
|
30785
31594
|
let destinationId = destIds.get(hit.host);
|
|
30786
31595
|
if (destinationId === void 0) {
|
|
30787
31596
|
destStmt.run({
|
|
30788
|
-
id:
|
|
31597
|
+
id: randomUUID10(),
|
|
30789
31598
|
kind: hit.kind,
|
|
30790
31599
|
name: hit.name,
|
|
30791
31600
|
host: hit.host,
|
|
@@ -30801,7 +31610,7 @@ var SqliteSharesRepository = class {
|
|
|
30801
31610
|
let endpointId = endpointIds.get(endpointKey);
|
|
30802
31611
|
if (endpointId === void 0) {
|
|
30803
31612
|
endpointStmt.run({
|
|
30804
|
-
id:
|
|
31613
|
+
id: randomUUID10(),
|
|
30805
31614
|
destinationId,
|
|
30806
31615
|
method: hit.method,
|
|
30807
31616
|
transport: hit.transport,
|
|
@@ -30814,7 +31623,7 @@ var SqliteSharesRepository = class {
|
|
|
30814
31623
|
endpointIds.set(endpointKey, endpointId);
|
|
30815
31624
|
}
|
|
30816
31625
|
siteStmt.run({
|
|
30817
|
-
id:
|
|
31626
|
+
id: randomUUID10(),
|
|
30818
31627
|
endpointId,
|
|
30819
31628
|
project: input2.project,
|
|
30820
31629
|
projectKey: input2.projectKey,
|
|
@@ -31179,6 +31988,7 @@ function purgeSampleData(db) {
|
|
|
31179
31988
|
}
|
|
31180
31989
|
|
|
31181
31990
|
// ../../packages/persistence/src/database.ts
|
|
31991
|
+
var CAPTURE_GRAIN = new Set(EventKind.options);
|
|
31182
31992
|
var UNSAFE_TEST_ONLY_RAW_HANDLE = /* @__PURE__ */ Symbol(
|
|
31183
31993
|
"aka.persistence.unsafeTestOnlyRawHandle"
|
|
31184
31994
|
);
|
|
@@ -31226,7 +32036,7 @@ function backupLegacyStore(db, file2) {
|
|
|
31226
32036
|
discardStore(file2, backup);
|
|
31227
32037
|
return backup;
|
|
31228
32038
|
}
|
|
31229
|
-
function openAndInitialize(file2) {
|
|
32039
|
+
function openAndInitialize(file2, base) {
|
|
31230
32040
|
let db = openWithPragmas(file2);
|
|
31231
32041
|
try {
|
|
31232
32042
|
if (isForeignSqliteLineage(db)) {
|
|
@@ -31239,7 +32049,7 @@ function openAndInitialize(file2) {
|
|
|
31239
32049
|
applyMigrations(db, file2);
|
|
31240
32050
|
tightenPerms(file2);
|
|
31241
32051
|
const policies = new SqlitePoliciesRepository(db);
|
|
31242
|
-
const installedPacks = new SqliteInstalledPacksRepository(db);
|
|
32052
|
+
const installedPacks = new SqliteInstalledPacksRepository(db, base);
|
|
31243
32053
|
const repositories = {
|
|
31244
32054
|
events: new SqliteEventsRepository(db),
|
|
31245
32055
|
findings: new SqliteFindingsRepository(db),
|
|
@@ -31275,7 +32085,7 @@ function openAndInitialize(file2) {
|
|
|
31275
32085
|
}
|
|
31276
32086
|
function openLocalDatabase(dir) {
|
|
31277
32087
|
ensureDataDirSync(dir);
|
|
31278
|
-
const file2 =
|
|
32088
|
+
const file2 = join7(dir, DB_FILENAME);
|
|
31279
32089
|
reapStalePartials(file2);
|
|
31280
32090
|
const {
|
|
31281
32091
|
db,
|
|
@@ -31303,7 +32113,13 @@ function openLocalDatabase(dir) {
|
|
|
31303
32113
|
inspectionDefinitions,
|
|
31304
32114
|
inspectionFindings,
|
|
31305
32115
|
configInventory
|
|
31306
|
-
} = openAndInitialize(
|
|
32116
|
+
} = openAndInitialize(
|
|
32117
|
+
file2,
|
|
32118
|
+
// `dir` is always `<base>/data` — every caller resolves it through
|
|
32119
|
+
// `dataDir()` — so its parent is the `~/.aka` base the layout splits into
|
|
32120
|
+
// settings/ and data/, and the pack-policy floor needs both halves.
|
|
32121
|
+
dirname2(dir)
|
|
32122
|
+
);
|
|
31307
32123
|
function captureRowId(event) {
|
|
31308
32124
|
return captureId(
|
|
31309
32125
|
event.metadata?.sessionId ?? null,
|
|
@@ -31316,6 +32132,21 @@ function openLocalDatabase(dir) {
|
|
|
31316
32132
|
historySync.markSynced([captureRowId(event)], atMs);
|
|
31317
32133
|
});
|
|
31318
32134
|
}
|
|
32135
|
+
function markCaptureOwed(event) {
|
|
32136
|
+
failOpenTransaction(db, () => {
|
|
32137
|
+
historySync.markCaptureOwed(captureRowId(event));
|
|
32138
|
+
});
|
|
32139
|
+
}
|
|
32140
|
+
function markAuditEventsDelivered(events2, atMs) {
|
|
32141
|
+
const stampable = events2.filter((event) => !CAPTURE_GRAIN.has(event.eventType));
|
|
32142
|
+
if (stampable.length === 0) return;
|
|
32143
|
+
failOpenTransaction(db, () => {
|
|
32144
|
+
historySync.markSynced(
|
|
32145
|
+
stampable.map((event) => event.id),
|
|
32146
|
+
atMs
|
|
32147
|
+
);
|
|
32148
|
+
});
|
|
32149
|
+
}
|
|
31319
32150
|
function recordCapture(event, detected) {
|
|
31320
32151
|
failOpenTransaction(db, () => {
|
|
31321
32152
|
const sessionId = event.metadata?.sessionId;
|
|
@@ -31402,7 +32233,7 @@ function openLocalDatabase(dir) {
|
|
|
31402
32233
|
const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
|
|
31403
32234
|
if (!definitionId) continue;
|
|
31404
32235
|
inspectionFindings.insertFinding({
|
|
31405
|
-
id:
|
|
32236
|
+
id: randomUUID11(),
|
|
31406
32237
|
auditEventId: record2.scanEvent.id,
|
|
31407
32238
|
inspectionDefinitionId: definitionId,
|
|
31408
32239
|
span: finding.span,
|
|
@@ -31498,6 +32329,8 @@ function openLocalDatabase(dir) {
|
|
|
31498
32329
|
inspectionFindings,
|
|
31499
32330
|
recordCapture,
|
|
31500
32331
|
markCaptureDelivered,
|
|
32332
|
+
markCaptureOwed,
|
|
32333
|
+
markAuditEventsDelivered,
|
|
31501
32334
|
ensureInventory,
|
|
31502
32335
|
recordConfigScan,
|
|
31503
32336
|
recordProjectFiles,
|
|
@@ -31516,164 +32349,30 @@ function openLocalDatabase(dir) {
|
|
|
31516
32349
|
};
|
|
31517
32350
|
}
|
|
31518
32351
|
|
|
31519
|
-
// ../../packages/persistence/src/
|
|
31520
|
-
import {
|
|
31521
|
-
import {
|
|
31522
|
-
closeSync,
|
|
31523
|
-
existsSync as existsSync2,
|
|
31524
|
-
openSync,
|
|
31525
|
-
readFileSync as readFileSync2,
|
|
31526
|
-
rmSync as rmSync5,
|
|
31527
|
-
statSync as statSync3,
|
|
31528
|
-
writeFileSync as writeFileSync2
|
|
31529
|
-
} from "fs";
|
|
31530
|
-
import { hostname as hostname3 } from "os";
|
|
31531
|
-
var PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
32352
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
32353
|
+
import { createHash as createHash3 } from "crypto";
|
|
31532
32354
|
|
|
31533
32355
|
// ../../packages/persistence/src/finding-key.ts
|
|
31534
|
-
import { createHash as
|
|
32356
|
+
import { createHash as createHash4 } from "crypto";
|
|
31535
32357
|
|
|
31536
32358
|
// ../../packages/persistence/src/fingerprint.ts
|
|
31537
32359
|
import { createHmac, randomBytes } from "crypto";
|
|
31538
|
-
import { existsSync as existsSync3, readFileSync as
|
|
31539
|
-
import { join as
|
|
32360
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
32361
|
+
import { join as join8 } from "path";
|
|
31540
32362
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
31541
32363
|
|
|
31542
|
-
// ../../packages/persistence/src/history-
|
|
32364
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
31543
32365
|
import { existsSync as existsSync4 } from "fs";
|
|
31544
|
-
import { join as
|
|
31545
|
-
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
31546
|
-
|
|
31547
|
-
// ../../packages/persistence/src/local-layout.ts
|
|
31548
|
-
import { renameSync as renameSync3 } from "fs";
|
|
31549
|
-
import { mkdir } from "fs/promises";
|
|
31550
|
-
import { homedir } from "os";
|
|
31551
|
-
import { join as join7 } from "path";
|
|
31552
|
-
function defaultDataDir() {
|
|
31553
|
-
return join7(homedir(), ".aka");
|
|
31554
|
-
}
|
|
31555
|
-
function settingsDir(base = defaultDataDir()) {
|
|
31556
|
-
return join7(base, "settings");
|
|
31557
|
-
}
|
|
31558
|
-
function dataDir(base = defaultDataDir()) {
|
|
31559
|
-
return join7(base, "data");
|
|
31560
|
-
}
|
|
31561
|
-
function dbPath(base = defaultDataDir()) {
|
|
31562
|
-
return join7(dataDir(base), "aka.db");
|
|
31563
|
-
}
|
|
31564
|
-
function ensureLayoutDirSync(dir = defaultDataDir()) {
|
|
31565
|
-
ensureDataDirSync(dir);
|
|
31566
|
-
}
|
|
31567
|
-
function migrateLegacyLayout(base = defaultDataDir()) {
|
|
31568
|
-
const moves = [
|
|
31569
|
-
{ name: "config.json", dest: settingsDir(base) },
|
|
31570
|
-
{ name: "policy-cache.json", dest: dataDir(base) }
|
|
31571
|
-
];
|
|
31572
|
-
for (const { name, dest } of moves) {
|
|
31573
|
-
try {
|
|
31574
|
-
ensureDataDirSync(dest);
|
|
31575
|
-
const moved = join7(dest, name);
|
|
31576
|
-
renameSync3(join7(base, name), moved);
|
|
31577
|
-
tightenFile(moved);
|
|
31578
|
-
} catch {
|
|
31579
|
-
}
|
|
31580
|
-
}
|
|
31581
|
-
}
|
|
32366
|
+
import { join as join9 } from "path";
|
|
31582
32367
|
|
|
31583
|
-
// ../../packages/persistence/src/
|
|
31584
|
-
import {
|
|
31585
|
-
import {
|
|
31586
|
-
|
|
31587
|
-
if (platform2 === "darwin") {
|
|
31588
|
-
return [
|
|
31589
|
-
posix.join("/Library", "Application Support", "AKASecurity", MANAGED_SETTINGS_FILENAME),
|
|
31590
|
-
posix.join("/Library", "Managed Preferences", MANAGED_SETTINGS_FILENAME)
|
|
31591
|
-
];
|
|
31592
|
-
}
|
|
31593
|
-
if (platform2 === "win32") {
|
|
31594
|
-
return [win32.join("C:\\", "ProgramData", "AKASecurity", MANAGED_SETTINGS_FILENAME)];
|
|
31595
|
-
}
|
|
31596
|
-
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
31597
|
-
}
|
|
31598
|
-
function readManagedSettings(paths = managedSettingsPaths()) {
|
|
31599
|
-
for (const path of paths) {
|
|
31600
|
-
let text;
|
|
31601
|
-
try {
|
|
31602
|
-
text = readFileSync4(path, "utf8");
|
|
31603
|
-
} catch {
|
|
31604
|
-
continue;
|
|
31605
|
-
}
|
|
31606
|
-
const record2 = parseJsonObject(text);
|
|
31607
|
-
if (!record2) continue;
|
|
31608
|
-
const parsed = ManagedSettings.safeParse(record2);
|
|
31609
|
-
if (parsed.success) return parsed.data;
|
|
31610
|
-
}
|
|
31611
|
-
return null;
|
|
31612
|
-
}
|
|
31613
|
-
function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
|
|
31614
|
-
if (!managed) return settings;
|
|
31615
|
-
const { values } = managed;
|
|
31616
|
-
const merged = { ...settings };
|
|
31617
|
-
if (values.runMode !== void 0) merged.runMode = values.runMode;
|
|
31618
|
-
if (values.controlPlane !== void 0) {
|
|
31619
|
-
merged.controlPlane = {
|
|
31620
|
-
...values.controlPlane,
|
|
31621
|
-
// The administrator pinned WHICH deployment, not WHEN this machine
|
|
31622
|
-
// joined it. Keep the user's own attach time when the endpoint is
|
|
31623
|
-
// unchanged, so a managed machine does not appear to re-attach on every
|
|
31624
|
-
// read; stamp a fresh one when the administrator moved it.
|
|
31625
|
-
attachedAt: settings.controlPlane?.endpoint === values.controlPlane.endpoint ? settings.controlPlane.attachedAt : now().toISOString()
|
|
31626
|
-
};
|
|
31627
|
-
}
|
|
31628
|
-
if (values.historicalAccess !== void 0) merged.historicalAccess = values.historicalAccess;
|
|
31629
|
-
if (values.vaultKeyCustody !== void 0) merged.vaultKeyCustody = values.vaultKeyCustody;
|
|
31630
|
-
if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
|
|
31631
|
-
if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
|
|
31632
|
-
if (values.vaultConsent !== void 0) {
|
|
31633
|
-
merged.vaultConsent = values.vaultConsent ? (
|
|
31634
|
-
// Keep an existing valid grant so its acknowledgedAt survives; mint one
|
|
31635
|
-
// at the current version otherwise.
|
|
31636
|
-
settings.vaultConsent?.version === VAULT_CONSENT_VERSION ? settings.vaultConsent : { acknowledgedAt: now().toISOString(), version: VAULT_CONSENT_VERSION }
|
|
31637
|
-
) : void 0;
|
|
31638
|
-
}
|
|
31639
|
-
if (values.modelJudgeConsent !== void 0) {
|
|
31640
|
-
merged.modelJudgeConsent = values.modelJudgeConsent ? settings.modelJudgeConsent?.payloadVersion === MODEL_JUDGE_PAYLOAD_VERSION ? settings.modelJudgeConsent : {
|
|
31641
|
-
acknowledgedAt: now().toISOString(),
|
|
31642
|
-
payloadVersion: MODEL_JUDGE_PAYLOAD_VERSION
|
|
31643
|
-
} : void 0;
|
|
31644
|
-
}
|
|
31645
|
-
return merged;
|
|
31646
|
-
}
|
|
31647
|
-
|
|
31648
|
-
// ../../packages/persistence/src/settings.ts
|
|
31649
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
31650
|
-
import { join as join8 } from "path";
|
|
31651
|
-
var SETTINGS_FILENAME = "settings.json";
|
|
31652
|
-
function readWorkspaceSettings(base = defaultDataDir()) {
|
|
31653
|
-
return overlayManagedSettings(readUserSettings(base), readManagedSettings());
|
|
31654
|
-
}
|
|
31655
|
-
function readUserSettings(base) {
|
|
31656
|
-
const record2 = readJson(join8(settingsDir(base), SETTINGS_FILENAME));
|
|
31657
|
-
if (!record2) return defaultWorkspaceSettings();
|
|
31658
|
-
try {
|
|
31659
|
-
return WorkspaceSettings.parse(record2);
|
|
31660
|
-
} catch {
|
|
31661
|
-
return defaultWorkspaceSettings();
|
|
31662
|
-
}
|
|
31663
|
-
}
|
|
31664
|
-
function readJson(file2) {
|
|
31665
|
-
let text;
|
|
31666
|
-
try {
|
|
31667
|
-
text = readFileSync5(file2, "utf8");
|
|
31668
|
-
} catch {
|
|
31669
|
-
return null;
|
|
31670
|
-
}
|
|
31671
|
-
return parseJsonObject(text) ?? null;
|
|
31672
|
-
}
|
|
32368
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
32369
|
+
import { existsSync as existsSync5 } from "fs";
|
|
32370
|
+
import { join as join10 } from "path";
|
|
32371
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
31673
32372
|
|
|
31674
32373
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
31675
|
-
import { existsSync as
|
|
31676
|
-
import { dirname as
|
|
32374
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
32375
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
31677
32376
|
|
|
31678
32377
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
31679
32378
|
import {
|
|
@@ -31687,19 +32386,19 @@ import {
|
|
|
31687
32386
|
// ../../packages/persistence/src/vault/key-provider.ts
|
|
31688
32387
|
import { execFileSync } from "child_process";
|
|
31689
32388
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
31690
|
-
import { chmodSync as chmodSync3, readFileSync as
|
|
31691
|
-
import { join as
|
|
32389
|
+
import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
32390
|
+
import { join as join12 } from "path";
|
|
31692
32391
|
|
|
31693
32392
|
// ../../packages/persistence/src/vault/vault.ts
|
|
31694
32393
|
import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
|
|
31695
32394
|
|
|
31696
32395
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
31697
|
-
import { existsSync as
|
|
31698
|
-
import { join as
|
|
32396
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
32397
|
+
import { join as join13 } from "path";
|
|
31699
32398
|
|
|
31700
32399
|
// ../../packages/plugin-sdk/src/config.ts
|
|
31701
|
-
import { existsSync as
|
|
31702
|
-
import { join as
|
|
32400
|
+
import { existsSync as existsSync8 } from "fs";
|
|
32401
|
+
import { join as join14 } from "path";
|
|
31703
32402
|
|
|
31704
32403
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
31705
32404
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -31753,8 +32452,8 @@ function resolveProvider() {
|
|
|
31753
32452
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
31754
32453
|
try {
|
|
31755
32454
|
ensureLayoutDirSync(base);
|
|
31756
|
-
const settingsFile =
|
|
31757
|
-
if (
|
|
32455
|
+
const settingsFile = join14(settingsDir(base), "settings.json");
|
|
32456
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
31758
32457
|
} catch {
|
|
31759
32458
|
}
|
|
31760
32459
|
migrateLegacyLayout(base);
|
|
@@ -31777,9 +32476,9 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
31777
32476
|
}
|
|
31778
32477
|
|
|
31779
32478
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
31780
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
32479
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
31781
32480
|
import { homedir as homedir2 } from "os";
|
|
31782
|
-
import { basename as basename3, join as
|
|
32481
|
+
import { basename as basename3, join as join16 } from "path";
|
|
31783
32482
|
|
|
31784
32483
|
// ../../packages/detections/src/egress/registry.ts
|
|
31785
32484
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -34814,24 +35513,20 @@ function maskText(text) {
|
|
|
34814
35513
|
}
|
|
34815
35514
|
|
|
34816
35515
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
34817
|
-
import { existsSync as
|
|
34818
|
-
import { basename as basename2, dirname as
|
|
35516
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
|
|
35517
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
|
|
34819
35518
|
|
|
34820
35519
|
// ../../packages/plugin-sdk/src/events.ts
|
|
34821
|
-
import { createHash as
|
|
35520
|
+
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
34822
35521
|
|
|
34823
35522
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
34824
|
-
import { existsSync as
|
|
35523
|
+
import { existsSync as existsSync10 } from "fs";
|
|
34825
35524
|
import { fileURLToPath } from "url";
|
|
34826
35525
|
import { Worker } from "worker_threads";
|
|
34827
35526
|
|
|
34828
|
-
// ../../packages/plugin-sdk/src/
|
|
34829
|
-
|
|
34830
|
-
import {
|
|
34831
|
-
import { join as join15 } from "path";
|
|
34832
|
-
|
|
34833
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
34834
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
35527
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35528
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
35529
|
+
import { join as join18 } from "path";
|
|
34835
35530
|
|
|
34836
35531
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
34837
35532
|
import {
|
|
@@ -34843,16 +35538,42 @@ import {
|
|
|
34843
35538
|
readSync,
|
|
34844
35539
|
writeFileSync as writeFileSync5
|
|
34845
35540
|
} from "fs";
|
|
34846
|
-
import { join as
|
|
35541
|
+
import { join as join17 } from "path";
|
|
34847
35542
|
var TAIL_BYTES = 256 * 1024;
|
|
34848
35543
|
|
|
35544
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
35545
|
+
var HOST_FEATURE = {
|
|
35546
|
+
ModelSwitch: "model-switch",
|
|
35547
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
35548
|
+
};
|
|
35549
|
+
var HOST_FLOORS = {
|
|
35550
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
35551
|
+
label: "model-switch protection",
|
|
35552
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
35553
|
+
since: "2.1.251"
|
|
35554
|
+
},
|
|
35555
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
35556
|
+
label: "vault pointer display",
|
|
35557
|
+
hookEvents: ["MessageDisplay"],
|
|
35558
|
+
since: "2.1.152"
|
|
35559
|
+
}
|
|
35560
|
+
};
|
|
35561
|
+
|
|
35562
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
35563
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
35564
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
35565
|
+
import { join as join19 } from "path";
|
|
35566
|
+
|
|
35567
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
35568
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
35569
|
+
|
|
34849
35570
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
34850
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
34851
|
-
import { join as
|
|
35571
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
35572
|
+
import { join as join20 } from "path";
|
|
34852
35573
|
|
|
34853
35574
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
34854
35575
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
34855
|
-
import { basename as basename4, dirname as
|
|
35576
|
+
import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
|
|
34856
35577
|
|
|
34857
35578
|
// ../../packages/plugin-sdk/src/posture.ts
|
|
34858
35579
|
function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
@@ -34865,8 +35586,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
|
|
|
34865
35586
|
}
|
|
34866
35587
|
|
|
34867
35588
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
34868
|
-
import { existsSync as
|
|
34869
|
-
import { basename as basename5, join as
|
|
35589
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
35590
|
+
import { basename as basename5, join as join21 } from "path";
|
|
34870
35591
|
|
|
34871
35592
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
34872
35593
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -34984,11 +35705,11 @@ async function applySetupTriageSuppressions(entries, writer, opts) {
|
|
|
34984
35705
|
|
|
34985
35706
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
34986
35707
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
34987
|
-
import { join as
|
|
35708
|
+
import { join as join22 } from "path";
|
|
34988
35709
|
|
|
34989
35710
|
// ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
|
|
34990
35711
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
34991
|
-
import { join as
|
|
35712
|
+
import { join as join23 } from "path";
|
|
34992
35713
|
|
|
34993
35714
|
// ../../packages/setup-wizard/src/triage/dedupe.ts
|
|
34994
35715
|
function dedupeKey(hit) {
|
|
@@ -35043,12 +35764,12 @@ function deriveFalsePositivePatterns(hits, rec, plan) {
|
|
|
35043
35764
|
}
|
|
35044
35765
|
|
|
35045
35766
|
// ../../packages/setup-wizard/src/triage/gate-display.ts
|
|
35046
|
-
function findContext(entry,
|
|
35047
|
-
const byFingerprint =
|
|
35767
|
+
function findContext(entry, join27) {
|
|
35768
|
+
const byFingerprint = join27.find(
|
|
35048
35769
|
(j) => j.valueFingerprint !== void 0 && j.valueFingerprint === entry.valueFingerprint
|
|
35049
35770
|
);
|
|
35050
35771
|
if (byFingerprint) return byFingerprint.maskedContext;
|
|
35051
|
-
const byRuleAndMask =
|
|
35772
|
+
const byRuleAndMask = join27.find(
|
|
35052
35773
|
(j) => j.ruleId === entry.ruleId && j.maskedMatch === entry.maskedValue
|
|
35053
35774
|
);
|
|
35054
35775
|
return byRuleAndMask?.maskedContext;
|
|
@@ -35119,13 +35840,13 @@ function renderShowcase(showcase) {
|
|
|
35119
35840
|
|
|
35120
35841
|
${blocks.join("\n\n")}`;
|
|
35121
35842
|
}
|
|
35122
|
-
function renderSuppressionGate(entries,
|
|
35843
|
+
function renderSuppressionGate(entries, join27) {
|
|
35123
35844
|
if (entries.length === 0) {
|
|
35124
35845
|
return "No false-positive suppressions to confirm \u2014 nothing will be written.";
|
|
35125
35846
|
}
|
|
35126
35847
|
const header = entries.length === 1 ? "This looks like a false positive \u2014 take a look before I suppress it:" : `These ${String(entries.length)} look like false positives \u2014 take a look before I suppress them:`;
|
|
35127
35848
|
const blocks = entries.map((entry, i) => {
|
|
35128
|
-
const context = findContext(entry,
|
|
35849
|
+
const context = findContext(entry, join27);
|
|
35129
35850
|
const lines = [
|
|
35130
35851
|
`${String(i + 1)}. ${entry.ruleId} [${entry.category}]`,
|
|
35131
35852
|
` value: ${entry.maskedValue}`,
|
|
@@ -35211,9 +35932,9 @@ function mergeRecommendations(verdicts) {
|
|
|
35211
35932
|
}
|
|
35212
35933
|
|
|
35213
35934
|
// ../../packages/setup-wizard/src/triage/plan-file.ts
|
|
35214
|
-
import { mkdtempSync, readFileSync as
|
|
35935
|
+
import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
35215
35936
|
import { tmpdir } from "os";
|
|
35216
|
-
import { basename as basename6, dirname as
|
|
35937
|
+
import { basename as basename6, dirname as dirname6, join as join24 } from "path";
|
|
35217
35938
|
var SuppressionEntrySchema = external_exports.object({
|
|
35218
35939
|
ruleId: external_exports.string(),
|
|
35219
35940
|
category: DetectionCategory,
|
|
@@ -35268,19 +35989,19 @@ function serializePlan(plan, current) {
|
|
|
35268
35989
|
function writePlanFile(plan, current, rawValues, deps = {}) {
|
|
35269
35990
|
const serialized = serializePlan(plan, current);
|
|
35270
35991
|
assertRawFree(serialized, rawValues);
|
|
35271
|
-
const dir = (deps.mkTempDir ?? (() => mkdtempSync(
|
|
35272
|
-
const path =
|
|
35992
|
+
const dir = (deps.mkTempDir ?? (() => mkdtempSync(join24(tmpdir(), "aka-plan-"))))();
|
|
35993
|
+
const path = join24(dir, "setup-plan.json");
|
|
35273
35994
|
writeFileSync9(path, serialized, { encoding: "utf8", mode: 384 });
|
|
35274
35995
|
return path;
|
|
35275
35996
|
}
|
|
35276
35997
|
function readPlanFile(path) {
|
|
35277
|
-
const text =
|
|
35998
|
+
const text = readFileSync14(path, "utf8");
|
|
35278
35999
|
const json2 = JSON.parse(text);
|
|
35279
36000
|
return PersistedPlanSchema.parse(json2);
|
|
35280
36001
|
}
|
|
35281
36002
|
function deletePlanFile(path) {
|
|
35282
36003
|
rmSync7(path, { force: true });
|
|
35283
|
-
const dir =
|
|
36004
|
+
const dir = dirname6(path);
|
|
35284
36005
|
if (!basename6(dir).startsWith("aka-plan-")) return;
|
|
35285
36006
|
try {
|
|
35286
36007
|
rmdirSync(dir);
|
|
@@ -35348,8 +36069,8 @@ function buildJoinEntries(hits) {
|
|
|
35348
36069
|
}
|
|
35349
36070
|
|
|
35350
36071
|
// ../../packages/setup-wizard/src/triage/resolve.ts
|
|
35351
|
-
function resolveSuppressions(rec,
|
|
35352
|
-
const byId = new Map(
|
|
36072
|
+
function resolveSuppressions(rec, join27) {
|
|
36073
|
+
const byId = new Map(join27.map((e) => [e.id, e]));
|
|
35353
36074
|
const entries = [];
|
|
35354
36075
|
const skipped = [];
|
|
35355
36076
|
for (const cat of rec.perCategory) {
|
|
@@ -35451,7 +36172,7 @@ function parseTriageStream(text) {
|
|
|
35451
36172
|
return { hits, status: "complete" };
|
|
35452
36173
|
}
|
|
35453
36174
|
function planTriageWriteback(hits, rec) {
|
|
35454
|
-
const
|
|
36175
|
+
const join27 = buildJoinEntries(hits);
|
|
35455
36176
|
const rawValues = hits.map((h) => h.rawMatch);
|
|
35456
36177
|
const skipped = [];
|
|
35457
36178
|
const posture = {};
|
|
@@ -35491,7 +36212,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
35491
36212
|
}
|
|
35492
36213
|
const { entries, skipped: resolveSkips } = resolveSuppressions(
|
|
35493
36214
|
{ perCategory: safeCategories, notes: rec.notes },
|
|
35494
|
-
|
|
36215
|
+
join27
|
|
35495
36216
|
);
|
|
35496
36217
|
skipped.push(...resolveSkips);
|
|
35497
36218
|
let notes = rec.notes;
|
|
@@ -35501,7 +36222,7 @@ function planTriageWriteback(hits, rec) {
|
|
|
35501
36222
|
if (err instanceof RawEgressError) notes = SCRUBBED_NOTES;
|
|
35502
36223
|
else throw err;
|
|
35503
36224
|
}
|
|
35504
|
-
return { entries, posture, showcase, join:
|
|
36225
|
+
return { entries, posture, showcase, join: join27, notes, skipped };
|
|
35505
36226
|
}
|
|
35506
36227
|
function recommendedPosture(evidence) {
|
|
35507
36228
|
return { ...severityFloorPosture(), ...evidence };
|
|
@@ -35770,9 +36491,9 @@ function parseRecommendation(text) {
|
|
|
35770
36491
|
|
|
35771
36492
|
// src/triage/judge.ts
|
|
35772
36493
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
35773
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as
|
|
36494
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync8 } from "fs";
|
|
35774
36495
|
import { tmpdir as tmpdir2 } from "os";
|
|
35775
|
-
import { dirname as
|
|
36496
|
+
import { dirname as dirname7, join as join25 } from "path";
|
|
35776
36497
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
35777
36498
|
|
|
35778
36499
|
// ../../packages/plugin-sdk/src/bare-command.ts
|
|
@@ -35883,8 +36604,8 @@ function planBareCommand(command, args, deps = {}) {
|
|
|
35883
36604
|
}
|
|
35884
36605
|
|
|
35885
36606
|
// src/triage/judge.ts
|
|
35886
|
-
var TRIAGE_DIR =
|
|
35887
|
-
var DEFAULT_RUBRIC_PATH =
|
|
36607
|
+
var TRIAGE_DIR = dirname7(fileURLToPath2(import.meta.url));
|
|
36608
|
+
var DEFAULT_RUBRIC_PATH = join25(
|
|
35888
36609
|
TRIAGE_DIR,
|
|
35889
36610
|
"..",
|
|
35890
36611
|
"..",
|
|
@@ -35921,7 +36642,7 @@ function judgeEnv(platform2 = process.platform) {
|
|
|
35921
36642
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
35922
36643
|
};
|
|
35923
36644
|
if (platform2 === "darwin") {
|
|
35924
|
-
env.CLAUDE_CONFIG_DIR = mkdtempSync2(
|
|
36645
|
+
env.CLAUDE_CONFIG_DIR = mkdtempSync2(join25(tmpdir2(), "aka-judge-cfg-"));
|
|
35925
36646
|
}
|
|
35926
36647
|
return env;
|
|
35927
36648
|
}
|
|
@@ -35956,7 +36677,7 @@ function runJudge(hits, deps) {
|
|
|
35956
36677
|
if (typeof deps.spawn !== "function") {
|
|
35957
36678
|
throw new TypeError("runJudge requires deps.spawn \u2014 there is no live-spawn fallback");
|
|
35958
36679
|
}
|
|
35959
|
-
const rubric = deps.loadRubric?.() ??
|
|
36680
|
+
const rubric = deps.loadRubric?.() ?? readFileSync15(DEFAULT_RUBRIC_PATH, "utf8");
|
|
35960
36681
|
const hitsJsonl = hits.map((h) => JSON.stringify(toJudgePayload(h))).join("\n");
|
|
35961
36682
|
const fullPrompt = `${rubric}
|
|
35962
36683
|
|
|
@@ -36225,11 +36946,11 @@ function resolveCreatedBy() {
|
|
|
36225
36946
|
}
|
|
36226
36947
|
}
|
|
36227
36948
|
function loadRubric() {
|
|
36228
|
-
const here =
|
|
36229
|
-
const shipped =
|
|
36230
|
-
if (
|
|
36231
|
-
return
|
|
36232
|
-
|
|
36949
|
+
const here = dirname8(fileURLToPath4(import.meta.url));
|
|
36950
|
+
const shipped = join26(here, "triage-rubric.md");
|
|
36951
|
+
if (existsSync12(shipped)) return readFileSync16(shipped, "utf8");
|
|
36952
|
+
return readFileSync16(
|
|
36953
|
+
join26(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
|
|
36233
36954
|
"utf8"
|
|
36234
36955
|
);
|
|
36235
36956
|
}
|
|
@@ -36239,7 +36960,7 @@ async function main() {
|
|
|
36239
36960
|
argv,
|
|
36240
36961
|
// fd 0 = stdin; the wizard pipes `backfill.js --triage` into this on preview.
|
|
36241
36962
|
// Called only on the preview path — the confirm path never reads a stream.
|
|
36242
|
-
readStream: (streamPath) => streamPath !== void 0 ?
|
|
36963
|
+
readStream: (streamPath) => streamPath !== void 0 ? readFileSync16(streamPath, "utf8") : readFileSync16(0, "utf8"),
|
|
36243
36964
|
runJudge: (hits) => runJudge(hits, { spawn: spawnClaude, loadRubric }),
|
|
36244
36965
|
// The distinct model-judge egress consent, read from settings.json. When it
|
|
36245
36966
|
// is absent or stale the preview skips the judge instead of sending findings
|