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