@akasecurity/ai-tc-claude-code 0.9.10 → 0.9.12
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/commands/scan.md +1 -1
- package/commands/setup.md +31 -3
- package/package.json +4 -4
- package/scripts/apply-suppressions.js +1761 -947
- package/scripts/backfill.js +2097 -1184
- package/scripts/content-retention.js +33995 -0
- package/scripts/filescan.js +2149 -1102
- package/scripts/firstrun.js +1989 -1063
- package/scripts/history-sync.js +1943 -1037
- package/scripts/intro.js +530 -120
- package/scripts/message-display.js +1744 -930
- package/scripts/onboard.js +1748 -925
- package/scripts/post-model-switch.js +560 -144
- package/scripts/post-tool-use.js +2119 -1047
- package/scripts/pre-model-switch.js +2035 -1142
- package/scripts/pre-tool-use.js +2101 -1180
- package/scripts/query.js +2066 -1075
- package/scripts/reconcile.js +2058 -1165
- package/scripts/remediate.js +2091 -1176
- package/scripts/scan-worker.js +388 -74
- package/scripts/session-start.js +2025 -1108
- package/scripts/start-light.js +528 -118
- package/scripts/statusline.js +2031 -1138
- package/scripts/stop.js +791 -154
- package/scripts/sync.js +2144 -1099
- package/scripts/user-prompt-submit.js +2089 -1174
|
@@ -50,7 +50,7 @@ var require_ignore = __commonJS({
|
|
|
50
50
|
var REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
51
51
|
var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
|
|
52
52
|
var REGEX_TEST_TRAILING_SLASH = /\/$/;
|
|
53
|
-
var
|
|
53
|
+
var SLASH2 = "/";
|
|
54
54
|
var TMP_KEY_IGNORE = "node-ignore";
|
|
55
55
|
if (typeof Symbol !== "undefined") {
|
|
56
56
|
TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
|
|
@@ -422,11 +422,11 @@ var require_ignore = __commonJS({
|
|
|
422
422
|
if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
|
|
423
423
|
return this.test(path);
|
|
424
424
|
}
|
|
425
|
-
const slices = path.split(
|
|
425
|
+
const slices = path.split(SLASH2).filter(Boolean);
|
|
426
426
|
slices.pop();
|
|
427
427
|
if (slices.length) {
|
|
428
428
|
const parent = this._t(
|
|
429
|
-
slices.join(
|
|
429
|
+
slices.join(SLASH2) + SLASH2,
|
|
430
430
|
this._testCache,
|
|
431
431
|
true,
|
|
432
432
|
slices
|
|
@@ -442,14 +442,14 @@ var require_ignore = __commonJS({
|
|
|
442
442
|
return cache[path];
|
|
443
443
|
}
|
|
444
444
|
if (!slices) {
|
|
445
|
-
slices = path.split(
|
|
445
|
+
slices = path.split(SLASH2).filter(Boolean);
|
|
446
446
|
}
|
|
447
447
|
slices.pop();
|
|
448
448
|
if (!slices.length) {
|
|
449
449
|
return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
|
|
450
450
|
}
|
|
451
451
|
const parent = this._t(
|
|
452
|
-
slices.join(
|
|
452
|
+
slices.join(SLASH2) + SLASH2,
|
|
453
453
|
cache,
|
|
454
454
|
checkUnignored,
|
|
455
455
|
slices
|
|
@@ -492,8 +492,8 @@ var require_ignore = __commonJS({
|
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
// ../../packages/plugin-sdk/src/config.ts
|
|
495
|
-
import { existsSync as
|
|
496
|
-
import { join as
|
|
495
|
+
import { existsSync as existsSync8 } from "fs";
|
|
496
|
+
import { join as join16 } from "path";
|
|
497
497
|
|
|
498
498
|
// ../../packages/persistence/src/attached-derived.ts
|
|
499
499
|
import { rmSync } from "fs";
|
|
@@ -504,6 +504,14 @@ var POLICY_CACHE_FILENAME = "policy-cache.json";
|
|
|
504
504
|
import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
|
|
505
505
|
import { join as join2 } from "path";
|
|
506
506
|
|
|
507
|
+
// ../../packages/schema/src/drizzle/deferred-migrations.ts
|
|
508
|
+
var DEFERRED_MIGRATION_TAGS = [
|
|
509
|
+
"0031_audit_capture_by_time_index",
|
|
510
|
+
"0032_audit_capture_by_id_index",
|
|
511
|
+
"0033_audit_capture_location_index",
|
|
512
|
+
"0034_findings_read_indexes"
|
|
513
|
+
];
|
|
514
|
+
|
|
507
515
|
// ../../packages/schema/src/drizzle/sqlite-ddl.ts
|
|
508
516
|
var SQLITE_MIGRATIONS = [
|
|
509
517
|
{
|
|
@@ -621,6 +629,30 @@ var SQLITE_MIGRATIONS = [
|
|
|
621
629
|
{
|
|
622
630
|
tag: "0028_activity_session_probe_indexes",
|
|
623
631
|
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"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
tag: "0029_audit_capture_rollup_index",
|
|
635
|
+
sql: "CREATE INDEX `idx_audit_capture_rollup` ON `audit_events` (`event_type`,`started_at`,`repo`,`id`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
tag: "0030_audit_content_expiry",
|
|
639
|
+
sql: "ALTER TABLE `audit_events` ADD `content_expired_at` integer;--> statement-breakpoint\nCREATE INDEX `idx_audit_expirable_body` ON `audit_events` (`started_at`) WHERE content IS NOT NULL;"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
tag: "0031_audit_capture_by_time_index",
|
|
643
|
+
sql: "CREATE INDEX `idx_audit_capture_by_time` ON `audit_events` (`started_at`,`id`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
tag: "0032_audit_capture_by_id_index",
|
|
647
|
+
sql: "CREATE INDEX `idx_audit_capture_by_id` ON `audit_events` (`id`,`started_at`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
tag: "0033_audit_capture_location_index",
|
|
651
|
+
sql: "CREATE INDEX `idx_audit_capture_location` ON `audit_events` (`repo`,`file_path`,`started_at`,`id`,`event_type`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
tag: "0034_findings_read_indexes",
|
|
655
|
+
sql: "CREATE INDEX `idx_inspection_definitions_rule` ON `inspection_definitions` (`rule_id`,`severity`,`category`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_def` ON `inspection_findings` (`inspection_definition_id`,`audit_event_id`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_event_cover` ON `inspection_findings` (`audit_event_id`,`inspection_definition_id`,`action_taken`,`finding_key`,`id`);"
|
|
624
656
|
}
|
|
625
657
|
];
|
|
626
658
|
|
|
@@ -20668,6 +20700,15 @@ var FindingCategory = external_exports.enum([
|
|
|
20668
20700
|
]).meta({ id: "FindingCategory" });
|
|
20669
20701
|
var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
|
|
20670
20702
|
var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
|
|
20703
|
+
var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
|
|
20704
|
+
var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
|
|
20705
|
+
var FindingDelivery = external_exports.object({
|
|
20706
|
+
state: FindingDeliveryState,
|
|
20707
|
+
// The delivery time for `sent`; the failure time for `not_sent` when recorded.
|
|
20708
|
+
at: external_exports.iso.datetime().optional(),
|
|
20709
|
+
// Only on `not_sent`, and only when a known reason was recorded.
|
|
20710
|
+
reason: SyncFailureReason.optional()
|
|
20711
|
+
}).meta({ id: "FindingDelivery" });
|
|
20671
20712
|
var ResolutionMethod = external_exports.enum([
|
|
20672
20713
|
"enforced-in-flight",
|
|
20673
20714
|
"fixed-at-source",
|
|
@@ -20724,7 +20765,10 @@ var FindingInstance = external_exports.object({
|
|
|
20724
20765
|
// The session that event belongs to, when it has one — the seam a
|
|
20725
20766
|
// per-instance "view session" link needs. Absent for events captured
|
|
20726
20767
|
// outside a session.
|
|
20727
|
-
sessionId: external_exports.string().optional()
|
|
20768
|
+
sessionId: external_exports.string().optional(),
|
|
20769
|
+
// The delivery state of the event above (see FindingDelivery). Optional so
|
|
20770
|
+
// readers that do not project it stay valid.
|
|
20771
|
+
delivery: FindingDelivery.optional()
|
|
20728
20772
|
}).meta({ id: "FindingInstance" });
|
|
20729
20773
|
var FindingGroup = external_exports.object({
|
|
20730
20774
|
id: external_exports.string(),
|
|
@@ -20741,13 +20785,11 @@ var FindingGroup = external_exports.object({
|
|
|
20741
20785
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20742
20786
|
instances: external_exports.array(FindingInstance),
|
|
20743
20787
|
// Derived from instances' statuses with open-dominates precedence (see
|
|
20744
|
-
//
|
|
20788
|
+
// foldGroupStatus). Undefined only when no instance carries a status.
|
|
20745
20789
|
status: FindingStatus.optional(),
|
|
20746
|
-
// The distinct people across the WHOLE group, not just the
|
|
20747
|
-
//
|
|
20748
|
-
//
|
|
20749
|
-
// instance carries a user, or when the store supplied whole-group folds
|
|
20750
|
-
// without one.
|
|
20790
|
+
// The distinct people across the WHOLE group, not just the instances
|
|
20791
|
+
// carried here. Undefined when no instance carries a user, or when the
|
|
20792
|
+
// store supplied whole-group folds without one.
|
|
20751
20793
|
users: external_exports.array(FindingUser).optional()
|
|
20752
20794
|
}).meta({ id: "FindingGroup" });
|
|
20753
20795
|
var FindingStats = external_exports.object({
|
|
@@ -20776,21 +20818,34 @@ var FindingFacets = external_exports.object({
|
|
|
20776
20818
|
// counted under no value.
|
|
20777
20819
|
status: external_exports.array(FindingFacetItem),
|
|
20778
20820
|
// Host tool (attributes.tool_name). Present only on the instance-level
|
|
20779
|
-
// reads, which can filter by it; the
|
|
20821
|
+
// reads, which can filter by it; the type-level read omits the dimension
|
|
20780
20822
|
// because a group spans tools.
|
|
20781
|
-
tool: external_exports.array(FindingFacetItem).optional()
|
|
20823
|
+
tool: external_exports.array(FindingFacetItem).optional(),
|
|
20824
|
+
// Delivery states (FindingDeliveryState). Present only on the
|
|
20825
|
+
// instance-level reads, like `tool`.
|
|
20826
|
+
deployment: external_exports.array(FindingFacetItem).optional()
|
|
20782
20827
|
}).meta({ id: "FindingFacets" });
|
|
20783
|
-
var
|
|
20784
|
-
|
|
20828
|
+
var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
|
|
20829
|
+
id: "FindingTypeSummary"
|
|
20830
|
+
});
|
|
20831
|
+
var DEFAULT_FINDING_TYPES_LIMIT = 50;
|
|
20832
|
+
var MAX_FINDING_TYPES_LIMIT = 100;
|
|
20833
|
+
var ListFindingTypesQuery = external_exports.object({
|
|
20785
20834
|
// NOTE: severity filters by Severity (critical/high/medium/low), not by
|
|
20786
|
-
// FindingAction.
|
|
20835
|
+
// FindingAction. It narrows TYPES: a type's severity is the one its newest
|
|
20836
|
+
// firing version carries, and this list pages types.
|
|
20837
|
+
//
|
|
20838
|
+
// That is NOT a claim the findings of a type share it. A rule can hold several
|
|
20839
|
+
// definition versions at different severities, so a type kept by this filter
|
|
20840
|
+
// can hold findings that individually do not match — see totals.findings on
|
|
20841
|
+
// ListFindingTypesResponse, which counts them all.
|
|
20787
20842
|
severity: external_exports.array(Severity).optional(),
|
|
20788
20843
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20789
20844
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20790
20845
|
action: external_exports.array(FindingAction).optional(),
|
|
20791
|
-
// Matches a
|
|
20792
|
-
// individual
|
|
20793
|
-
//
|
|
20846
|
+
// Matches a type's DERIVED status (see FindingGroup.status), not its
|
|
20847
|
+
// individual findings' — so a filtered row's status always reads one of the
|
|
20848
|
+
// requested values.
|
|
20794
20849
|
status: external_exports.array(FindingStatus).optional(),
|
|
20795
20850
|
q: external_exports.string().optional(),
|
|
20796
20851
|
// Scope to findings whose event carries this session id (the Activity page's
|
|
@@ -20800,23 +20855,37 @@ var ListGroupedFindingsQuery = external_exports.object({
|
|
|
20800
20855
|
// from a time-scoped page (Activity's range) can carry that scope. Absent
|
|
20801
20856
|
// means all time — this list has no default window.
|
|
20802
20857
|
from: external_exports.iso.datetime().optional(),
|
|
20803
|
-
// A
|
|
20804
|
-
//
|
|
20805
|
-
//
|
|
20806
|
-
//
|
|
20807
|
-
//
|
|
20858
|
+
// A RULE id that must appear in the page even when the cursor has already
|
|
20859
|
+
// advanced past its sort position. This is what keeps the selected type
|
|
20860
|
+
// visible in the list once it paginates: the target is appended out of sort
|
|
20861
|
+
// order rather than scanned forward for. Never affects totals, facets or the
|
|
20862
|
+
// cursor. Unlike the grouped read this replaces, it names a rule only — an
|
|
20863
|
+
// instance id is resolved by `findingInstance`, which is a primary-key seek
|
|
20864
|
+
// and so is not bounded by what any page happens to hold.
|
|
20808
20865
|
includeId: external_exports.string().optional(),
|
|
20809
|
-
|
|
20810
|
-
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
20866
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
|
|
20811
20867
|
cursor: external_exports.string().optional()
|
|
20812
20868
|
});
|
|
20813
|
-
var
|
|
20869
|
+
var ListFindingTypesResponse = external_exports.object({
|
|
20814
20870
|
totals: external_exports.object({
|
|
20871
|
+
// Findings belonging to the matching TYPES — not findings that each match
|
|
20872
|
+
// the filters. The filters here select types, so a type that survives
|
|
20873
|
+
// contributes its whole instanceCount.
|
|
20874
|
+
//
|
|
20875
|
+
// `status` is the one exception, narrowed per finding via
|
|
20876
|
+
// countInstancesByStatus. `severity`, `provider` and `action` are not, so
|
|
20877
|
+
// this can exceed what the instance read reports for the same filters: a
|
|
20878
|
+
// rule whose severity moved between versions is kept on its newest and
|
|
20879
|
+
// still counts its older findings. Narrowing the other three needs
|
|
20880
|
+
// per-dimension counts the aggregate does not carry today.
|
|
20815
20881
|
findings: external_exports.number().int().nonnegative(),
|
|
20816
|
-
|
|
20882
|
+
// Counts TYPES, which is the unit this read pages. The instance read's
|
|
20883
|
+
// own totals count findings; the two deliberately answer different
|
|
20884
|
+
// questions and are never summed.
|
|
20885
|
+
types: external_exports.number().int().nonnegative()
|
|
20817
20886
|
}),
|
|
20818
20887
|
facets: FindingFacets,
|
|
20819
|
-
items: external_exports.array(
|
|
20888
|
+
items: external_exports.array(FindingTypeSummary),
|
|
20820
20889
|
nextCursor: external_exports.string().nullable(),
|
|
20821
20890
|
// Present only on session-scoped queries (`sessionId` set): per ruleId, how
|
|
20822
20891
|
// many times that rule fired in the session's persisted transcript. Findings
|
|
@@ -20824,7 +20893,7 @@ var ListGroupedFindingsResponse = external_exports.object({
|
|
|
20824
20893
|
// every firing, so the two numbers legitimately differ — this map lets a
|
|
20825
20894
|
// session-scoped view show both.
|
|
20826
20895
|
sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
|
|
20827
|
-
}).meta({ id: "
|
|
20896
|
+
}).meta({ id: "ListFindingTypesResponse" });
|
|
20828
20897
|
var ApplyFindingActionRequest = external_exports.object({
|
|
20829
20898
|
// 'quarantined' is system-assigned (see FindingAction) — clients may not set
|
|
20830
20899
|
// it, so it is excluded from the request contract. The mapping helper
|
|
@@ -20854,16 +20923,19 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
|
|
|
20854
20923
|
var MAX_FLAT_FINDINGS_LIMIT = 200;
|
|
20855
20924
|
var ListFindingInstancesQuery = external_exports.object({
|
|
20856
20925
|
severity: external_exports.array(Severity).optional(),
|
|
20857
|
-
// Rule ids, the same vocabulary the
|
|
20926
|
+
// Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
|
|
20927
|
+
// ONE of them is how the master/detail view scopes its right-hand panel.
|
|
20858
20928
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
20859
20929
|
provider: external_exports.array(FindingProvider).optional(),
|
|
20860
20930
|
action: external_exports.array(FindingAction).optional(),
|
|
20861
20931
|
// Matches each instance's OWN derived status (deriveFindingStatus), unlike
|
|
20862
|
-
// the
|
|
20932
|
+
// the types query's type-level fold.
|
|
20863
20933
|
status: external_exports.array(FindingStatus).optional(),
|
|
20864
20934
|
// Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
|
|
20865
20935
|
// where the free-text `q` can only match the rendered "via Bash" label.
|
|
20866
20936
|
tool: external_exports.array(external_exports.string()).optional(),
|
|
20937
|
+
// The delivery state of each finding's event (see FindingDelivery).
|
|
20938
|
+
deployment: external_exports.array(FindingDeliveryState).optional(),
|
|
20867
20939
|
// Exact repository / file-path matches, for the drill-down out of the
|
|
20868
20940
|
// locations view. A row whose event carries no repo/file matches neither.
|
|
20869
20941
|
repo: external_exports.string().optional(),
|
|
@@ -20876,37 +20948,51 @@ var ListFindingInstancesQuery = external_exports.object({
|
|
|
20876
20948
|
});
|
|
20877
20949
|
var ListFindingInstancesResponse = external_exports.object({
|
|
20878
20950
|
// Instances matching the filters across the whole scope, not just this
|
|
20879
|
-
// page — cursor-independent, like the
|
|
20951
|
+
// page — cursor-independent, like the types list's totals.
|
|
20880
20952
|
totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
|
|
20881
|
-
// Counts in INSTANCES here, where the
|
|
20953
|
+
// Counts in INSTANCES here, where the types response counts types. Each
|
|
20882
20954
|
// dimension still excludes its own filter.
|
|
20883
20955
|
facets: FindingFacets,
|
|
20884
20956
|
items: external_exports.array(FindingInstanceDetail),
|
|
20885
20957
|
nextCursor: external_exports.string().nullable()
|
|
20886
20958
|
}).meta({ id: "ListFindingInstancesResponse" });
|
|
20887
|
-
var
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20892
|
-
|
|
20893
|
-
|
|
20894
|
-
//
|
|
20895
|
-
//
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
// chips, and the count is what conveys scale.
|
|
20899
|
-
ruleIds: external_exports.array(external_exports.string())
|
|
20900
|
-
}).meta({ id: "FindingLocationFile" });
|
|
20901
|
-
var FindingLocationRepo = external_exports.object({
|
|
20959
|
+
var ListFindingInstancesPage = external_exports.object({
|
|
20960
|
+
items: external_exports.array(FindingInstanceDetail),
|
|
20961
|
+
nextCursor: external_exports.string().nullable()
|
|
20962
|
+
}).meta({ id: "ListFindingInstancesPage" });
|
|
20963
|
+
var FindingLocationSummary = external_exports.object({
|
|
20964
|
+
// Opaque, stable, minted from the pair by encodeLocationId. It exists
|
|
20965
|
+
// because a location's identity is two values and a URL param carries one:
|
|
20966
|
+
// `?loc=` names a location the way `?rule=` names a type. Only ever compared
|
|
20967
|
+
// for EQUALITY — the page's selection check, this read's `includeId`, the
|
|
20968
|
+
// client's page dedupe — never decoded, and never a sort key.
|
|
20969
|
+
id: external_exports.string(),
|
|
20902
20970
|
/** Empty when the instances carried no repo attribute. */
|
|
20903
20971
|
repo: external_exports.string(),
|
|
20972
|
+
// Empty when the instances carried no file path (a prompt, or a tool call
|
|
20973
|
+
// with no file attribution). Both halves empty is a real location — usually
|
|
20974
|
+
// the largest one in a store — and is selectable like any other.
|
|
20975
|
+
file: external_exports.string(),
|
|
20904
20976
|
instanceCount: external_exports.number().int().nonnegative(),
|
|
20977
|
+
// The WORST severity present, not the first row's. It is this list's primary
|
|
20978
|
+
// sort key, so it is also what explains why a row is where it is, and it is
|
|
20979
|
+
// how a reader decides what to open without opening everything.
|
|
20905
20980
|
maxSeverity: Severity,
|
|
20906
20981
|
latestDetectedAt: external_exports.iso.datetime(),
|
|
20982
|
+
// Folded from the instances' derived statuses with the same open-dominates
|
|
20983
|
+
// precedence a group uses, so it answers "is anything left to do here" and
|
|
20984
|
+
// not much more: a location holding 1 open among 40 resolved reads like one
|
|
20985
|
+
// holding 40 open. That loss is accepted — the panel beside this list
|
|
20986
|
+
// carries each finding's own status, and instanceCount sits next to the
|
|
20987
|
+
// badge.
|
|
20907
20988
|
status: FindingStatus.optional(),
|
|
20908
|
-
|
|
20909
|
-
|
|
20989
|
+
// Every distinct rule seen at this location, UNCAPPED — so the length is a
|
|
20990
|
+
// tally rather than a sample and a row can say how many there are. Bounded
|
|
20991
|
+
// by the ruleset, not by the store. The view bounds what it DISPLAYS.
|
|
20992
|
+
ruleIds: external_exports.array(external_exports.string())
|
|
20993
|
+
}).meta({ id: "FindingLocationSummary" });
|
|
20994
|
+
var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
|
|
20995
|
+
var MAX_FINDING_LOCATIONS_LIMIT = 100;
|
|
20910
20996
|
var ListFindingLocationsQuery = external_exports.object({
|
|
20911
20997
|
severity: external_exports.array(Severity).optional(),
|
|
20912
20998
|
subtype: external_exports.array(external_exports.string()).optional(),
|
|
@@ -20916,21 +21002,47 @@ var ListFindingLocationsQuery = external_exports.object({
|
|
|
20916
21002
|
// instances that match, and folds its status from those.
|
|
20917
21003
|
status: external_exports.array(FindingStatus).optional(),
|
|
20918
21004
|
tool: external_exports.array(external_exports.string()).optional(),
|
|
21005
|
+
// The delivery state of each finding's event (see FindingDelivery).
|
|
21006
|
+
deployment: external_exports.array(FindingDeliveryState).optional(),
|
|
20919
21007
|
q: external_exports.string().optional(),
|
|
20920
21008
|
sessionId: external_exports.string().optional(),
|
|
20921
21009
|
from: external_exports.iso.datetime().optional(),
|
|
20922
|
-
|
|
21010
|
+
// A LOCATION id (see FindingLocationSummary.id) that must appear in the page
|
|
21011
|
+
// even when the cursor has already advanced past its sort position — the
|
|
21012
|
+
// counterpart of ListFindingTypesQuery.includeId, and needed far more often
|
|
21013
|
+
// here. Selecting a row pushes the URL, which re-renders the server and resets
|
|
21014
|
+
// the client's page cache to page 0; with distinct (repo, file) pairs running
|
|
21015
|
+
// into the thousands, a selection sitting off page 0 is the ordinary case
|
|
21016
|
+
// rather than a deep-link corner. Never affects totals, facets or the cursor.
|
|
21017
|
+
includeId: external_exports.string().optional(),
|
|
21018
|
+
limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
|
|
21019
|
+
cursor: external_exports.string().optional()
|
|
20923
21020
|
});
|
|
20924
21021
|
var ListFindingLocationsResponse = external_exports.object({
|
|
20925
21022
|
totals: external_exports.object({
|
|
21023
|
+
// Findings matching the filters across the whole scope. Unlike the types
|
|
21024
|
+
// read's same-named field this needs no caveat: the filters here narrow
|
|
21025
|
+
// per finding, so this is the sum of every row's instanceCount.
|
|
20926
21026
|
findings: external_exports.number().int().nonnegative(),
|
|
20927
|
-
|
|
20928
|
-
|
|
21027
|
+
// Counts LOCATIONS, the unit this read pages — the number the paginator
|
|
21028
|
+
// states. The facets beside it count FINDINGS (see below); a surface
|
|
21029
|
+
// showing both says which is which.
|
|
21030
|
+
locations: external_exports.number().int().nonnegative()
|
|
20929
21031
|
}),
|
|
20930
|
-
|
|
20931
|
-
|
|
20932
|
-
|
|
20933
|
-
|
|
21032
|
+
// Counts in FINDINGS, where the types response counts types, each dimension
|
|
21033
|
+
// still excluding its own filter. Deliberately not locations: counting those
|
|
21034
|
+
// needs a set of location keys per dimension per value — memory tracking the
|
|
21035
|
+
// store times the vocabulary, in a read whose scan promises flat memory —
|
|
21036
|
+
// and the cheap per-location version is not an approximation but WRONG. A
|
|
21037
|
+
// location holding {claudecode, block} and {codex, warn} would survive
|
|
21038
|
+
// provider=claudecode AND action=warn, under which no single finding
|
|
21039
|
+
// matches, so the facet would contradict the instanceCount this whole view
|
|
21040
|
+
// rests on. Findings also keep the toolbar in the same unit as the page
|
|
21041
|
+
// tally and the panel it sits above.
|
|
21042
|
+
facets: FindingFacets,
|
|
21043
|
+
/** Sorted by max severity, then most recent, then (repo, file). */
|
|
21044
|
+
items: external_exports.array(FindingLocationSummary),
|
|
21045
|
+
nextCursor: external_exports.string().nullable()
|
|
20934
21046
|
}).meta({ id: "ListFindingLocationsResponse" });
|
|
20935
21047
|
|
|
20936
21048
|
// ../../packages/schema/src/zod/meta.ts
|
|
@@ -21094,6 +21206,10 @@ var CaptureAttributes = external_exports.object({
|
|
|
21094
21206
|
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
21095
21207
|
// authorized the bypass.
|
|
21096
21208
|
exception_ids: external_exports.array(external_exports.guid()).optional(),
|
|
21209
|
+
// The persisted spellings of EventMetadata's messageId/conversationId — the
|
|
21210
|
+
// join back to the `llm_call` leaf for the same assistant turn.
|
|
21211
|
+
message_id: external_exports.string().optional(),
|
|
21212
|
+
conversation_id: external_exports.string().optional(),
|
|
21097
21213
|
// Whole milliseconds this capture's inspection blocked its caller — the
|
|
21098
21214
|
// plugin's own added latency (see EventMetadata.inspectionMs, whose value
|
|
21099
21215
|
// this is). Promoted to the `inspection_ms` generated column so the facet is
|
|
@@ -21102,7 +21218,19 @@ var CaptureAttributes = external_exports.object({
|
|
|
21102
21218
|
// inline json_extract and is not itself an optimization.
|
|
21103
21219
|
// ABSENT on replayed captures (backfill / worktree scan) and on rows written
|
|
21104
21220
|
// before the measurement shipped — never present as a placeholder 0.
|
|
21105
|
-
inspection_ms: external_exports.number().int().nonnegative().optional()
|
|
21221
|
+
inspection_ms: external_exports.number().int().nonnegative().optional(),
|
|
21222
|
+
// What a `redact` this capture could not carry out became instead (see
|
|
21223
|
+
// EventMetadata.redactDegradedTo, whose value this is). Present only when a
|
|
21224
|
+
// degrade actually happened, so absence is the ordinary case rather than a
|
|
21225
|
+
// reader having to distinguish it from a zero.
|
|
21226
|
+
//
|
|
21227
|
+
// PER CAPTURE, while `inspection_findings.action_taken` is per finding —
|
|
21228
|
+
// so on a multi-finding row this does not say which finding degraded, and
|
|
21229
|
+
// its presence does not mean the fallback decided the capture's action. A
|
|
21230
|
+
// capture denied by another finding's own Block policy carries `block`
|
|
21231
|
+
// here too. The full statement is on EventMetadata.redactDegradedTo; it is
|
|
21232
|
+
// repeated rather than referenced because a store reader opens this file.
|
|
21233
|
+
redact_degraded_to: ActionTaken.optional()
|
|
21106
21234
|
}).catchall(external_exports.unknown());
|
|
21107
21235
|
var ToolCallInspection = external_exports.object({
|
|
21108
21236
|
ruleId: external_exports.string().min(1),
|
|
@@ -21301,7 +21429,17 @@ var AuditEvent = external_exports.object({
|
|
|
21301
21429
|
/** `share` to a first-party/internal destination. */
|
|
21302
21430
|
internal: external_exports.boolean(),
|
|
21303
21431
|
/** Event needs review (e.g. unverified egress). */
|
|
21304
|
-
flagged: external_exports.boolean()
|
|
21432
|
+
flagged: external_exports.boolean(),
|
|
21433
|
+
/**
|
|
21434
|
+
* The body this event's `title` is drawn from was cleared by local body
|
|
21435
|
+
* expiry, so an EMPTY title here means "gone", not "never had one".
|
|
21436
|
+
*
|
|
21437
|
+
* A separate flag rather than a sentinel written into `title`: the title is
|
|
21438
|
+
* rendered text, and a store-layer module that invented display copy for it
|
|
21439
|
+
* would be choosing words the view is supposed to choose. Additive and
|
|
21440
|
+
* defaulted, so an older producer still validates.
|
|
21441
|
+
*/
|
|
21442
|
+
bodyExpired: external_exports.boolean().default(false)
|
|
21305
21443
|
}).meta({ id: "ActivityAuditEvent" });
|
|
21306
21444
|
var ActivitySessionSummary = external_exports.object({
|
|
21307
21445
|
id: external_exports.string(),
|
|
@@ -22098,6 +22236,14 @@ var ControlPlaneErrorBody = external_exports.object({
|
|
|
22098
22236
|
message: external_exports.string().optional()
|
|
22099
22237
|
}).optional()
|
|
22100
22238
|
});
|
|
22239
|
+
var RemoteFailureKind = external_exports.enum([
|
|
22240
|
+
"unauthorized",
|
|
22241
|
+
"forbidden",
|
|
22242
|
+
"route-absent",
|
|
22243
|
+
"invalid-request",
|
|
22244
|
+
"rejected",
|
|
22245
|
+
"unreachable"
|
|
22246
|
+
]);
|
|
22101
22247
|
var AttachDeviceRequest = external_exports.object({
|
|
22102
22248
|
// This machine's own continuity id, so re-attaching ROTATES the credential
|
|
22103
22249
|
// on one machine record instead of producing a second one. Client-minted
|
|
@@ -22633,6 +22779,12 @@ var EventMetadata = external_exports.object({
|
|
|
22633
22779
|
// to 'allow' — the enforcement audit trail's link back to the grant that
|
|
22634
22780
|
// authorized the bypass. Absent on captures where no exception applied.
|
|
22635
22781
|
exceptionIds: external_exports.array(external_exports.guid()).optional(),
|
|
22782
|
+
// The assistant message this capture belongs to, and the conversation it sits
|
|
22783
|
+
// in — set by the browser extension's network capture so a stored `response`
|
|
22784
|
+
// row can be joined to the `llm_call` leaf describing the same turn. Absent
|
|
22785
|
+
// on every other capture path, which has no such id.
|
|
22786
|
+
messageId: external_exports.string().optional(),
|
|
22787
|
+
conversationId: external_exports.string().optional(),
|
|
22636
22788
|
// How long THIS capture's inspection blocked its caller, in whole
|
|
22637
22789
|
// milliseconds — the plugin's own added latency, NOT the LLM call it sat in
|
|
22638
22790
|
// front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
|
|
@@ -22645,7 +22797,37 @@ var EventMetadata = external_exports.object({
|
|
|
22645
22797
|
// Absent is also what every pre-measurement client writes, and what a
|
|
22646
22798
|
// clock failure degrades to — a reader must treat absence as "not measured"
|
|
22647
22799
|
// and never as a zero, which would read as "inspection is free".
|
|
22648
|
-
inspectionMs: external_exports.number().int().nonnegative().optional()
|
|
22800
|
+
inspectionMs: external_exports.number().int().nonnegative().optional(),
|
|
22801
|
+
// What a `redact` this capture COULD NOT CARRY OUT became instead — the
|
|
22802
|
+
// workspace's `redactFallback`, applied because the field could not be
|
|
22803
|
+
// masked in place (a shell command, a URL, or any argument on a host whose
|
|
22804
|
+
// hook contract offers no rewrite channel).
|
|
22805
|
+
//
|
|
22806
|
+
// It exists because the action alone cannot say why. A finding recorded as
|
|
22807
|
+
// `warn` reads identically whether its detection was ASSIGNED Warn or was
|
|
22808
|
+
// assigned Redact on a field that could not take one — and those are
|
|
22809
|
+
// different facts about the same row: the first is a policy the user chose,
|
|
22810
|
+
// the second is a masking the host could not perform. Absent means no
|
|
22811
|
+
// degrade happened, which is every ordinary capture.
|
|
22812
|
+
//
|
|
22813
|
+
// TWO LIMITS a reader of a stored row has to know, because the grain here
|
|
22814
|
+
// is the CAPTURE while `actionTaken` is per FINDING:
|
|
22815
|
+
//
|
|
22816
|
+
// - It does not say WHICH finding degraded. A capture carrying a degraded
|
|
22817
|
+
// `redact` alongside a finding ASSIGNED the same action stores both
|
|
22818
|
+
// identically and one reason for the pair; attributing it to both
|
|
22819
|
+
// describes the assigned one wrongly, and to neither loses the degrade.
|
|
22820
|
+
// - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
|
|
22821
|
+
// became, not the reason the capture ended as it did — a capture denied
|
|
22822
|
+
// by some other finding's own Block policy still carries `block` here,
|
|
22823
|
+
// and clearing the workspace's fallback would not have let it through.
|
|
22824
|
+
// Gate on the value against what a fallback can produce; never read the
|
|
22825
|
+
// field's presence as "this was the fallback's doing".
|
|
22826
|
+
//
|
|
22827
|
+
// Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
|
|
22828
|
+
// Closing either means moving the reason onto the finding row, which
|
|
22829
|
+
// already carries its own action.
|
|
22830
|
+
redactDegradedTo: ActionTaken.optional()
|
|
22649
22831
|
}).meta({ id: "EventMetadata" });
|
|
22650
22832
|
var Event = external_exports.object({
|
|
22651
22833
|
id: external_exports.guid(),
|
|
@@ -22755,7 +22937,32 @@ var RotateKeyInput = external_exports.object({
|
|
|
22755
22937
|
confirmation: external_exports.string()
|
|
22756
22938
|
});
|
|
22757
22939
|
|
|
22940
|
+
// ../../packages/schema/src/zod/finding-delivery.ts
|
|
22941
|
+
var KNOWN_REASONS = SyncFailureReason.options;
|
|
22942
|
+
function knownReason(value) {
|
|
22943
|
+
return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
|
|
22944
|
+
}
|
|
22945
|
+
function deriveFindingDelivery(row) {
|
|
22946
|
+
if (row.kind === "code_change") return { state: "local_scan" };
|
|
22947
|
+
if (row.syncedAt !== null && row.syncedAt > 0) {
|
|
22948
|
+
return { state: "sent", at: epochMillisToIso(row.syncedAt) };
|
|
22949
|
+
}
|
|
22950
|
+
if (row.syncedAt !== null) {
|
|
22951
|
+
const reason = knownReason(row.syncFailure);
|
|
22952
|
+
return {
|
|
22953
|
+
state: "not_sent",
|
|
22954
|
+
...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
|
|
22955
|
+
...reason === void 0 ? {} : { reason }
|
|
22956
|
+
};
|
|
22957
|
+
}
|
|
22958
|
+
if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
|
|
22959
|
+
return { state: "never_offered" };
|
|
22960
|
+
}
|
|
22961
|
+
|
|
22758
22962
|
// ../../packages/schema/src/zod/findings-group-build.ts
|
|
22963
|
+
function lookupOwn(map2, key) {
|
|
22964
|
+
return Object.hasOwn(map2, key) ? map2[key] : void 0;
|
|
22965
|
+
}
|
|
22759
22966
|
function toApiAction(dbVal) {
|
|
22760
22967
|
const map2 = {
|
|
22761
22968
|
log: "monitored",
|
|
@@ -22764,7 +22971,7 @@ function toApiAction(dbVal) {
|
|
|
22764
22971
|
warn: "warned",
|
|
22765
22972
|
allow: "allowed"
|
|
22766
22973
|
};
|
|
22767
|
-
return map2
|
|
22974
|
+
return lookupOwn(map2, dbVal) ?? "allowed";
|
|
22768
22975
|
}
|
|
22769
22976
|
function toApiCategory(dbVal) {
|
|
22770
22977
|
if (dbVal === "code_context") return "source_code";
|
|
@@ -22772,13 +22979,18 @@ function toApiCategory(dbVal) {
|
|
|
22772
22979
|
return parsed.success ? parsed.data : "custom";
|
|
22773
22980
|
}
|
|
22774
22981
|
function toApiProvider(sourceTool) {
|
|
22775
|
-
return TOOL_TO_HARNESS
|
|
22982
|
+
return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
|
|
22776
22983
|
}
|
|
22777
|
-
var
|
|
22984
|
+
var FINDING_STATUS_PRECEDENCE = [
|
|
22985
|
+
"open",
|
|
22986
|
+
"handled",
|
|
22987
|
+
"dismissed",
|
|
22988
|
+
"resolved"
|
|
22989
|
+
];
|
|
22778
22990
|
function foldGroupStatus(instanceStatuses) {
|
|
22779
22991
|
const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
|
|
22780
22992
|
if (statuses.size === 0) return void 0;
|
|
22781
|
-
for (const candidate of
|
|
22993
|
+
for (const candidate of FINDING_STATUS_PRECEDENCE) {
|
|
22782
22994
|
if (statuses.has(candidate)) return candidate;
|
|
22783
22995
|
}
|
|
22784
22996
|
return void 0;
|
|
@@ -22791,139 +23003,62 @@ function deriveFindingStatus(row) {
|
|
|
22791
23003
|
if (row.latestResolutionStatus === "dismissed") return "dismissed";
|
|
22792
23004
|
return "open";
|
|
22793
23005
|
}
|
|
22794
|
-
function distinctUsers(instances) {
|
|
22795
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22796
|
-
const users = [];
|
|
22797
|
-
for (const i of instances) {
|
|
22798
|
-
if (i.user === void 0 || seen.has(i.user.id)) continue;
|
|
22799
|
-
seen.add(i.user.id);
|
|
22800
|
-
users.push(i.user);
|
|
22801
|
-
}
|
|
22802
|
-
return users;
|
|
22803
|
-
}
|
|
22804
23006
|
function sortUsers(users) {
|
|
22805
23007
|
return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
22806
23008
|
}
|
|
22807
|
-
function
|
|
22808
|
-
const overrides = opts.overrides;
|
|
23009
|
+
function buildFindingTypes(aggregates, opts = {}) {
|
|
22809
23010
|
const packNames = opts.packNames;
|
|
22810
|
-
const
|
|
22811
|
-
const
|
|
22812
|
-
|
|
22813
|
-
const
|
|
22814
|
-
|
|
22815
|
-
else byRuleId.set(row.ruleId, [row]);
|
|
22816
|
-
}
|
|
22817
|
-
const groups = [];
|
|
22818
|
-
for (const [ruleId, ruleRows] of byRuleId) {
|
|
22819
|
-
const instances = ruleRows.map((r) => {
|
|
22820
|
-
const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
|
|
22821
|
-
return {
|
|
22822
|
-
id: r.id,
|
|
22823
|
-
provider: toApiProvider(r.sourceTool),
|
|
22824
|
-
repo: r.repo,
|
|
22825
|
-
file: r.file,
|
|
22826
|
-
...r.toolName === void 0 ? {} : { toolName: r.toolName },
|
|
22827
|
-
...r.eventId === void 0 ? {} : { eventId: r.eventId },
|
|
22828
|
-
...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
|
|
22829
|
-
...r.user === void 0 ? {} : { user: r.user },
|
|
22830
|
-
action: toApiAction(effectiveDbAction),
|
|
22831
|
-
detectedAt: r.occurredAt,
|
|
22832
|
-
confidence: r.confidence,
|
|
22833
|
-
status: r.status
|
|
22834
|
-
};
|
|
22835
|
-
});
|
|
22836
|
-
const agg = aggregates?.get(ruleId);
|
|
22837
|
-
const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
|
|
22838
|
-
const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
|
|
22839
|
-
(max, r) => r.occurredAt > max ? r.occurredAt : max,
|
|
22840
|
-
ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
22841
|
-
);
|
|
22842
|
-
const seenProviders = /* @__PURE__ */ new Set();
|
|
22843
|
-
const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
|
|
22844
|
-
if (seenProviders.has(p)) return false;
|
|
22845
|
-
seenProviders.add(p);
|
|
22846
|
-
return true;
|
|
22847
|
-
});
|
|
22848
|
-
const actionSet = new Set(
|
|
22849
|
-
agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
|
|
22850
|
-
);
|
|
23011
|
+
const types = [];
|
|
23012
|
+
for (const [ruleId, agg] of aggregates) {
|
|
23013
|
+
const users = sortUsers(agg.users ?? []);
|
|
23014
|
+
const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
|
|
23015
|
+
const actionSet = new Set(agg.actionsTaken.map(toApiAction));
|
|
22851
23016
|
const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
|
|
22852
|
-
const
|
|
22853
|
-
const
|
|
22854
|
-
id: ruleId,
|
|
22855
|
-
name: packNames?.get(ruleId) ?? null
|
|
22856
|
-
};
|
|
22857
|
-
const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
|
|
22858
|
-
const policy = { id: `category:${apiCategory}`, name: apiCategory };
|
|
22859
|
-
const match = {
|
|
22860
|
-
maskedValue: ruleRows[0]?.maskedMatch ?? "",
|
|
22861
|
-
contextPrefix: ""
|
|
22862
|
-
// empty (pending privacy review)
|
|
22863
|
-
};
|
|
22864
|
-
const status = foldGroupStatus(
|
|
22865
|
-
agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
|
|
22866
|
-
);
|
|
22867
|
-
const group = {
|
|
23017
|
+
const apiCategory = toApiCategory(agg.category ?? "custom");
|
|
23018
|
+
const type = {
|
|
22868
23019
|
id: ruleId,
|
|
22869
23020
|
category: apiCategory,
|
|
22870
23021
|
subtype: ruleId,
|
|
22871
23022
|
// human label comes with pack metadata later
|
|
22872
|
-
severity,
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
instanceCount: agg?.instanceCount ?? instances.length,
|
|
23023
|
+
severity: agg.severity ?? "low",
|
|
23024
|
+
detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
|
|
23025
|
+
policy: { id: `category:${apiCategory}`, name: apiCategory },
|
|
23026
|
+
instanceCount: agg.instanceCount,
|
|
22877
23027
|
providers,
|
|
22878
23028
|
aggregateAction,
|
|
22879
|
-
latestDetectedAt,
|
|
22880
|
-
|
|
22881
|
-
status,
|
|
23029
|
+
latestDetectedAt: agg.latestDetectedAt,
|
|
23030
|
+
status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
|
|
22882
23031
|
...users.length > 0 ? { users } : {}
|
|
22883
23032
|
};
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
|
|
22887
|
-
haystackCache.set(group, buildHaystack(group, agg.searchText));
|
|
22888
|
-
}
|
|
23033
|
+
actionsCache.set(type, [...actionSet]);
|
|
23034
|
+
if (agg.searchText !== void 0) {
|
|
23035
|
+
haystackCache.set(type, buildHaystack(type, agg.searchText));
|
|
22889
23036
|
}
|
|
22890
|
-
|
|
23037
|
+
types.push(type);
|
|
22891
23038
|
}
|
|
22892
|
-
return
|
|
23039
|
+
return types;
|
|
22893
23040
|
}
|
|
22894
23041
|
var haystackCache = /* @__PURE__ */ new WeakMap();
|
|
22895
|
-
function buildHaystack(
|
|
23042
|
+
function buildHaystack(t, extra) {
|
|
22896
23043
|
return [
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
...g.instances.map((i) => i.repo),
|
|
22903
|
-
...g.instances.map((i) => i.file),
|
|
22904
|
-
...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
|
|
22905
|
-
...g.instances.map((i) => i.id),
|
|
22906
|
-
// The people: the whole group's list when the store folded one, plus the
|
|
22907
|
-
// preview's own — the two overlap, and a haystack does not mind.
|
|
22908
|
-
...(g.users ?? []).map((u) => u.name),
|
|
22909
|
-
...g.instances.map((i) => i.user?.name ?? ""),
|
|
23044
|
+
t.subtype,
|
|
23045
|
+
t.category,
|
|
23046
|
+
t.policy.name,
|
|
23047
|
+
t.id,
|
|
23048
|
+
...(t.users ?? []).map((u) => u.name),
|
|
22910
23049
|
...extra === void 0 ? [] : [extra]
|
|
22911
23050
|
].join(" ").toLowerCase();
|
|
22912
23051
|
}
|
|
22913
|
-
function
|
|
22914
|
-
const cached2 = haystackCache.get(
|
|
23052
|
+
function typeHaystack(t) {
|
|
23053
|
+
const cached2 = haystackCache.get(t);
|
|
22915
23054
|
if (cached2 !== void 0) return cached2;
|
|
22916
|
-
const haystack = buildHaystack(
|
|
22917
|
-
haystackCache.set(
|
|
23055
|
+
const haystack = buildHaystack(t);
|
|
23056
|
+
haystackCache.set(t, haystack);
|
|
22918
23057
|
return haystack;
|
|
22919
23058
|
}
|
|
22920
23059
|
var actionsCache = /* @__PURE__ */ new WeakMap();
|
|
22921
|
-
function
|
|
22922
|
-
|
|
22923
|
-
if (cached2 !== void 0) return cached2;
|
|
22924
|
-
const actions = [...new Set(g.instances.map((i) => i.action))];
|
|
22925
|
-
actionsCache.set(g, actions);
|
|
22926
|
-
return actions;
|
|
23060
|
+
function typeActions(t) {
|
|
23061
|
+
return actionsCache.get(t) ?? [];
|
|
22927
23062
|
}
|
|
22928
23063
|
function countInstancesByStatus(statusInputs, statuses) {
|
|
22929
23064
|
const statusSet = new Set(statuses);
|
|
@@ -22934,8 +23069,8 @@ function countInstancesByStatus(statusInputs, statuses) {
|
|
|
22934
23069
|
}
|
|
22935
23070
|
return sum;
|
|
22936
23071
|
}
|
|
22937
|
-
function applyFindingFilters(
|
|
22938
|
-
let filtered =
|
|
23072
|
+
function applyFindingFilters(types, opts) {
|
|
23073
|
+
let filtered = types;
|
|
22939
23074
|
if (opts.severity && opts.severity.length > 0) {
|
|
22940
23075
|
const sevSet = new Set(opts.severity);
|
|
22941
23076
|
filtered = filtered.filter((g) => sevSet.has(g.severity));
|
|
@@ -22946,7 +23081,7 @@ function applyFindingFilters(groups, opts) {
|
|
|
22946
23081
|
}
|
|
22947
23082
|
if (opts.actions && opts.actions.length > 0) {
|
|
22948
23083
|
const actionSet = new Set(opts.actions);
|
|
22949
|
-
filtered = filtered.filter((
|
|
23084
|
+
filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
|
|
22950
23085
|
}
|
|
22951
23086
|
if (opts.subtype && opts.subtype.length > 0) {
|
|
22952
23087
|
const subtypeSet = new Set(opts.subtype);
|
|
@@ -22958,26 +23093,31 @@ function applyFindingFilters(groups, opts) {
|
|
|
22958
23093
|
}
|
|
22959
23094
|
if (opts.q) {
|
|
22960
23095
|
const q = opts.q.toLowerCase();
|
|
22961
|
-
filtered = filtered.filter((
|
|
23096
|
+
filtered = filtered.filter((t) => typeHaystack(t).includes(q));
|
|
22962
23097
|
}
|
|
22963
23098
|
return filtered;
|
|
22964
23099
|
}
|
|
22965
|
-
|
|
22966
|
-
|
|
23100
|
+
function rankByOrder(members2) {
|
|
23101
|
+
return Object.fromEntries(members2.map((member, index) => [member, index]));
|
|
23102
|
+
}
|
|
23103
|
+
var SEVERITY_RANK = rankByOrder(Severity.options);
|
|
23104
|
+
function severityRank(severity) {
|
|
23105
|
+
return lookupOwn(SEVERITY_RANK, severity);
|
|
23106
|
+
}
|
|
22967
23107
|
function compareFindingGroupOrder(a, b) {
|
|
22968
|
-
const rankA =
|
|
22969
|
-
const rankB =
|
|
23108
|
+
const rankA = severityRank(a.severity) ?? -1;
|
|
23109
|
+
const rankB = severityRank(b.severity) ?? -1;
|
|
22970
23110
|
const severityDiff = rankA - rankB;
|
|
22971
23111
|
if (severityDiff !== 0) return severityDiff;
|
|
22972
23112
|
const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
|
|
22973
23113
|
if (recencyDiff !== 0) return recencyDiff;
|
|
22974
23114
|
return a.id.localeCompare(b.id);
|
|
22975
23115
|
}
|
|
22976
|
-
function
|
|
22977
|
-
return [...
|
|
23116
|
+
function sortFindingTypes(types) {
|
|
23117
|
+
return [...types].sort(compareFindingGroupOrder);
|
|
22978
23118
|
}
|
|
22979
|
-
function computeFindingFacets(
|
|
22980
|
-
const forSeverity = applyFindingFilters(
|
|
23119
|
+
function computeFindingFacets(allTypes, opts) {
|
|
23120
|
+
const forSeverity = applyFindingFilters(allTypes, {
|
|
22981
23121
|
providers: opts.providers,
|
|
22982
23122
|
actions: opts.actions,
|
|
22983
23123
|
statuses: opts.statuses,
|
|
@@ -22988,7 +23128,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22988
23128
|
for (const g of forSeverity) {
|
|
22989
23129
|
severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
|
|
22990
23130
|
}
|
|
22991
|
-
const forProvider = applyFindingFilters(
|
|
23131
|
+
const forProvider = applyFindingFilters(allTypes, {
|
|
22992
23132
|
actions: opts.actions,
|
|
22993
23133
|
statuses: opts.statuses,
|
|
22994
23134
|
q: opts.q,
|
|
@@ -22999,7 +23139,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
22999
23139
|
for (const g of forProvider) {
|
|
23000
23140
|
for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
|
|
23001
23141
|
}
|
|
23002
|
-
const forAction = applyFindingFilters(
|
|
23142
|
+
const forAction = applyFindingFilters(allTypes, {
|
|
23003
23143
|
providers: opts.providers,
|
|
23004
23144
|
statuses: opts.statuses,
|
|
23005
23145
|
q: opts.q,
|
|
@@ -23008,9 +23148,9 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23008
23148
|
});
|
|
23009
23149
|
const actionMap = /* @__PURE__ */ new Map();
|
|
23010
23150
|
for (const g of forAction) {
|
|
23011
|
-
for (const a of
|
|
23151
|
+
for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
|
|
23012
23152
|
}
|
|
23013
|
-
const forSubtype = applyFindingFilters(
|
|
23153
|
+
const forSubtype = applyFindingFilters(allTypes, {
|
|
23014
23154
|
providers: opts.providers,
|
|
23015
23155
|
actions: opts.actions,
|
|
23016
23156
|
statuses: opts.statuses,
|
|
@@ -23019,7 +23159,7 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23019
23159
|
});
|
|
23020
23160
|
const subtypeMap = /* @__PURE__ */ new Map();
|
|
23021
23161
|
for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
|
|
23022
|
-
const forStatus = applyFindingFilters(
|
|
23162
|
+
const forStatus = applyFindingFilters(allTypes, {
|
|
23023
23163
|
providers: opts.providers,
|
|
23024
23164
|
actions: opts.actions,
|
|
23025
23165
|
q: opts.q,
|
|
@@ -23041,6 +23181,20 @@ function computeFindingFacets(allGroups, opts) {
|
|
|
23041
23181
|
}
|
|
23042
23182
|
|
|
23043
23183
|
// ../../packages/schema/src/zod/findings-flat-build.ts
|
|
23184
|
+
function compareCodePoints(a, b) {
|
|
23185
|
+
const aIter = a[Symbol.iterator]();
|
|
23186
|
+
const bIter = b[Symbol.iterator]();
|
|
23187
|
+
for (; ; ) {
|
|
23188
|
+
const aNext = aIter.next();
|
|
23189
|
+
const bNext = bIter.next();
|
|
23190
|
+
if (aNext.done && bNext.done) return 0;
|
|
23191
|
+
if (aNext.done) return -1;
|
|
23192
|
+
if (bNext.done) return 1;
|
|
23193
|
+
const aPoint = aNext.value.codePointAt(0) ?? 0;
|
|
23194
|
+
const bPoint = bNext.value.codePointAt(0) ?? 0;
|
|
23195
|
+
if (aPoint !== bPoint) return aPoint - bPoint;
|
|
23196
|
+
}
|
|
23197
|
+
}
|
|
23044
23198
|
function rowHaystack(row) {
|
|
23045
23199
|
return [
|
|
23046
23200
|
row.ruleId,
|
|
@@ -23065,12 +23219,24 @@ function matchesDimension(row, opts, dimension) {
|
|
|
23065
23219
|
return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
|
|
23066
23220
|
case "statuses":
|
|
23067
23221
|
return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
|
|
23222
|
+
case "deliveries":
|
|
23223
|
+
return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
|
|
23068
23224
|
case "tools":
|
|
23069
23225
|
return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
|
|
23226
|
+
// An EMPTY value is a real filter here, not an absent one. The location
|
|
23227
|
+
// list buckets a finding whose event recorded no repo — or no file — under
|
|
23228
|
+
// the empty string, and selecting that bucket has to narrow the panel to
|
|
23229
|
+
// exactly it. Only `undefined` means "no filter"; a caller that wants every
|
|
23230
|
+
// row omits the key, which every call site already does.
|
|
23231
|
+
//
|
|
23232
|
+
// Reading '' as unset is what this replaced, and it failed in the one place
|
|
23233
|
+
// it mattered: the no-repo/no-file bucket is often the largest in a real
|
|
23234
|
+
// store, and its panel dropped both predicates and returned the WHOLE scope
|
|
23235
|
+
// — a row reading 3 findings beside a panel listing every finding there is.
|
|
23070
23236
|
case "repo":
|
|
23071
|
-
return opts.repo === void 0 ||
|
|
23237
|
+
return opts.repo === void 0 || row.repo === opts.repo;
|
|
23072
23238
|
case "file":
|
|
23073
|
-
return opts.file === void 0 ||
|
|
23239
|
+
return opts.file === void 0 || row.file === opts.file;
|
|
23074
23240
|
case "q":
|
|
23075
23241
|
return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
|
|
23076
23242
|
}
|
|
@@ -23081,6 +23247,7 @@ var DIMENSIONS = [
|
|
|
23081
23247
|
"providers",
|
|
23082
23248
|
"actions",
|
|
23083
23249
|
"statuses",
|
|
23250
|
+
"deliveries",
|
|
23084
23251
|
"tools",
|
|
23085
23252
|
"repo",
|
|
23086
23253
|
"file",
|
|
@@ -23094,10 +23261,19 @@ function matchesInstanceFilters(row, opts, except) {
|
|
|
23094
23261
|
return true;
|
|
23095
23262
|
}
|
|
23096
23263
|
function toItems(counts) {
|
|
23097
|
-
return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
|
|
23264
|
+
return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
|
|
23265
|
+
(a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
|
|
23266
|
+
// NFD spelling of the same text) as equal, so a count tie between
|
|
23267
|
+
// them would otherwise be ordered by whichever the Map iteration
|
|
23268
|
+
// produced. compareCodePoints breaks that tie deterministically, which
|
|
23269
|
+
// makes this a TOTAL order — not one that agrees with SQL collation,
|
|
23270
|
+
// which it need not: foldFacetTuples runs this same sort over grouped
|
|
23271
|
+
// tuples, so both paths order facets identically by construction.
|
|
23272
|
+
compareCodePoints(a.value, b.value)
|
|
23273
|
+
);
|
|
23098
23274
|
}
|
|
23099
|
-
function bump(counts, value) {
|
|
23100
|
-
counts.set(value, (counts.get(value) ?? 0) +
|
|
23275
|
+
function bump(counts, value, by = 1) {
|
|
23276
|
+
counts.set(value, (counts.get(value) ?? 0) + by);
|
|
23101
23277
|
}
|
|
23102
23278
|
function createInstanceFacetAccumulator(opts) {
|
|
23103
23279
|
const severity = /* @__PURE__ */ new Map();
|
|
@@ -23106,6 +23282,7 @@ function createInstanceFacetAccumulator(opts) {
|
|
|
23106
23282
|
const action = /* @__PURE__ */ new Map();
|
|
23107
23283
|
const status = /* @__PURE__ */ new Map();
|
|
23108
23284
|
const tool = /* @__PURE__ */ new Map();
|
|
23285
|
+
const deployment = /* @__PURE__ */ new Map();
|
|
23109
23286
|
return {
|
|
23110
23287
|
add(row) {
|
|
23111
23288
|
if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
|
|
@@ -23120,6 +23297,9 @@ function createInstanceFacetAccumulator(opts) {
|
|
|
23120
23297
|
if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
|
|
23121
23298
|
bump(tool, row.toolName);
|
|
23122
23299
|
}
|
|
23300
|
+
if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
|
|
23301
|
+
bump(deployment, row.delivery.state);
|
|
23302
|
+
}
|
|
23123
23303
|
},
|
|
23124
23304
|
facets: () => ({
|
|
23125
23305
|
severity: toItems(severity),
|
|
@@ -23127,7 +23307,8 @@ function createInstanceFacetAccumulator(opts) {
|
|
|
23127
23307
|
provider: toItems(provider),
|
|
23128
23308
|
action: toItems(action),
|
|
23129
23309
|
status: toItems(status),
|
|
23130
|
-
tool: toItems(tool)
|
|
23310
|
+
tool: toItems(tool),
|
|
23311
|
+
deployment: toItems(deployment)
|
|
23131
23312
|
})
|
|
23132
23313
|
};
|
|
23133
23314
|
}
|
|
@@ -23141,6 +23322,7 @@ function toInstanceDetail(row) {
|
|
|
23141
23322
|
...row.toolName === void 0 ? {} : { toolName: row.toolName },
|
|
23142
23323
|
eventId: row.eventId,
|
|
23143
23324
|
...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
|
|
23325
|
+
...row.delivery === void 0 ? {} : { delivery: row.delivery },
|
|
23144
23326
|
...row.user === void 0 ? {} : { user: row.user },
|
|
23145
23327
|
action: toApiAction(row.actionTaken),
|
|
23146
23328
|
detectedAt: row.occurredAt,
|
|
@@ -23155,12 +23337,6 @@ function toInstanceDetail(row) {
|
|
|
23155
23337
|
policy: { id: `category:${category}`, name: category }
|
|
23156
23338
|
};
|
|
23157
23339
|
}
|
|
23158
|
-
var SEVERITY_ORDER2 = {
|
|
23159
|
-
critical: 0,
|
|
23160
|
-
high: 1,
|
|
23161
|
-
medium: 2,
|
|
23162
|
-
low: 3
|
|
23163
|
-
};
|
|
23164
23340
|
function newLocationAccumulator() {
|
|
23165
23341
|
return {
|
|
23166
23342
|
instanceCount: 0,
|
|
@@ -23175,7 +23351,7 @@ function newLocationAccumulator() {
|
|
|
23175
23351
|
}
|
|
23176
23352
|
function addToLocation(acc, row) {
|
|
23177
23353
|
acc.instanceCount += 1;
|
|
23178
|
-
const rank =
|
|
23354
|
+
const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
23179
23355
|
if (rank < acc.maxSeverityRank) {
|
|
23180
23356
|
acc.maxSeverityRank = rank;
|
|
23181
23357
|
acc.maxSeverity = row.severity;
|
|
@@ -23184,6 +23360,23 @@ function addToLocation(acc, row) {
|
|
|
23184
23360
|
acc.statuses.push(row.status);
|
|
23185
23361
|
acc.ruleIds.add(row.ruleId);
|
|
23186
23362
|
}
|
|
23363
|
+
function compareLocationOrder(a, b) {
|
|
23364
|
+
const rankA = severityRank(a.maxSeverity) ?? -1;
|
|
23365
|
+
const rankB = severityRank(b.maxSeverity) ?? -1;
|
|
23366
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
23367
|
+
if (a.latestDetectedAt !== b.latestDetectedAt) {
|
|
23368
|
+
return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
|
|
23369
|
+
}
|
|
23370
|
+
const repoDiff = compareCodePoints(a.repo, b.repo);
|
|
23371
|
+
if (repoDiff !== 0) return repoDiff;
|
|
23372
|
+
return compareCodePoints(a.file, b.file);
|
|
23373
|
+
}
|
|
23374
|
+
function encodeLocationId(repo, file2) {
|
|
23375
|
+
return `${encodePart(repo)}/${encodePart(file2)}`;
|
|
23376
|
+
}
|
|
23377
|
+
function encodePart(value) {
|
|
23378
|
+
return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
|
|
23379
|
+
}
|
|
23187
23380
|
|
|
23188
23381
|
// ../../packages/schema/src/zod/installed-pack.ts
|
|
23189
23382
|
var InstalledPack = external_exports.object({
|
|
@@ -23251,6 +23444,11 @@ var Policy = external_exports.object({
|
|
|
23251
23444
|
// test `prohibitedModels` passes and `reversibleRuleIds` fails.
|
|
23252
23445
|
provenance: PolicyProvenance.optional()
|
|
23253
23446
|
}).meta({ id: "Policy" });
|
|
23447
|
+
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23448
|
+
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23449
|
+
var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
|
|
23450
|
+
id: "RedactFallback"
|
|
23451
|
+
});
|
|
23254
23452
|
var PolicyBundle = external_exports.object({
|
|
23255
23453
|
version: external_exports.string(),
|
|
23256
23454
|
policies: external_exports.array(Policy),
|
|
@@ -23298,6 +23496,16 @@ var PolicyBundle = external_exports.object({
|
|
|
23298
23496
|
// control plane), so no name resolution stands between the decision and the
|
|
23299
23497
|
// comparison.
|
|
23300
23498
|
prohibitedModels: external_exports.array(external_exports.string()).optional(),
|
|
23499
|
+
// What a resolved `redact` becomes on a field the host cannot rewrite, as
|
|
23500
|
+
// the ORGANIZATION would have it. Merged raise-only against the device's own
|
|
23501
|
+
// `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
|
|
23502
|
+
// a control plane can tighten a machine and never loosen one — the same
|
|
23503
|
+
// direction `mergeRaiseOnly` enforces for policies.
|
|
23504
|
+
//
|
|
23505
|
+
// Optional so an older backend, and an older on-disk cache, still parses;
|
|
23506
|
+
// absent leaves the device's own setting in force, which is the behaviour
|
|
23507
|
+
// that predates the field and the safe direction to default.
|
|
23508
|
+
redactFallback: RedactFallback.optional(),
|
|
23301
23509
|
customKeywords: external_exports.array(external_exports.string()),
|
|
23302
23510
|
fetchedAt: external_exports.iso.datetime()
|
|
23303
23511
|
}).meta({ id: "PolicyBundle" });
|
|
@@ -23327,11 +23535,6 @@ function severityFloorPolicy(category) {
|
|
|
23327
23535
|
const peak = CATEGORY_PEAK_SEVERITY[category];
|
|
23328
23536
|
return peak === "critical" || peak === "high" ? "warn" : "monitor";
|
|
23329
23537
|
}
|
|
23330
|
-
var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
|
|
23331
|
-
var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
|
|
23332
|
-
var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
|
|
23333
|
-
id: "RedactFallback"
|
|
23334
|
-
});
|
|
23335
23538
|
var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
|
|
23336
23539
|
var BUILTIN_POLICY_SPECS = {
|
|
23337
23540
|
monitor: {
|
|
@@ -23635,7 +23838,7 @@ function isVaultConsentValid(consent) {
|
|
|
23635
23838
|
}
|
|
23636
23839
|
|
|
23637
23840
|
// ../../packages/schema/src/zod/local.ts
|
|
23638
|
-
var WORKSPACE_SETTINGS_SPEC_VERSION =
|
|
23841
|
+
var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
|
|
23639
23842
|
var MODEL_JUDGE_PAYLOAD_VERSION = 1;
|
|
23640
23843
|
var RunMode = external_exports.enum(["standalone", "attached"]);
|
|
23641
23844
|
var ControlPlaneConnection = external_exports.object({
|
|
@@ -23655,6 +23858,15 @@ var HistorySyncConsent = external_exports.object({
|
|
|
23655
23858
|
payloadVersion: external_exports.number().int().positive(),
|
|
23656
23859
|
endpoint: external_exports.string()
|
|
23657
23860
|
});
|
|
23861
|
+
var BODY_RETENTION_DEFAULT_DAYS = 30;
|
|
23862
|
+
var BodyRetention = external_exports.object({
|
|
23863
|
+
enabled: external_exports.boolean().default(false),
|
|
23864
|
+
// Never 0, and the ceiling is a fat-finger guard rather than a policy
|
|
23865
|
+
// limit — `enabled` is the real gate. A low value cannot reach a row the
|
|
23866
|
+
// sync ledger still owes: the sweep's age filter only ever NARROWS a
|
|
23867
|
+
// candidate set that is already bounded by "delivered, or never owed".
|
|
23868
|
+
retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
|
|
23869
|
+
}).meta({ id: "BodyRetention" });
|
|
23658
23870
|
var WorkspaceSettings = external_exports.object({
|
|
23659
23871
|
specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
|
|
23660
23872
|
runMode: RunMode.default("standalone"),
|
|
@@ -23698,12 +23910,18 @@ var WorkspaceSettings = external_exports.object({
|
|
|
23698
23910
|
// covers the current payload and must be re-granted.
|
|
23699
23911
|
modelJudgeConsent: ModelJudgeConsent.optional(),
|
|
23700
23912
|
// Records that the user consented to the DEFERRED send — the outbox — along
|
|
23701
|
-
// with the payload shape and the endpoint they agreed to. Since payload
|
|
23702
|
-
// that covers
|
|
23703
|
-
// carry prompt/reply text in `content
|
|
23704
|
-
// Absent until granted, and a grant for a different endpoint
|
|
23705
|
-
// payload no longer counts.
|
|
23706
|
-
historySyncConsent: HistorySyncConsent.optional()
|
|
23913
|
+
// with the payload shape and the endpoint they agreed to. Since payload v3
|
|
23914
|
+
// that covers the pre-attach backlog AND undelivered captures alike, and both
|
|
23915
|
+
// carry prompt/reply/tool-output text in `content`; the key name predates
|
|
23916
|
+
// both widenings. Absent until granted, and a grant for a different endpoint
|
|
23917
|
+
// or an older payload no longer counts.
|
|
23918
|
+
historySyncConsent: HistorySyncConsent.optional(),
|
|
23919
|
+
// Local body expiry (see BodyRetention). Off until switched on; expiring a
|
|
23920
|
+
// body never removes the row or its findings.
|
|
23921
|
+
bodyRetention: BodyRetention.default({
|
|
23922
|
+
enabled: false,
|
|
23923
|
+
retainDays: BODY_RETENTION_DEFAULT_DAYS
|
|
23924
|
+
})
|
|
23707
23925
|
});
|
|
23708
23926
|
function defaultWorkspaceSettings() {
|
|
23709
23927
|
return WorkspaceSettings.parse({});
|
|
@@ -23798,12 +24016,15 @@ function toCaptureAttributes(event) {
|
|
|
23798
24016
|
...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
|
|
23799
24017
|
...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
|
|
23800
24018
|
...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
|
|
24019
|
+
...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
|
|
23801
24020
|
// `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
|
|
23802
24021
|
// has ever populated either), but every legacy metadata key still rides
|
|
23803
24022
|
// the bag rather than being silently dropped — CaptureAttributes'
|
|
23804
24023
|
// `.catchall(z.unknown())` carries the long tail.
|
|
23805
24024
|
...metadata?.model !== void 0 ? { model: metadata.model } : {},
|
|
23806
|
-
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
|
|
24025
|
+
...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
|
|
24026
|
+
...metadata?.messageId !== void 0 ? { message_id: metadata.messageId } : {},
|
|
24027
|
+
...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
|
|
23807
24028
|
};
|
|
23808
24029
|
}
|
|
23809
24030
|
function captureDefinitionVersion(finding) {
|
|
@@ -23831,10 +24052,22 @@ var ManagedSettingKey = external_exports.enum([
|
|
|
23831
24052
|
"vaultInlineReveal",
|
|
23832
24053
|
"modelJudgeConsent",
|
|
23833
24054
|
"dataSharesInPlace",
|
|
23834
|
-
"redactFallback"
|
|
24055
|
+
"redactFallback",
|
|
24056
|
+
// Pins the toggle and the day count together — see BodyRetention on why the
|
|
24057
|
+
// two are one unit. An administrator mandating a window wants the count
|
|
24058
|
+
// enforced with it, not one a user can widen while the toggle stays on.
|
|
24059
|
+
"bodyRetention"
|
|
23835
24060
|
]).meta({ id: "ManagedSettingKey" });
|
|
24061
|
+
function isManagedSettingKey(value) {
|
|
24062
|
+
return ManagedSettingKey.safeParse(value).success;
|
|
24063
|
+
}
|
|
23836
24064
|
var ManagedSettingsValues = external_exports.object({
|
|
23837
24065
|
runMode: external_exports.enum(["standalone", "attached"]).optional(),
|
|
24066
|
+
// `controlPlane` and `bodyRetention` are the two nested values, and both are
|
|
24067
|
+
// plain, non-strict objects: a key under either that this build does not know
|
|
24068
|
+
// is stripped and nothing reports it. The unknown-value split in
|
|
24069
|
+
// ManagedSettings below classifies top-level names only, so it stops at
|
|
24070
|
+
// these boundaries.
|
|
23838
24071
|
controlPlane: external_exports.object({
|
|
23839
24072
|
endpoint: external_exports.string().min(1),
|
|
23840
24073
|
label: external_exports.string().min(1).optional()
|
|
@@ -23845,7 +24078,8 @@ var ManagedSettingsValues = external_exports.object({
|
|
|
23845
24078
|
vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
|
|
23846
24079
|
modelJudgeConsent: external_exports.boolean().optional(),
|
|
23847
24080
|
dataSharesInPlace: external_exports.boolean().optional(),
|
|
23848
|
-
redactFallback: RedactFallback.optional()
|
|
24081
|
+
redactFallback: RedactFallback.optional(),
|
|
24082
|
+
bodyRetention: BodyRetention.optional()
|
|
23849
24083
|
}).meta({ id: "ManagedSettingsValues" });
|
|
23850
24084
|
var ManagedSettings = external_exports.object({
|
|
23851
24085
|
specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
|
|
@@ -23853,11 +24087,59 @@ var ManagedSettings = external_exports.object({
|
|
|
23853
24087
|
// decision from a bug. Absent renders as a generic "your organization".
|
|
23854
24088
|
organization: external_exports.string().min(1).optional(),
|
|
23855
24089
|
// What the administrator pinned.
|
|
23856
|
-
|
|
24090
|
+
//
|
|
24091
|
+
// Parsed as a RECORD rather than as the nested schema, and split below for
|
|
24092
|
+
// the same reason `lockedFields` is parsed as names: a plain `z.object`
|
|
24093
|
+
// drops an unrecognised key and succeeds, so a pin this build does not know
|
|
24094
|
+
// vanished and nothing anywhere said so. A pin with no lock is a supported
|
|
24095
|
+
// shape — it is a DEFAULT the user may still change — so that silence hit
|
|
24096
|
+
// exactly the file an administrator is most likely to write while a fleet
|
|
24097
|
+
// is mid-upgrade.
|
|
24098
|
+
//
|
|
24099
|
+
// Splitting here rather than calling `.strict()`: strict would REFUSE the
|
|
24100
|
+
// file, which is the outcome the lock half already rejected — an older
|
|
24101
|
+
// build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
|
|
24102
|
+
// value still fails, because the nested schema is re-run over the known
|
|
24103
|
+
// subset and its issues are re-raised on this parse.
|
|
24104
|
+
values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
|
|
23857
24105
|
// Which of those the user may not change. A key here with no matching value
|
|
23858
24106
|
// freezes whatever the user last chose; a value with no lock is a DEFAULT
|
|
23859
24107
|
// the user may still override. The two are separable on purpose.
|
|
23860
|
-
|
|
24108
|
+
//
|
|
24109
|
+
// Parsed as NAMES rather than as the enum, and split below: a name this
|
|
24110
|
+
// build does not know is dropped from the locked set and reported, never a
|
|
24111
|
+
// reason to refuse the file. The same shape reaches an older build whenever
|
|
24112
|
+
// an administrator locks a key a newer build added, and refusing it there
|
|
24113
|
+
// ran that build entirely unmanaged — every pin and lock gone — on exactly
|
|
24114
|
+
// the fleets most likely to carry a version skew. A name outside the enum
|
|
24115
|
+
// is still never HONOURED: the lockable set stays explicit above.
|
|
24116
|
+
lockedFields: external_exports.array(external_exports.string()).default([])
|
|
24117
|
+
}).transform(({ lockedFields, values, ...rest }, ctx) => {
|
|
24118
|
+
const known = [];
|
|
24119
|
+
const unknown2 = [];
|
|
24120
|
+
for (const name of lockedFields) {
|
|
24121
|
+
if (isManagedSettingKey(name)) known.push(name);
|
|
24122
|
+
else unknown2.push(name);
|
|
24123
|
+
}
|
|
24124
|
+
const knownValues = /* @__PURE__ */ Object.create(null);
|
|
24125
|
+
const unknownValues = [];
|
|
24126
|
+
for (const [name, value] of Object.entries(values)) {
|
|
24127
|
+
if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
|
|
24128
|
+
else unknownValues.push(name);
|
|
24129
|
+
}
|
|
24130
|
+
const pinned = ManagedSettingsValues.safeParse(knownValues);
|
|
24131
|
+
if (!pinned.success) {
|
|
24132
|
+
for (const issue2 of pinned.error.issues)
|
|
24133
|
+
ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
|
|
24134
|
+
return external_exports.NEVER;
|
|
24135
|
+
}
|
|
24136
|
+
return {
|
|
24137
|
+
...rest,
|
|
24138
|
+
values: pinned.data,
|
|
24139
|
+
lockedFields: known,
|
|
24140
|
+
...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
|
|
24141
|
+
...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
|
|
24142
|
+
};
|
|
23861
24143
|
}).meta({ id: "ManagedSettings" });
|
|
23862
24144
|
|
|
23863
24145
|
// ../../packages/schema/src/zod/project-files.ts
|
|
@@ -23981,7 +24263,11 @@ var FindingsTimeseriesPoint = external_exports.object({
|
|
|
23981
24263
|
timestamp: external_exports.iso.date(),
|
|
23982
24264
|
critical: external_exports.number().int().nonnegative(),
|
|
23983
24265
|
high: external_exports.number().int().nonnegative(),
|
|
23984
|
-
medium: external_exports.number().int().nonnegative()
|
|
24266
|
+
medium: external_exports.number().int().nonnegative(),
|
|
24267
|
+
// Optional and additive, so a producer written against the earlier
|
|
24268
|
+
// three-series contract keeps validating. A consumer plotting it resolves the
|
|
24269
|
+
// absent case itself — the chart point requires a number.
|
|
24270
|
+
low: external_exports.number().int().nonnegative().optional()
|
|
23985
24271
|
}).meta({ id: "FindingsTimeseriesPoint" });
|
|
23986
24272
|
var FindingsTimeseriesResponse = external_exports.object({
|
|
23987
24273
|
range: TimeRange,
|
|
@@ -24007,6 +24293,10 @@ var ResolvedFeedItem = external_exports.object({
|
|
|
24007
24293
|
findingKey: external_exports.string(),
|
|
24008
24294
|
ruleId: external_exports.string(),
|
|
24009
24295
|
severity: Severity,
|
|
24296
|
+
// Repository slug, and the file path RELATIVE to it. The pair is what
|
|
24297
|
+
// identifies the file: a bare path matches the same name in every repo.
|
|
24298
|
+
// Optional and additive; empty when the event carried no repo.
|
|
24299
|
+
repo: external_exports.string().optional(),
|
|
24010
24300
|
path: external_exports.string(),
|
|
24011
24301
|
// ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
|
|
24012
24302
|
// findings domain). The reader `.toISOString()`s the DB epoch-ms values.
|
|
@@ -24112,7 +24402,23 @@ var SaveSettingsInput = external_exports.object({
|
|
|
24112
24402
|
modelJudgeConsent: ModelJudgeConsentChoice,
|
|
24113
24403
|
historySyncConsent: HistorySyncConsentChoice,
|
|
24114
24404
|
vaultConsent: external_exports.string(),
|
|
24115
|
-
vaultInlineReveal: external_exports.string()
|
|
24405
|
+
vaultInlineReveal: external_exports.string(),
|
|
24406
|
+
// Widened to `string` like its neighbours rather than typed as
|
|
24407
|
+
// `RedactFallback`, on this module's own layering rule: shape here, VALUE at
|
|
24408
|
+
// the call site, so the domain check receives the type it was written for.
|
|
24409
|
+
//
|
|
24410
|
+
// NOT because a narrower schema would reject differently. `parseActionInput`
|
|
24411
|
+
// is a `safeParse` wrapper and throws for no field schema, so either spelling
|
|
24412
|
+
// reaches a recoverable `{ ok: false }` and there is no rejected promise to
|
|
24413
|
+
// trade against. The real cost runs the other way and is the part worth
|
|
24414
|
+
// knowing: a value this schema admits and the domain enum then rejects lands
|
|
24415
|
+
// on the action's shared refusal, which names NO field, where a shape
|
|
24416
|
+
// rejection reaches `malformedInput` and names the schema key.
|
|
24417
|
+
redactFallback: external_exports.string(),
|
|
24418
|
+
// Shape only, the way the enum fields above are strings only: the RANGE is
|
|
24419
|
+
// `BodyRetention`'s and the action checks it there, so there is one place
|
|
24420
|
+
// that decides what a legal horizon is rather than two that can drift.
|
|
24421
|
+
bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
|
|
24116
24422
|
});
|
|
24117
24423
|
var AttachInput = external_exports.object({
|
|
24118
24424
|
endpoint: external_exports.string(),
|
|
@@ -24284,6 +24590,52 @@ function reviewSeverityRank(reasons) {
|
|
|
24284
24590
|
return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
|
|
24285
24591
|
}
|
|
24286
24592
|
|
|
24593
|
+
// ../../packages/schema/src/zod/web-capture.ts
|
|
24594
|
+
var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
|
|
24595
|
+
var WebUsage = external_exports.object({
|
|
24596
|
+
inputTokens: external_exports.number().int().nonnegative().optional(),
|
|
24597
|
+
outputTokens: external_exports.number().int().nonnegative().optional(),
|
|
24598
|
+
cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
|
|
24599
|
+
cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
|
|
24600
|
+
});
|
|
24601
|
+
var WebToolCall = external_exports.object({
|
|
24602
|
+
toolUseId: external_exports.string().min(1),
|
|
24603
|
+
toolName: external_exports.string().min(1),
|
|
24604
|
+
target: external_exports.string().optional(),
|
|
24605
|
+
isError: external_exports.boolean().optional(),
|
|
24606
|
+
inputSize: external_exports.number().int().nonnegative().optional(),
|
|
24607
|
+
outputSize: external_exports.number().int().nonnegative().optional()
|
|
24608
|
+
});
|
|
24609
|
+
var WebExchange = external_exports.object({
|
|
24610
|
+
messageId: external_exports.string().min(1),
|
|
24611
|
+
startedAt: external_exports.iso.datetime(),
|
|
24612
|
+
model: external_exports.string().optional(),
|
|
24613
|
+
usage: WebUsage.optional(),
|
|
24614
|
+
usageSource: WebUsageSource,
|
|
24615
|
+
stopReason: external_exports.string().optional(),
|
|
24616
|
+
conversationId: external_exports.string().optional(),
|
|
24617
|
+
turnIndex: external_exports.number().int().nonnegative().optional(),
|
|
24618
|
+
toolCalls: external_exports.array(WebToolCall).default([]),
|
|
24619
|
+
// Absent when the adapter recovered no text. Capped by the caller at
|
|
24620
|
+
// RESPONSE_TEXT_MAX_BYTES; `truncated` records that the cap was reached, so a
|
|
24621
|
+
// short capture is never mistaken for a short reply.
|
|
24622
|
+
responseText: external_exports.string().optional(),
|
|
24623
|
+
truncated: external_exports.boolean().default(false)
|
|
24624
|
+
});
|
|
24625
|
+
var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
|
|
24626
|
+
var WebCaptureStatus = external_exports.object({
|
|
24627
|
+
patched: external_exports.boolean(),
|
|
24628
|
+
live: external_exports.boolean(),
|
|
24629
|
+
blind: external_exports.boolean(),
|
|
24630
|
+
sendsSeenDom: external_exports.number().int().nonnegative(),
|
|
24631
|
+
exchangesSeenNet: external_exports.number().int().nonnegative(),
|
|
24632
|
+
parseFailures: external_exports.number().int().nonnegative(),
|
|
24633
|
+
unparsedBodies: external_exports.number().int().nonnegative(),
|
|
24634
|
+
// The adapter-declared JSON key paths that were absent from a real payload —
|
|
24635
|
+
// the earliest signal that a site's contract moved.
|
|
24636
|
+
shapeMisses: external_exports.array(external_exports.string()).default([])
|
|
24637
|
+
});
|
|
24638
|
+
|
|
24287
24639
|
// ../../packages/persistence/src/paths.ts
|
|
24288
24640
|
import {
|
|
24289
24641
|
chmodSync,
|
|
@@ -24553,6 +24905,22 @@ function discardStore(file2, backup) {
|
|
|
24553
24905
|
}
|
|
24554
24906
|
}
|
|
24555
24907
|
|
|
24908
|
+
// ../../packages/persistence/src/internal/sql-functions.ts
|
|
24909
|
+
var utf8 = new TextDecoder();
|
|
24910
|
+
function akaLower(value) {
|
|
24911
|
+
if (value === null) return null;
|
|
24912
|
+
if (typeof value === "string") return value.toLowerCase();
|
|
24913
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
|
|
24914
|
+
return utf8.decode(value).toLowerCase();
|
|
24915
|
+
}
|
|
24916
|
+
function registerSqlFunctions(db) {
|
|
24917
|
+
db.function(
|
|
24918
|
+
"aka_lower",
|
|
24919
|
+
{ deterministic: true, directOnly: true, useBigIntArguments: true },
|
|
24920
|
+
akaLower
|
|
24921
|
+
);
|
|
24922
|
+
}
|
|
24923
|
+
|
|
24556
24924
|
// ../../packages/persistence/src/internal/sql-text.ts
|
|
24557
24925
|
function escapeLikePattern(s) {
|
|
24558
24926
|
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
@@ -24637,6 +25005,11 @@ function schemaObjectExists(db, kind, name) {
|
|
|
24637
25005
|
function indexExists(db, name) {
|
|
24638
25006
|
return schemaObjectExists(db, "index", name);
|
|
24639
25007
|
}
|
|
25008
|
+
function indexColumns(db, name) {
|
|
25009
|
+
if (!indexExists(db, name)) return [];
|
|
25010
|
+
const columns = db.prepare(`PRAGMA index_info(${name})`).all();
|
|
25011
|
+
return columns.map((c) => c.name).filter((c) => c !== null);
|
|
25012
|
+
}
|
|
24640
25013
|
function columnNames(db, table, opts) {
|
|
24641
25014
|
const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
|
|
24642
25015
|
const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
|
|
@@ -24698,6 +25071,647 @@ function mapRowsTolerant(rows, map2) {
|
|
|
24698
25071
|
return out;
|
|
24699
25072
|
}
|
|
24700
25073
|
|
|
25074
|
+
// ../../packages/persistence/src/internal/outbox-lane.ts
|
|
25075
|
+
var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
|
|
25076
|
+
var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
25077
|
+
|
|
25078
|
+
// ../../packages/persistence/src/sync-failure.ts
|
|
25079
|
+
var SYNC_FAILURE_REASONS = SyncFailureReason.options;
|
|
25080
|
+
function syncFailureRejectCondition(column = "sync_failure") {
|
|
25081
|
+
const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
|
|
25082
|
+
return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
|
|
25083
|
+
}
|
|
25084
|
+
|
|
25085
|
+
// ../../packages/persistence/src/repositories/history-sync.ts
|
|
25086
|
+
var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
|
|
25087
|
+
var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
25088
|
+
var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
|
|
25089
|
+
var COUNTED_EVENT_TYPES = [
|
|
25090
|
+
...STRUCTURAL_EVENT_TYPES,
|
|
25091
|
+
...OUTBOX_CAPTURE_EVENT_TYPES
|
|
25092
|
+
];
|
|
25093
|
+
var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
25094
|
+
var PARTITION_BUCKETS = `
|
|
25095
|
+
SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
|
|
25096
|
+
SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
|
|
25097
|
+
SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
|
|
25098
|
+
SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
|
|
25099
|
+
AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
|
|
25100
|
+
SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
|
|
25101
|
+
AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
|
|
25102
|
+
-- Spelled as what it INCLUDES rather than what it excludes, so a reason
|
|
25103
|
+
-- added later lands in no bucket and fails the sum assertion, instead
|
|
25104
|
+
-- of silently joining this one.
|
|
25105
|
+
SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
|
|
25106
|
+
AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
|
|
25107
|
+
THEN 1 ELSE 0 END) AS failed,
|
|
25108
|
+
COUNT(*) AS total`;
|
|
25109
|
+
var COUNTED_SCOPE = `
|
|
25110
|
+
WHERE event_type IN (${COUNTED_TYPE_LIST})
|
|
25111
|
+
AND (
|
|
25112
|
+
event_type IN (${TYPE_LIST})
|
|
25113
|
+
OR synced_at IS NOT NULL
|
|
25114
|
+
OR outbox_owed = 1
|
|
25115
|
+
)`;
|
|
25116
|
+
var SKIPPED = -1;
|
|
25117
|
+
var ROW_COLUMNS = `id,
|
|
25118
|
+
parent_id AS parentId,
|
|
25119
|
+
root_session_id AS rootSessionId,
|
|
25120
|
+
event_type AS eventType,
|
|
25121
|
+
host_id AS hostId,
|
|
25122
|
+
harness_id AS harnessId,
|
|
25123
|
+
source_project_id AS sourceProjectId,
|
|
25124
|
+
started_at AS startedAt,
|
|
25125
|
+
ended_at AS endedAt,
|
|
25126
|
+
severity,
|
|
25127
|
+
priority,
|
|
25128
|
+
content,
|
|
25129
|
+
content_hash AS contentHash,
|
|
25130
|
+
attributes`;
|
|
25131
|
+
var SqliteHistorySyncRepository = class {
|
|
25132
|
+
constructor(db) {
|
|
25133
|
+
this.db = db;
|
|
25134
|
+
this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
|
|
25135
|
+
this.sessionsStmt = db.prepare(
|
|
25136
|
+
`SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
|
|
25137
|
+
FROM audit_events
|
|
25138
|
+
WHERE synced_at IS NULL
|
|
25139
|
+
AND event_type IN (${TYPE_LIST})
|
|
25140
|
+
AND started_at < :before
|
|
25141
|
+
GROUP BY sessionId
|
|
25142
|
+
ORDER BY earliest
|
|
25143
|
+
LIMIT :limit`
|
|
25144
|
+
);
|
|
25145
|
+
this.rowsStmt = db.prepare(
|
|
25146
|
+
`SELECT ${ROW_COLUMNS}
|
|
25147
|
+
FROM audit_events
|
|
25148
|
+
WHERE synced_at IS NULL
|
|
25149
|
+
AND event_type IN (${TYPE_LIST})
|
|
25150
|
+
AND started_at < :before
|
|
25151
|
+
AND COALESCE(root_session_id, id) = :sessionId
|
|
25152
|
+
ORDER BY (event_type = 'session') DESC, started_at
|
|
25153
|
+
LIMIT :limit`
|
|
25154
|
+
);
|
|
25155
|
+
this.captureRowsStmt = db.prepare(
|
|
25156
|
+
`SELECT ${ROW_COLUMNS}
|
|
25157
|
+
FROM audit_events
|
|
25158
|
+
WHERE synced_at IS NULL
|
|
25159
|
+
AND sync_claimed_at IS NULL
|
|
25160
|
+
AND outbox_owed = 1
|
|
25161
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
25162
|
+
AND started_at < :before
|
|
25163
|
+
ORDER BY started_at
|
|
25164
|
+
LIMIT :limit`
|
|
25165
|
+
);
|
|
25166
|
+
this.markOwedStmt = db.prepare(
|
|
25167
|
+
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
25168
|
+
);
|
|
25169
|
+
this.markCaptureBacklogOwedStmt = db.prepare(
|
|
25170
|
+
`UPDATE audit_events SET outbox_owed = 1
|
|
25171
|
+
WHERE synced_at IS NULL
|
|
25172
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
25173
|
+
AND started_at < :before`
|
|
25174
|
+
);
|
|
25175
|
+
this.stampStmt = db.prepare(
|
|
25176
|
+
`UPDATE audit_events
|
|
25177
|
+
SET synced_at = :at,
|
|
25178
|
+
sync_claimed_at = NULL,
|
|
25179
|
+
sync_failed_at = :failedAt,
|
|
25180
|
+
sync_failure = :failure
|
|
25181
|
+
WHERE id = :id`
|
|
25182
|
+
);
|
|
25183
|
+
this.claimRowStmt = db.prepare(
|
|
25184
|
+
`UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
|
|
25185
|
+
);
|
|
25186
|
+
this.releaseRowStmt = db.prepare(
|
|
25187
|
+
`UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
|
|
25188
|
+
);
|
|
25189
|
+
this.releaseStaleClaimsStmt = db.prepare(
|
|
25190
|
+
`UPDATE audit_events SET sync_claimed_at = NULL
|
|
25191
|
+
WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
|
|
25192
|
+
);
|
|
25193
|
+
this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
|
|
25194
|
+
FROM audit_events${COUNTED_SCOPE}`);
|
|
25195
|
+
this.partitionByKindStmt = db.prepare(
|
|
25196
|
+
`SELECT event_type AS kind,${PARTITION_BUCKETS}
|
|
25197
|
+
FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
|
|
25198
|
+
GROUP BY event_type`
|
|
25199
|
+
);
|
|
25200
|
+
this.countsStmt = db.prepare(
|
|
25201
|
+
`SELECT
|
|
25202
|
+
SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
|
|
25203
|
+
SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
|
|
25204
|
+
SUM(CASE WHEN synced_at = ${String(SKIPPED)}
|
|
25205
|
+
AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
|
|
25206
|
+
THEN 1 ELSE 0 END) AS skipped,
|
|
25207
|
+
SUM(CASE WHEN synced_at = ${String(SKIPPED)}
|
|
25208
|
+
AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
|
|
25209
|
+
SUM(CASE WHEN synced_at = ${String(SKIPPED)}
|
|
25210
|
+
AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
|
|
25211
|
+
FROM audit_events
|
|
25212
|
+
WHERE event_type IN (${TYPE_LIST})`
|
|
25213
|
+
);
|
|
25214
|
+
this.captureSkipCountStmt = db.prepare(
|
|
25215
|
+
// EVERY sentinel capture, whatever the reason — deliberately NOT split the
|
|
25216
|
+
// way the structural totals are. The split exists because a refusal is
|
|
25217
|
+
// terminal only against the deployment that gave it, and the structural
|
|
25218
|
+
// re-arm frees it on a change of deployment. The capture lane has no such
|
|
25219
|
+
// escape: re-arming a capture would offer one deployment's undelivered
|
|
25220
|
+
// prompts, with their text, to a deployment that never saw them, which is
|
|
25221
|
+
// exactly what disownCapturesStmt exists to prevent. So on this lane both
|
|
25222
|
+
// reasons mean the same thing — this row will not be sent — and splitting
|
|
25223
|
+
// them would put refused captures in a bucket nothing reads and nothing
|
|
25224
|
+
// frees.
|
|
25225
|
+
`SELECT COUNT(*) AS skipped
|
|
25226
|
+
FROM audit_events
|
|
25227
|
+
WHERE synced_at = ${String(SKIPPED)}
|
|
25228
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})`
|
|
25229
|
+
);
|
|
25230
|
+
this.fingerprintStmt = db.prepare(
|
|
25231
|
+
`SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
|
|
25232
|
+
FROM history_sync WHERE id = 1`
|
|
25233
|
+
);
|
|
25234
|
+
this.setFingerprintStmt = db.prepare(
|
|
25235
|
+
`UPDATE history_sync
|
|
25236
|
+
SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
|
|
25237
|
+
WHERE id = 1`
|
|
25238
|
+
);
|
|
25239
|
+
this.disownCapturesStmt = db.prepare(
|
|
25240
|
+
`UPDATE audit_events SET outbox_owed = NULL
|
|
25241
|
+
WHERE outbox_owed IS NOT NULL
|
|
25242
|
+
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
25243
|
+
AND started_at < :attachedAt`
|
|
25244
|
+
);
|
|
25245
|
+
this.rearmStmt = db.prepare(
|
|
25246
|
+
`UPDATE audit_events
|
|
25247
|
+
SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
|
|
25248
|
+
WHERE (synced_at > 0
|
|
25249
|
+
OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
|
|
25250
|
+
AND event_type IN (${TYPE_LIST})`
|
|
25251
|
+
);
|
|
25252
|
+
this.claimStmt = db.prepare(
|
|
25253
|
+
`UPDATE history_sync
|
|
25254
|
+
SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
|
|
25255
|
+
WHERE id = 1
|
|
25256
|
+
AND (owner_pid IS NULL
|
|
25257
|
+
OR heartbeat_at IS NULL
|
|
25258
|
+
OR heartbeat_at < :staleBefore
|
|
25259
|
+
OR heartbeat_at > :now)`
|
|
25260
|
+
);
|
|
25261
|
+
this.heartbeatStmt = db.prepare(
|
|
25262
|
+
`UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
|
|
25263
|
+
);
|
|
25264
|
+
this.releaseStmt = db.prepare(
|
|
25265
|
+
`UPDATE history_sync
|
|
25266
|
+
SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
|
|
25267
|
+
WHERE id = 1 AND owner_pid = :pid`
|
|
25268
|
+
);
|
|
25269
|
+
this.closeWindowStmt = db.prepare(
|
|
25270
|
+
`UPDATE audit_events
|
|
25271
|
+
SET synced_at = ${String(SKIPPED)},
|
|
25272
|
+
sync_failed_at = :at,
|
|
25273
|
+
sync_failure = 'detached_undelivered'
|
|
25274
|
+
WHERE synced_at IS NULL
|
|
25275
|
+
AND event_type IN (${TYPE_LIST})
|
|
25276
|
+
AND started_at >= :attachedAt`
|
|
25277
|
+
);
|
|
25278
|
+
this.releaseBoundaryStmt = db.prepare(
|
|
25279
|
+
`UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
|
|
25280
|
+
);
|
|
25281
|
+
this.freezeBoundaryStmt = db.prepare(
|
|
25282
|
+
`UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
|
|
25283
|
+
);
|
|
25284
|
+
this.leaseStmt = db.prepare(
|
|
25285
|
+
`SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
|
|
25286
|
+
acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
|
|
25287
|
+
FROM history_sync WHERE id = 1`
|
|
25288
|
+
);
|
|
25289
|
+
this.inspectionsStmt = db.prepare(
|
|
25290
|
+
`SELECT d.rule_id AS ruleId,
|
|
25291
|
+
d.name AS ruleName,
|
|
25292
|
+
d.version AS ruleVersion,
|
|
25293
|
+
d.category AS category,
|
|
25294
|
+
d.severity AS severity,
|
|
25295
|
+
f.span_start AS spanStart,
|
|
25296
|
+
f.span_end AS spanEnd,
|
|
25297
|
+
f.masked_match AS maskedMatch,
|
|
25298
|
+
f.action_taken AS actionTaken,
|
|
25299
|
+
f.confidence AS confidence
|
|
25300
|
+
FROM inspection_findings f
|
|
25301
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
25302
|
+
WHERE f.audit_event_id = :auditEventId
|
|
25303
|
+
ORDER BY f.span_start, f.id`
|
|
25304
|
+
);
|
|
25305
|
+
}
|
|
25306
|
+
db;
|
|
25307
|
+
ensureRowStmt;
|
|
25308
|
+
sessionsStmt;
|
|
25309
|
+
rowsStmt;
|
|
25310
|
+
stampStmt;
|
|
25311
|
+
countsStmt;
|
|
25312
|
+
fingerprintStmt;
|
|
25313
|
+
setFingerprintStmt;
|
|
25314
|
+
rearmStmt;
|
|
25315
|
+
claimStmt;
|
|
25316
|
+
heartbeatStmt;
|
|
25317
|
+
releaseStmt;
|
|
25318
|
+
leaseStmt;
|
|
25319
|
+
inspectionsStmt;
|
|
25320
|
+
closeWindowStmt;
|
|
25321
|
+
releaseBoundaryStmt;
|
|
25322
|
+
freezeBoundaryStmt;
|
|
25323
|
+
captureRowsStmt;
|
|
25324
|
+
markOwedStmt;
|
|
25325
|
+
markCaptureBacklogOwedStmt;
|
|
25326
|
+
captureSkipCountStmt;
|
|
25327
|
+
disownCapturesStmt;
|
|
25328
|
+
partitionStmt;
|
|
25329
|
+
partitionByKindStmt;
|
|
25330
|
+
claimRowStmt;
|
|
25331
|
+
releaseRowStmt;
|
|
25332
|
+
releaseStaleClaimsStmt;
|
|
25333
|
+
/**
|
|
25334
|
+
* The masked detections recorded against one tool call.
|
|
25335
|
+
*
|
|
25336
|
+
* These travel with the event because a tool call's target is not
|
|
25337
|
+
* re-inspectable from the event alone — unlike a capture, where the text
|
|
25338
|
+
* itself is re-scannable. What crosses is the masked match and the rule that
|
|
25339
|
+
* produced it, never the value.
|
|
25340
|
+
*/
|
|
25341
|
+
inspectionsFor(auditEventId) {
|
|
25342
|
+
return allRows(this.inspectionsStmt, { auditEventId });
|
|
25343
|
+
}
|
|
25344
|
+
/**
|
|
25345
|
+
* Sessions with structural rows still to send, oldest first.
|
|
25346
|
+
*
|
|
25347
|
+
* BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
|
|
25348
|
+
* read. Anything recorded after the machine attached is the live forward
|
|
25349
|
+
* path's to deliver; this drain exists for what was recorded before it, and a
|
|
25350
|
+
* row both paths send is at best a duplicate request and at worst — for a
|
|
25351
|
+
* session root — an overwrite of the inventory ids the live path resolved.
|
|
25352
|
+
*/
|
|
25353
|
+
pendingSessions(limit, before) {
|
|
25354
|
+
return allRows(this.sessionsStmt, { limit, before }).map(
|
|
25355
|
+
(r) => r.sessionId
|
|
25356
|
+
);
|
|
25357
|
+
}
|
|
25358
|
+
/** One session's undelivered structural rows within the backlog, root first. */
|
|
25359
|
+
pendingRows(sessionId, limit, before) {
|
|
25360
|
+
return allRows(this.rowsStmt, { sessionId, limit, before });
|
|
25361
|
+
}
|
|
25362
|
+
/**
|
|
25363
|
+
* Captures this machine still owes the deployment, oldest first.
|
|
25364
|
+
*
|
|
25365
|
+
* Selected by the `outbox_owed` marker the attached forward path writes, not
|
|
25366
|
+
* by a time window — see captureRowsStmt for why a window could not express
|
|
25367
|
+
* this. `before` is the grace window that leaves a just-recorded capture to
|
|
25368
|
+
* the live path.
|
|
25369
|
+
*/
|
|
25370
|
+
pendingCaptureRows(limit, before) {
|
|
25371
|
+
return allRows(this.captureRowsStmt, { limit, before });
|
|
25372
|
+
}
|
|
25373
|
+
/**
|
|
25374
|
+
* Record that a capture is OWED to the deployment.
|
|
25375
|
+
*
|
|
25376
|
+
* Written by the attached forward path when a live send did not confirm
|
|
25377
|
+
* delivery, and read by the drain as the whole of its eligibility test. It is
|
|
25378
|
+
* a fact rather than an inference: the machine was attached, the send did not
|
|
25379
|
+
* land, so the row is owed — which no time window can state, because the same
|
|
25380
|
+
* window that holds the rows a past attachment left owed also holds every
|
|
25381
|
+
* capture recorded while the machine was DETACHED, and those were never
|
|
25382
|
+
* offered to anyone.
|
|
25383
|
+
*
|
|
25384
|
+
* Idempotent, and never un-set: `markSynced` settling the row is what takes it
|
|
25385
|
+
* out of the drain's read.
|
|
25386
|
+
*/
|
|
25387
|
+
markCaptureOwed(id) {
|
|
25388
|
+
this.markOwedStmt.run({ id });
|
|
25389
|
+
}
|
|
25390
|
+
/**
|
|
25391
|
+
* Mark every capture already on disk as owed, as of `before`.
|
|
25392
|
+
*
|
|
25393
|
+
* The consent-time backfill, called once from `aka attach` when a human
|
|
25394
|
+
* grants existing-history consent — never from an ongoing drain pass, and
|
|
25395
|
+
* never inferred from a boundary that could later move. `before` is the
|
|
25396
|
+
* caller's own "now" at the moment consent was granted, so what this marks
|
|
25397
|
+
* is exactly the backlog the consent prompt already counted, not whatever a
|
|
25398
|
+
* later re-attach or key rotation might widen it to.
|
|
25399
|
+
*
|
|
25400
|
+
* Returns how many rows matched, for the caller to log or test against. Not a
|
|
25401
|
+
* count of NEWLY marked rows — a row still unsynced from an earlier call
|
|
25402
|
+
* matches again and is counted again, the same as `UPDATE`'s own `changes`.
|
|
25403
|
+
*/
|
|
25404
|
+
markCaptureBacklogOwed(before) {
|
|
25405
|
+
return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
|
|
25406
|
+
}
|
|
25407
|
+
/**
|
|
25408
|
+
* Record delivery. Called only AFTER the far side has accepted the rows.
|
|
25409
|
+
*
|
|
25410
|
+
* CLEARS any failure reason in the same statement. A row that failed against
|
|
25411
|
+
* one deployment and then landed is delivered, and leaving the reason behind
|
|
25412
|
+
* would leave the store holding two contradictory answers about one row —
|
|
25413
|
+
* with the surface free to render either.
|
|
25414
|
+
*/
|
|
25415
|
+
markSynced(ids, atMs) {
|
|
25416
|
+
this.stampAll(ids, atMs, null);
|
|
25417
|
+
}
|
|
25418
|
+
/**
|
|
25419
|
+
* Record that THIS MACHINE cannot express the row on the wire.
|
|
25420
|
+
*
|
|
25421
|
+
* Reserved for a local defect — a row that cannot be rebuilt into a valid
|
|
25422
|
+
* payload, or a body the client itself refused to send. It fails identically
|
|
25423
|
+
* against every deployment, so it is terminal everywhere and the re-arm leaves
|
|
25424
|
+
* it alone. A row that merely failed to REACH the deployment stays NULL, so it
|
|
25425
|
+
* is retried; marking those would turn one outage into permanent data loss.
|
|
25426
|
+
*/
|
|
25427
|
+
markSkipped(ids, atMs) {
|
|
25428
|
+
this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
|
|
25429
|
+
}
|
|
25430
|
+
/**
|
|
25431
|
+
* Record that THIS DEPLOYMENT refused the row.
|
|
25432
|
+
*
|
|
25433
|
+
* The same sentinel as `markSkipped`, and deliberately so: both stop the row
|
|
25434
|
+
* being re-offered on this lane, and `synced_at` goes on answering whether a
|
|
25435
|
+
* row is outstanding rather than why. What separates them is the reason, and
|
|
25436
|
+
* what the reason buys is the re-arm — a refusal is one deployment's verdict
|
|
25437
|
+
* on one body, so it is terminal only for as long as this machine points at
|
|
25438
|
+
* that deployment, and `rearmFor` clears it when the deployment changes.
|
|
25439
|
+
*
|
|
25440
|
+
* Leaving such a row NULL instead would be worse than the loss it replaces:
|
|
25441
|
+
* these reads carry no cursor, so an unstamped row the deployment refuses is
|
|
25442
|
+
* the head of every subsequent page, and the lane stalls behind it for ever.
|
|
25443
|
+
*/
|
|
25444
|
+
markRefused(ids, atMs) {
|
|
25445
|
+
this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
|
|
25446
|
+
}
|
|
25447
|
+
eachInTransaction(ids, run) {
|
|
25448
|
+
if (ids.length === 0) return;
|
|
25449
|
+
withTransaction(
|
|
25450
|
+
this.db,
|
|
25451
|
+
() => {
|
|
25452
|
+
for (const id of ids) run(id);
|
|
25453
|
+
},
|
|
25454
|
+
"IMMEDIATE"
|
|
25455
|
+
);
|
|
25456
|
+
}
|
|
25457
|
+
stampAll(ids, value, failure, failedAtMs) {
|
|
25458
|
+
if (ids.length === 0) return;
|
|
25459
|
+
const failedAt = failure === null ? null : failedAtMs ?? null;
|
|
25460
|
+
withTransaction(
|
|
25461
|
+
this.db,
|
|
25462
|
+
() => {
|
|
25463
|
+
for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
|
|
25464
|
+
},
|
|
25465
|
+
"IMMEDIATE"
|
|
25466
|
+
);
|
|
25467
|
+
}
|
|
25468
|
+
/**
|
|
25469
|
+
* Claim rows as in-flight.
|
|
25470
|
+
*
|
|
25471
|
+
* Advisory in exactly the sense the lease is: it records that a send is in
|
|
25472
|
+
* progress so a surface can say so, and a lost claim costs a row showing as
|
|
25473
|
+
* queued while it is actually being sent. It is not exclusion — the far side
|
|
25474
|
+
* settles a duplicate on the row id.
|
|
25475
|
+
*/
|
|
25476
|
+
claimRows(ids, atMs) {
|
|
25477
|
+
this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
|
|
25478
|
+
}
|
|
25479
|
+
/** Give back a claim without settling — the send failed, the row is queued again. */
|
|
25480
|
+
releaseRows(ids) {
|
|
25481
|
+
this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
|
|
25482
|
+
}
|
|
25483
|
+
/**
|
|
25484
|
+
* Clear claims older than `staleBefore`, and report how many were cleared.
|
|
25485
|
+
*
|
|
25486
|
+
* A process killed between claiming and settling leaves rows claimed with
|
|
25487
|
+
* nothing left to settle them. Without this they read as "sending" for ever.
|
|
25488
|
+
*/
|
|
25489
|
+
releaseStaleClaims(staleBefore) {
|
|
25490
|
+
return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
|
|
25491
|
+
}
|
|
25492
|
+
/**
|
|
25493
|
+
* Every tracked row in exactly one delivery state.
|
|
25494
|
+
*
|
|
25495
|
+
* Takes no boundary on purpose. The boundary answers "what should the drain
|
|
25496
|
+
* pick up now", which is a different question from "what state is this row
|
|
25497
|
+
* in" — and a machine that has never attached has no boundary to pass, so
|
|
25498
|
+
* requiring one would force a caller to invent one and report the whole store
|
|
25499
|
+
* as queued.
|
|
25500
|
+
*/
|
|
25501
|
+
/**
|
|
25502
|
+
* The same partition, one row per kind that a lane carries.
|
|
25503
|
+
*
|
|
25504
|
+
* A kind with nothing to report is ABSENT rather than a row of zeros: the
|
|
25505
|
+
* scope decides which rows exist at all, so a kind that has never been
|
|
25506
|
+
* recorded — or whose captures nobody ever owed — produces no group. A caller
|
|
25507
|
+
* rendering a fixed list of kinds must therefore treat a missing one as "no
|
|
25508
|
+
* rows", never as "zero sent"; the two look identical in a bar and mean
|
|
25509
|
+
* different things.
|
|
25510
|
+
*/
|
|
25511
|
+
partitionByKind() {
|
|
25512
|
+
return allRows(
|
|
25513
|
+
this.partitionByKindStmt,
|
|
25514
|
+
{}
|
|
25515
|
+
).map((row) => ({
|
|
25516
|
+
kind: row.kind,
|
|
25517
|
+
queued: row.queued ?? 0,
|
|
25518
|
+
inProgress: row.inProgress ?? 0,
|
|
25519
|
+
synced: row.synced ?? 0,
|
|
25520
|
+
failed: row.failed ?? 0,
|
|
25521
|
+
refused: row.refused ?? 0,
|
|
25522
|
+
detached: row.detached ?? 0,
|
|
25523
|
+
total: row.total ?? 0
|
|
25524
|
+
}));
|
|
25525
|
+
}
|
|
25526
|
+
partition() {
|
|
25527
|
+
const row = getRow(this.partitionStmt, {});
|
|
25528
|
+
return {
|
|
25529
|
+
queued: row?.queued ?? 0,
|
|
25530
|
+
inProgress: row?.inProgress ?? 0,
|
|
25531
|
+
synced: row?.synced ?? 0,
|
|
25532
|
+
failed: row?.failed ?? 0,
|
|
25533
|
+
refused: row?.refused ?? 0,
|
|
25534
|
+
detached: row?.detached ?? 0,
|
|
25535
|
+
total: row?.total ?? 0
|
|
25536
|
+
};
|
|
25537
|
+
}
|
|
25538
|
+
/** `pending` counts only what is inside the backlog; sent and skipped are totals. */
|
|
25539
|
+
counts(before) {
|
|
25540
|
+
const row = getRow(this.countsStmt, { before });
|
|
25541
|
+
const captures = getRow(this.captureSkipCountStmt);
|
|
25542
|
+
return {
|
|
25543
|
+
pending: row?.pending ?? 0,
|
|
25544
|
+
sent: row?.sent ?? 0,
|
|
25545
|
+
skipped: row?.skipped ?? 0,
|
|
25546
|
+
refused: row?.refused ?? 0,
|
|
25547
|
+
detached: row?.detached ?? 0,
|
|
25548
|
+
capturesSkipped: captures?.skipped ?? 0
|
|
25549
|
+
};
|
|
25550
|
+
}
|
|
25551
|
+
/**
|
|
25552
|
+
* The deployment the current stamps were made against, and where its backlog
|
|
25553
|
+
* ends.
|
|
25554
|
+
*
|
|
25555
|
+
* READ-ONLY. An absent row reads as an absent deployment, which is what a
|
|
25556
|
+
* machine that has never drained is — and every writer below seeds the row
|
|
25557
|
+
* before it needs one, so nothing depends on this creating it. Keeping the
|
|
25558
|
+
* write off the gate path matters because the gate runs on every pass while a
|
|
25559
|
+
* write has to take the database's write lock.
|
|
25560
|
+
*/
|
|
25561
|
+
deployment() {
|
|
25562
|
+
const row = getRow(
|
|
25563
|
+
this.fingerprintStmt
|
|
25564
|
+
);
|
|
25565
|
+
return {
|
|
25566
|
+
fingerprint: row?.fingerprint ?? void 0,
|
|
25567
|
+
backlogBefore: row?.backlogBefore ?? void 0
|
|
25568
|
+
};
|
|
25569
|
+
}
|
|
25570
|
+
/**
|
|
25571
|
+
* Point the ledger at a different deployment, discarding what it recorded
|
|
25572
|
+
* about the previous one.
|
|
25573
|
+
*
|
|
25574
|
+
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
25575
|
+
* machine has just left are undelivered as far as the new one is concerned.
|
|
25576
|
+
* All four in one transaction, so a crash between them cannot leave stamps
|
|
25577
|
+
* attributed to the wrong deployment, a boundary that belongs to another, or
|
|
25578
|
+
* a disown with no re-mark to follow it.
|
|
25579
|
+
*
|
|
25580
|
+
* The boundary is written HERE and only here, which is what freezes it: a
|
|
25581
|
+
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
25582
|
+
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
25583
|
+
* the live path has since delivered.
|
|
25584
|
+
*
|
|
25585
|
+
* `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
|
|
25586
|
+
* granted existing-history consent for the deployment this call is arming —
|
|
25587
|
+
* a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
|
|
25588
|
+
* instant, `backlogBefore` is the ATTACH instant, and the two can be far
|
|
25589
|
+
* apart. Passed only when that grant is valid, since this method has no way
|
|
25590
|
+
* to check consent itself and must not mark a row owed for a machine that
|
|
25591
|
+
* never agreed to it. Applied AFTER the disown above, in the SAME
|
|
25592
|
+
* transaction: what the disown clears is every marker below `backlogBefore`,
|
|
25593
|
+
* which includes this deployment's OWN pre-attach rows — `aka attach` calls
|
|
25594
|
+
* `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
|
|
25595
|
+
* on the cleared side of that bound — and the re-mark in the same
|
|
25596
|
+
* transaction is what puts those rows back. A crash between the two cannot
|
|
25597
|
+
* strand the ledger disowned with nothing re-marked — the transaction either
|
|
25598
|
+
* lands whole or not at all, and a fingerprint mismatch that has not yet
|
|
25599
|
+
* committed re-enters this method on the very next pass. Omit it (the
|
|
25600
|
+
* structural-only tests do) to exercise the disown in isolation.
|
|
25601
|
+
*
|
|
25602
|
+
* The disown is bounded by `backlogBefore`, which is what keeps it from
|
|
25603
|
+
* touching a marker the NEW deployment's OWN live path has already set: B's
|
|
25604
|
+
* live path can mark a capture owed from the moment `aka attach` writes the
|
|
25605
|
+
* descriptor, before the drain's first pass ever reaches this method, and
|
|
25606
|
+
* such a row sits at or after the bound rather than below it. What keeps the
|
|
25607
|
+
* disown from eating THIS SAME CALL's own re-mark is the order, not the
|
|
25608
|
+
* bound — disown runs first, re-mark second, both inside the one
|
|
25609
|
+
* transaction above.
|
|
25610
|
+
*/
|
|
25611
|
+
rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
|
|
25612
|
+
this.ensureRowStmt.run();
|
|
25613
|
+
withTransaction(
|
|
25614
|
+
this.db,
|
|
25615
|
+
() => {
|
|
25616
|
+
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
25617
|
+
this.rearmStmt.run();
|
|
25618
|
+
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
25619
|
+
this.disownCapturesStmt.run({ attachedAt: backlogBefore });
|
|
25620
|
+
}
|
|
25621
|
+
if (backfillCapturesBefore !== void 0) {
|
|
25622
|
+
this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
|
|
25623
|
+
}
|
|
25624
|
+
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
25625
|
+
},
|
|
25626
|
+
"IMMEDIATE"
|
|
25627
|
+
);
|
|
25628
|
+
}
|
|
25629
|
+
/**
|
|
25630
|
+
* End the attached period: hand its rows to the live path, and release the
|
|
25631
|
+
* boundary so the next attachment can freeze a new one.
|
|
25632
|
+
*
|
|
25633
|
+
* WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
|
|
25634
|
+
* nothing delivers. The fingerprint is unchanged, so the boundary is never
|
|
25635
|
+
* re-frozen and stays at the FIRST attachment — while nothing forwards at all
|
|
25636
|
+
* during the detached period, because the machine is not attached. Rows
|
|
25637
|
+
* recorded in that window sit after the boundary and before the re-attach, so
|
|
25638
|
+
* neither path takes them, and the pending count reports none outstanding.
|
|
25639
|
+
*
|
|
25640
|
+
* WHAT IT RECORDS, and what it deliberately does not. These rows were the
|
|
25641
|
+
* closing attachment's to deliver and are no longer outstanding — that is what
|
|
25642
|
+
* lets the boundary move. It is NOT a claim that any of them arrived, and the
|
|
25643
|
+
* distinction is not academic: this used to write a delivery TIME, which every
|
|
25644
|
+
* read treats as delivery, so one detach turned a window of undelivered rows
|
|
25645
|
+
* into a window of delivered ones and no surface could tell. It writes the
|
|
25646
|
+
* skip sentinel and a reason of its own instead, so "no longer owed" and
|
|
25647
|
+
* "received" stop being the same fact.
|
|
25648
|
+
*
|
|
25649
|
+
* A change of deployment still frees them (see the re-arm), because the next
|
|
25650
|
+
* deployment has seen none of this machine's history — so the rows reach it
|
|
25651
|
+
* exactly as they did when this wrote a delivery time.
|
|
25652
|
+
*
|
|
25653
|
+
* ONE TRANSACTION, so a crash cannot release the boundary while leaving the
|
|
25654
|
+
* window unstamped — that half-state would re-send the whole attached period
|
|
25655
|
+
* on the next attach, which is the failure the boundary exists to prevent.
|
|
25656
|
+
*/
|
|
25657
|
+
closeAttachedWindow(attachedAtMs, atMs) {
|
|
25658
|
+
this.ensureRowStmt.run();
|
|
25659
|
+
withTransaction(
|
|
25660
|
+
this.db,
|
|
25661
|
+
() => {
|
|
25662
|
+
const row = getRow(this.fingerprintStmt);
|
|
25663
|
+
const from = row?.backlogBefore ?? attachedAtMs;
|
|
25664
|
+
this.closeWindowStmt.run({ at: atMs, attachedAt: from });
|
|
25665
|
+
this.releaseBoundaryStmt.run();
|
|
25666
|
+
},
|
|
25667
|
+
"IMMEDIATE"
|
|
25668
|
+
);
|
|
25669
|
+
}
|
|
25670
|
+
/**
|
|
25671
|
+
* Freeze a boundary for the deployment already on file, KEEPING the stamps.
|
|
25672
|
+
*
|
|
25673
|
+
* The re-attach half of the above. Distinct from `rearmFor`, which is for a
|
|
25674
|
+
* different deployment and therefore discards what was delivered to the old
|
|
25675
|
+
* one: here the recipient is the same, so everything already sent to it stays
|
|
25676
|
+
* sent.
|
|
25677
|
+
*/
|
|
25678
|
+
freezeBoundary(backlogBefore) {
|
|
25679
|
+
this.ensureRowStmt.run();
|
|
25680
|
+
this.freezeBoundaryStmt.run({ backlogBefore });
|
|
25681
|
+
}
|
|
25682
|
+
/** Take the claim, or report that someone live already holds it. */
|
|
25683
|
+
claim(pid, host, nowMs, staleAfterMs) {
|
|
25684
|
+
this.ensureRowStmt.run();
|
|
25685
|
+
let taken = false;
|
|
25686
|
+
withTransaction(
|
|
25687
|
+
this.db,
|
|
25688
|
+
() => {
|
|
25689
|
+
const result = this.claimStmt.run({
|
|
25690
|
+
pid,
|
|
25691
|
+
host,
|
|
25692
|
+
now: nowMs,
|
|
25693
|
+
staleBefore: nowMs - staleAfterMs
|
|
25694
|
+
});
|
|
25695
|
+
taken = result.changes === 1;
|
|
25696
|
+
},
|
|
25697
|
+
"IMMEDIATE"
|
|
25698
|
+
);
|
|
25699
|
+
return taken;
|
|
25700
|
+
}
|
|
25701
|
+
/** Say the holder is still alive. A no-op once the claim has moved on. */
|
|
25702
|
+
heartbeat(pid, nowMs) {
|
|
25703
|
+
this.heartbeatStmt.run({ now: nowMs, pid });
|
|
25704
|
+
}
|
|
25705
|
+
/** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
|
|
25706
|
+
release(pid) {
|
|
25707
|
+
this.releaseStmt.run({ pid });
|
|
25708
|
+
}
|
|
25709
|
+
/** Who holds the claim, if anyone. Read-only, for the same reason as above. */
|
|
25710
|
+
lease() {
|
|
25711
|
+
return getRow(this.leaseStmt);
|
|
25712
|
+
}
|
|
25713
|
+
};
|
|
25714
|
+
|
|
24701
25715
|
// ../../packages/persistence/src/migrations.ts
|
|
24702
25716
|
function describeObject(object2) {
|
|
24703
25717
|
return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
|
|
@@ -24710,7 +25724,7 @@ function createdIndexName(statement) {
|
|
|
24710
25724
|
return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
|
|
24711
25725
|
}
|
|
24712
25726
|
var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
|
|
24713
|
-
function applyMigrations(db, file2) {
|
|
25727
|
+
function applyMigrations(db, file2, options = {}) {
|
|
24714
25728
|
const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
|
|
24715
25729
|
db.exec(
|
|
24716
25730
|
"CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
|
|
@@ -24724,6 +25738,7 @@ function applyMigrations(db, file2) {
|
|
|
24724
25738
|
);
|
|
24725
25739
|
for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
|
|
24726
25740
|
if (applied.has(migration.tag)) continue;
|
|
25741
|
+
if (options.skipTags?.has(migration.tag) === true) continue;
|
|
24727
25742
|
if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
|
|
24728
25743
|
const evidence = evidenceObjects(migration.sql);
|
|
24729
25744
|
const present = evidence.filter((o) => evidenceExists(db, o));
|
|
@@ -25130,10 +26145,62 @@ function ensureSyncedAtColumn(db, table) {
|
|
|
25130
26145
|
if (!columns.includes("outbox_owed")) {
|
|
25131
26146
|
db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
|
|
25132
26147
|
}
|
|
26148
|
+
if (!columns.includes("sync_failed_at")) {
|
|
26149
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failed_at integer`);
|
|
26150
|
+
}
|
|
26151
|
+
if (!columns.includes("sync_failure")) {
|
|
26152
|
+
withTransaction(
|
|
26153
|
+
db,
|
|
26154
|
+
() => {
|
|
26155
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failure text`);
|
|
26156
|
+
db.exec(
|
|
26157
|
+
`UPDATE ${table} SET synced_at = NULL
|
|
26158
|
+
WHERE synced_at = -1
|
|
26159
|
+
AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
|
|
26160
|
+
);
|
|
26161
|
+
},
|
|
26162
|
+
"IMMEDIATE"
|
|
26163
|
+
);
|
|
26164
|
+
}
|
|
25133
26165
|
db.exec(
|
|
25134
|
-
`CREATE
|
|
25135
|
-
|
|
26166
|
+
`CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
|
|
26167
|
+
BEFORE UPDATE OF sync_failure ON ${table}
|
|
26168
|
+
WHEN ${syncFailureRejectCondition()}
|
|
26169
|
+
BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
|
|
25136
26170
|
);
|
|
26171
|
+
const syncIndexColumns = [
|
|
26172
|
+
"event_type",
|
|
26173
|
+
"synced_at",
|
|
26174
|
+
"sync_claimed_at",
|
|
26175
|
+
"started_at",
|
|
26176
|
+
// Appended LAST on purpose. The delivery-state read now projects it, so it
|
|
26177
|
+
// has to be in the index for the read to stay covered — but putting it
|
|
26178
|
+
// ahead of `started_at` would reorder the prefix the structural drain's
|
|
26179
|
+
// reads match on.
|
|
26180
|
+
"sync_failure"
|
|
26181
|
+
// `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
|
|
26182
|
+
//
|
|
26183
|
+
// The delivery-state read tests it — a capture's state depends on whether a
|
|
26184
|
+
// live forward marked it owed — so carrying it here makes that read covering
|
|
26185
|
+
// rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
|
|
26186
|
+
// But a sixth column changes what the planner charges for this index, and
|
|
26187
|
+
// with no ANALYZE statistics it plans from schema shape alone: measured, it
|
|
26188
|
+
// then stops choosing the per-session index for the token rollup and walks
|
|
26189
|
+
// every `llm_call` in the store through the event-type index instead. That
|
|
26190
|
+
// read grows with the store; this one does not.
|
|
26191
|
+
//
|
|
26192
|
+
// 40 ms on the largest store measured, once per render, is a cost worth
|
|
26193
|
+
// paying to leave every other read's plan where it was.
|
|
26194
|
+
];
|
|
26195
|
+
const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
|
|
26196
|
+
const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
|
|
26197
|
+
if (!syncIndexMatches) {
|
|
26198
|
+
db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
|
|
26199
|
+
db.exec(
|
|
26200
|
+
`CREATE INDEX idx_audit_events_sync
|
|
26201
|
+
ON audit_events (${syncIndexColumns.join(", ")})`
|
|
26202
|
+
);
|
|
26203
|
+
}
|
|
25137
26204
|
db.exec(
|
|
25138
26205
|
`CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
|
|
25139
26206
|
ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
|
|
@@ -25355,7 +26422,11 @@ function buildAuditEvent(row) {
|
|
|
25355
26422
|
link: linkParsed?.success ? linkParsed.data : null,
|
|
25356
26423
|
targetId: row.target_id,
|
|
25357
26424
|
internal: intToBool(row.internal),
|
|
25358
|
-
flagged: intToBool(row.flagged)
|
|
26425
|
+
flagged: intToBool(row.flagged),
|
|
26426
|
+
// Only meaningful when the title came out empty — a row whose body was
|
|
26427
|
+
// expired but whose title fell back to `tool_name` still has something to
|
|
26428
|
+
// render, and flagging it would make the view apologise for nothing.
|
|
26429
|
+
bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
|
|
25359
26430
|
};
|
|
25360
26431
|
}
|
|
25361
26432
|
var TIMELINE_COLUMNS = `
|
|
@@ -25363,6 +26434,7 @@ var TIMELINE_COLUMNS = `
|
|
|
25363
26434
|
event_type,
|
|
25364
26435
|
started_at,
|
|
25365
26436
|
coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
|
|
26437
|
+
content_expired_at,
|
|
25366
26438
|
coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
|
|
25367
26439
|
coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
|
|
25368
26440
|
json_extract(attributes, '$.severity') AS severity,
|
|
@@ -25489,7 +26561,8 @@ var SqliteActivityRepository = class {
|
|
|
25489
26561
|
SELECT 1 FROM audit_events d
|
|
25490
26562
|
WHERE d.root_session_id = audit_events.id
|
|
25491
26563
|
AND (d.content LIKE ? ESCAPE '\\'
|
|
25492
|
-
OR json_extract(d.attributes, '$.detail')
|
|
26564
|
+
OR coalesce(json_extract(d.attributes, '$.detail'),
|
|
26565
|
+
json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
|
|
25493
26566
|
);
|
|
25494
26567
|
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
25495
26568
|
}
|
|
@@ -26027,6 +27100,88 @@ var SqliteAuditEventsRepository = class {
|
|
|
26027
27100
|
}
|
|
26028
27101
|
};
|
|
26029
27102
|
|
|
27103
|
+
// ../../packages/persistence/src/repositories/body-retention.ts
|
|
27104
|
+
var DEFAULT_BATCH_SIZE = 500;
|
|
27105
|
+
var DEFAULT_MAX_ROWS = 5e4;
|
|
27106
|
+
var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
|
|
27107
|
+
var SqliteBodyRetentionRepository = class {
|
|
27108
|
+
constructor(db) {
|
|
27109
|
+
this.db = db;
|
|
27110
|
+
const select = (laneClause) => `
|
|
27111
|
+
SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
|
|
27112
|
+
FROM audit_events
|
|
27113
|
+
WHERE content IS NOT NULL
|
|
27114
|
+
AND started_at < :cutoff
|
|
27115
|
+
AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
27116
|
+
${laneClause}
|
|
27117
|
+
ORDER BY started_at
|
|
27118
|
+
LIMIT :limit`;
|
|
27119
|
+
this.candidatesStmt = this.db.prepare(select(""));
|
|
27120
|
+
this.candidatesSyncSafeStmt = this.db.prepare(
|
|
27121
|
+
select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
|
|
27122
|
+
);
|
|
27123
|
+
this.heldBySyncStmt = this.db.prepare(`
|
|
27124
|
+
SELECT COUNT(*) AS n
|
|
27125
|
+
FROM audit_events
|
|
27126
|
+
WHERE content IS NOT NULL
|
|
27127
|
+
AND started_at < :cutoff
|
|
27128
|
+
AND event_type IN (${SYNC_LANE_TYPES_SQL})
|
|
27129
|
+
AND synced_at IS NULL`);
|
|
27130
|
+
this.expireStmt = this.db.prepare(
|
|
27131
|
+
`UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
|
|
27132
|
+
);
|
|
27133
|
+
}
|
|
27134
|
+
db;
|
|
27135
|
+
candidatesStmt;
|
|
27136
|
+
candidatesSyncSafeStmt;
|
|
27137
|
+
heldBySyncStmt;
|
|
27138
|
+
expireStmt;
|
|
27139
|
+
/** How many bytes a pass with these options would free, changing nothing. */
|
|
27140
|
+
preview(opts) {
|
|
27141
|
+
const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
|
|
27142
|
+
const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
|
|
27143
|
+
const rows = stmt.all({ cutoff: opts.cutoff, limit });
|
|
27144
|
+
return {
|
|
27145
|
+
rowsExpired: rows.length,
|
|
27146
|
+
bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
|
|
27147
|
+
rowsHeldBySync: this.countHeldBySync(opts)
|
|
27148
|
+
};
|
|
27149
|
+
}
|
|
27150
|
+
/** Clear eligible bodies, in bounded batches. */
|
|
27151
|
+
expire(opts) {
|
|
27152
|
+
const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
27153
|
+
const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
|
|
27154
|
+
const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
|
|
27155
|
+
let rowsExpired = 0;
|
|
27156
|
+
let bytesFreed = 0;
|
|
27157
|
+
let done = true;
|
|
27158
|
+
while (rowsExpired < maxRows) {
|
|
27159
|
+
const remaining = Math.min(batchSize, maxRows - rowsExpired);
|
|
27160
|
+
const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
|
|
27161
|
+
if (batch.length === 0) break;
|
|
27162
|
+
withTransaction(
|
|
27163
|
+
this.db,
|
|
27164
|
+
() => {
|
|
27165
|
+
for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
|
|
27166
|
+
},
|
|
27167
|
+
"IMMEDIATE"
|
|
27168
|
+
);
|
|
27169
|
+
rowsExpired += batch.length;
|
|
27170
|
+
bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
|
|
27171
|
+
if (batch.length < remaining) break;
|
|
27172
|
+
if (rowsExpired >= maxRows) {
|
|
27173
|
+
done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
|
|
27174
|
+
}
|
|
27175
|
+
}
|
|
27176
|
+
return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
|
|
27177
|
+
}
|
|
27178
|
+
countHeldBySync(opts) {
|
|
27179
|
+
if (opts.sweepSyncLane) return 0;
|
|
27180
|
+
const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
|
|
27181
|
+
return row.n;
|
|
27182
|
+
}
|
|
27183
|
+
};
|
|
27184
|
+
|
|
26030
27185
|
// ../../packages/persistence/src/repositories/classified-data.ts
|
|
26031
27186
|
var SqliteClassifiedDataRepository = class {
|
|
26032
27187
|
constructor(db) {
|
|
@@ -26827,23 +27982,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
|
|
|
26827
27982
|
)`;
|
|
26828
27983
|
|
|
26829
27984
|
// ../../packages/persistence/src/repositories/findings.ts
|
|
26830
|
-
var PREVIEW_INSTANCES_PER_GROUP = 200;
|
|
26831
|
-
var DEFAULT_LOCATIONS_LIMIT = 100;
|
|
26832
|
-
var LOCATION_RULE_IDS_CAP = 20;
|
|
26833
|
-
function compareLocationOrder(a, b) {
|
|
26834
|
-
return compareFindingGroupOrder(
|
|
26835
|
-
{
|
|
26836
|
-
severity: a.maxSeverity,
|
|
26837
|
-
latestDetectedAt: a.latestDetectedAt,
|
|
26838
|
-
id: ""
|
|
26839
|
-
},
|
|
26840
|
-
{
|
|
26841
|
-
severity: b.maxSeverity,
|
|
26842
|
-
latestDetectedAt: b.latestDetectedAt,
|
|
26843
|
-
id: ""
|
|
26844
|
-
}
|
|
26845
|
-
);
|
|
26846
|
-
}
|
|
26847
27985
|
var CONCAT_SEP = ",";
|
|
26848
27986
|
var TUPLE_SEP = "|";
|
|
26849
27987
|
function splitConcat(value) {
|
|
@@ -26872,7 +28010,15 @@ function toFlatFindingRow(r) {
|
|
|
26872
28010
|
...r.tool_name === null ? {} : { toolName: r.tool_name },
|
|
26873
28011
|
eventId: r.event_id,
|
|
26874
28012
|
...r.session_id === null ? {} : { sessionId: r.session_id },
|
|
26875
|
-
status: deriveInstanceStatus(r)
|
|
28013
|
+
status: deriveInstanceStatus(r),
|
|
28014
|
+
delivery: deriveFindingDelivery({
|
|
28015
|
+
kind: r.kind,
|
|
28016
|
+
syncedAt: r.synced_at,
|
|
28017
|
+
syncClaimedAt: r.sync_claimed_at,
|
|
28018
|
+
syncFailedAt: r.sync_failed_at,
|
|
28019
|
+
syncFailure: r.sync_failure,
|
|
28020
|
+
outboxOwed: r.outbox_owed
|
|
28021
|
+
})
|
|
26876
28022
|
};
|
|
26877
28023
|
}
|
|
26878
28024
|
function encodeGroupCursor(group) {
|
|
@@ -26895,13 +28041,51 @@ function decodeGroupCursor(cursor) {
|
|
|
26895
28041
|
return null;
|
|
26896
28042
|
}
|
|
26897
28043
|
function firstAfter(sorted, cursor) {
|
|
26898
|
-
const index = sorted.findIndex((
|
|
28044
|
+
const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
|
|
26899
28045
|
return index === -1 ? sorted.length : index;
|
|
26900
28046
|
}
|
|
26901
28047
|
function findDeepLinked(sorted, page, id) {
|
|
26902
|
-
if (page.some((
|
|
26903
|
-
return sorted.find((
|
|
28048
|
+
if (page.some((t) => t.id === id)) return void 0;
|
|
28049
|
+
return sorted.find((t) => t.id === id);
|
|
28050
|
+
}
|
|
28051
|
+
function encodeLocationCursor(location) {
|
|
28052
|
+
const payload = {
|
|
28053
|
+
sev: location.maxSeverity,
|
|
28054
|
+
t: location.latestDetectedAt,
|
|
28055
|
+
r: location.repo,
|
|
28056
|
+
f: location.file
|
|
28057
|
+
};
|
|
28058
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
28059
|
+
}
|
|
28060
|
+
function decodeLocationCursor(cursor) {
|
|
28061
|
+
const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
28062
|
+
if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
|
|
28063
|
+
return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
|
|
28064
|
+
}
|
|
28065
|
+
return null;
|
|
26904
28066
|
}
|
|
28067
|
+
function firstLocationAfter(sorted, cursor) {
|
|
28068
|
+
const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
|
|
28069
|
+
return index === -1 ? sorted.length : index;
|
|
28070
|
+
}
|
|
28071
|
+
function findDeepLinkedLocation(sorted, page, id) {
|
|
28072
|
+
if (page.some((l) => l.id === id)) return void 0;
|
|
28073
|
+
return sorted.find((l) => l.id === id);
|
|
28074
|
+
}
|
|
28075
|
+
var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
|
|
28076
|
+
d.severity AS severity, f.masked_match AS masked_match,
|
|
28077
|
+
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
28078
|
+
e.started_at AS occurred_at,
|
|
28079
|
+
e.source_tool AS source_tool,
|
|
28080
|
+
e.repo AS repo,
|
|
28081
|
+
e.file_path AS file,
|
|
28082
|
+
e.tool_name AS tool_name,
|
|
28083
|
+
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
28084
|
+
e.event_type AS kind, f.finding_key AS finding_key,
|
|
28085
|
+
${latestResolutionStatusSql("f")} AS latest_status,
|
|
28086
|
+
e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
|
|
28087
|
+
e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
|
|
28088
|
+
e.outbox_owed AS outbox_owed`;
|
|
26905
28089
|
var DAY_MS3 = 864e5;
|
|
26906
28090
|
var SqliteFindingsRepository = class {
|
|
26907
28091
|
constructor(db) {
|
|
@@ -27022,30 +28206,26 @@ var SqliteFindingsRepository = class {
|
|
|
27022
28206
|
);
|
|
27023
28207
|
}
|
|
27024
28208
|
/**
|
|
27025
|
-
*
|
|
27026
|
-
*
|
|
27027
|
-
*
|
|
27028
|
-
*
|
|
27029
|
-
*
|
|
27030
|
-
*
|
|
27031
|
-
*
|
|
27032
|
-
*
|
|
27033
|
-
*
|
|
27034
|
-
*
|
|
27035
|
-
*
|
|
27036
|
-
*
|
|
28209
|
+
* Finding TYPES for the dashboard — one row per rule, scoped to the four
|
|
28210
|
+
* capture kinds (audit_events also holds structural/reconciler/scan rows this
|
|
28211
|
+
* list must never surface), with per-filter-excluded facets, the requested
|
|
28212
|
+
* filters applied, and sorted by severity then recency. Filtering and faceting
|
|
28213
|
+
* run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
|
|
28214
|
+
* full filtered set; `items` is the requested page (default 50), keyset-paged.
|
|
28215
|
+
* Under a `status` filter, `totals.findings` counts only findings whose
|
|
28216
|
+
* derived status was requested.
|
|
28217
|
+
*
|
|
28218
|
+
* ONE read, which materializes no findings: a single aggregate per rule_id,
|
|
28219
|
+
* folding EVERY finding into the numbers a type row and the filters need
|
|
28220
|
+
* (count, severity, category, providers, actions, statuses, latest, search
|
|
28221
|
+
* text). The findings OF a type come from listFindingInstances scoped to
|
|
28222
|
+
* `subtype`, so neither list bounds the other and no per-type cap exists.
|
|
27037
28223
|
*
|
|
27038
|
-
* Two reads, neither of which materializes a row per finding:
|
|
27039
|
-
* 1. one aggregate row per rule_id, folding EVERY instance into the numbers
|
|
27040
|
-
* the group and the filters need (count, providers, actions, statuses,
|
|
27041
|
-
* latest, search text);
|
|
27042
|
-
* 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
|
|
27043
|
-
* populate `instances` for the table's expanded rows.
|
|
27044
28224
|
* The aggregates carry raw DB values and are translated by the same
|
|
27045
|
-
* @akasecurity/schema mappers
|
|
27046
|
-
* rule is ever restated in SQL.
|
|
28225
|
+
* @akasecurity/schema mappers every other path uses, so no enum mapping or
|
|
28226
|
+
* status rule is ever restated in SQL.
|
|
27047
28227
|
*/
|
|
27048
|
-
|
|
28228
|
+
listFindingTypes(query) {
|
|
27049
28229
|
const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
|
|
27050
28230
|
const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
|
|
27051
28231
|
const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
|
|
@@ -27058,12 +28238,7 @@ var SqliteFindingsRepository = class {
|
|
|
27058
28238
|
predicate,
|
|
27059
28239
|
params: sessionParams
|
|
27060
28240
|
});
|
|
27061
|
-
const
|
|
27062
|
-
sessionId: query.sessionId,
|
|
27063
|
-
from: query.from
|
|
27064
|
-
});
|
|
27065
|
-
const groupable = rows.map(toFlatFindingRow);
|
|
27066
|
-
const allGroups = buildFindingGroups(groupable, { aggregates });
|
|
28241
|
+
const allTypes = buildFindingTypes(aggregates);
|
|
27067
28242
|
const filterOpts = {
|
|
27068
28243
|
severity: query.severity,
|
|
27069
28244
|
providers: query.provider,
|
|
@@ -27072,30 +28247,25 @@ var SqliteFindingsRepository = class {
|
|
|
27072
28247
|
subtype: query.subtype,
|
|
27073
28248
|
q: query.q
|
|
27074
28249
|
};
|
|
27075
|
-
const facets = computeFindingFacets(
|
|
27076
|
-
const sorted =
|
|
28250
|
+
const facets = computeFindingFacets(allTypes, filterOpts);
|
|
28251
|
+
const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
|
|
27077
28252
|
const statusFilter = query.status ?? [];
|
|
27078
28253
|
const totals = {
|
|
27079
|
-
findings: sorted.reduce((acc,
|
|
27080
|
-
if (statusFilter.length === 0) return acc +
|
|
27081
|
-
const agg = aggregates.get(
|
|
27082
|
-
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ??
|
|
28254
|
+
findings: sorted.reduce((acc, t) => {
|
|
28255
|
+
if (statusFilter.length === 0) return acc + t.instanceCount;
|
|
28256
|
+
const agg = aggregates.get(t.id);
|
|
28257
|
+
return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
|
|
27083
28258
|
}, 0),
|
|
27084
|
-
|
|
28259
|
+
types: sorted.length
|
|
27085
28260
|
};
|
|
27086
|
-
const limit = query.limit ??
|
|
28261
|
+
const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
|
|
27087
28262
|
const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
|
|
27088
28263
|
const start = cursor === null ? 0 : firstAfter(sorted, cursor);
|
|
27089
28264
|
const page = sorted.slice(start, start + limit);
|
|
27090
28265
|
const lastOnPage = page.at(-1);
|
|
27091
28266
|
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
|
|
27092
28267
|
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
|
|
27093
|
-
const
|
|
27094
|
-
const narrow = (g) => statusSet ? {
|
|
27095
|
-
...g,
|
|
27096
|
-
instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
|
|
27097
|
-
} : g;
|
|
27098
|
-
const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
|
|
28268
|
+
const items = [...page, ...deepLinked ? [deepLinked] : []];
|
|
27099
28269
|
return Promise.resolve({
|
|
27100
28270
|
totals,
|
|
27101
28271
|
facets,
|
|
@@ -27106,7 +28276,7 @@ var SqliteFindingsRepository = class {
|
|
|
27106
28276
|
}
|
|
27107
28277
|
/**
|
|
27108
28278
|
* One row per rule_id, folding EVERY instance of the group into the values
|
|
27109
|
-
*
|
|
28279
|
+
* buildFindingTypes cannot recover from an aggregate. Bounded by the number of
|
|
27110
28280
|
* distinct rule_ids (the installed packs' rules), not by the store's size.
|
|
27111
28281
|
*
|
|
27112
28282
|
* A single scan, folded in two levels: the inner SELECT groups by
|
|
@@ -27160,6 +28330,7 @@ var SqliteFindingsRepository = class {
|
|
|
27160
28330
|
providers: query.provider,
|
|
27161
28331
|
actions: query.action,
|
|
27162
28332
|
statuses: query.status,
|
|
28333
|
+
deliveries: query.deployment,
|
|
27163
28334
|
tools: query.tool,
|
|
27164
28335
|
repo: query.repo,
|
|
27165
28336
|
file: query.file,
|
|
@@ -27200,13 +28371,25 @@ var SqliteFindingsRepository = class {
|
|
|
27200
28371
|
});
|
|
27201
28372
|
}
|
|
27202
28373
|
/**
|
|
27203
|
-
* The same findings folded by
|
|
28374
|
+
* The same findings folded by WHERE they live — one row per (repo, file) pair.
|
|
27204
28375
|
*
|
|
27205
28376
|
* The grouping keys come from the capturing event's attributes, which is what
|
|
27206
|
-
* the local store relates a finding to
|
|
27207
|
-
*
|
|
27208
|
-
* empty-string bucket, which
|
|
27209
|
-
*
|
|
28377
|
+
* the local store relates a finding to; there is no finding↔asset row to group
|
|
28378
|
+
* by instead. A repo or file the event did not record folds into the
|
|
28379
|
+
* empty-string bucket, which is a real location like any other: it is listed,
|
|
28380
|
+
* it is selectable, and its `?loc=` token is as good as any other row's.
|
|
28381
|
+
*
|
|
28382
|
+
* ONE flat list rather than repos nesting files. A rollup can only be paged by
|
|
28383
|
+
* repo, which leaves the file list inside it unbounded — the shape the by-type
|
|
28384
|
+
* list was rebuilt to remove — and two-level pagination inside an
|
|
28385
|
+
* expand/collapse table is what pushed that view to master/detail in the first
|
|
28386
|
+
* place.
|
|
28387
|
+
*
|
|
28388
|
+
* Every filter narrows the FINDINGS and the locations fall out of what
|
|
28389
|
+
* survives, so each row's `instanceCount` is exactly what listFindingInstances
|
|
28390
|
+
* reports for the same filters scoped to that pair. The view depends on it:
|
|
28391
|
+
* one toolbar sits over both panels precisely because a location owns none of
|
|
28392
|
+
* its fields.
|
|
27210
28393
|
*/
|
|
27211
28394
|
listFindingLocations(query) {
|
|
27212
28395
|
const opts = {
|
|
@@ -27215,16 +28398,20 @@ var SqliteFindingsRepository = class {
|
|
|
27215
28398
|
providers: query.provider,
|
|
27216
28399
|
actions: query.action,
|
|
27217
28400
|
statuses: query.status,
|
|
28401
|
+
deliveries: query.deployment,
|
|
27218
28402
|
tools: query.tool,
|
|
27219
28403
|
q: query.q
|
|
27220
28404
|
};
|
|
27221
|
-
const limit = query.limit ??
|
|
28405
|
+
const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
|
|
28406
|
+
const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
|
|
27222
28407
|
const byRepo = /* @__PURE__ */ new Map();
|
|
28408
|
+
const accumulator = createInstanceFacetAccumulator(opts);
|
|
27223
28409
|
let total = 0;
|
|
27224
28410
|
for (const row of this.scanFindingRows({
|
|
27225
28411
|
sessionId: query.sessionId,
|
|
27226
28412
|
from: query.from
|
|
27227
28413
|
})) {
|
|
28414
|
+
accumulator.add(row);
|
|
27228
28415
|
if (!matchesInstanceFilters(row, opts)) continue;
|
|
27229
28416
|
total += 1;
|
|
27230
28417
|
let files = byRepo.get(row.repo);
|
|
@@ -27239,103 +28426,35 @@ var SqliteFindingsRepository = class {
|
|
|
27239
28426
|
}
|
|
27240
28427
|
addToLocation(acc, row);
|
|
27241
28428
|
}
|
|
27242
|
-
|
|
27243
|
-
const
|
|
27244
|
-
|
|
27245
|
-
|
|
27246
|
-
|
|
27247
|
-
|
|
27248
|
-
|
|
27249
|
-
|
|
27250
|
-
|
|
27251
|
-
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
27260
|
-
|
|
27261
|
-
|
|
27262
|
-
|
|
27263
|
-
|
|
27264
|
-
);
|
|
27265
|
-
const statuses = fileRows.map((f) => f.status);
|
|
27266
|
-
const folded = foldGroupStatus(statuses);
|
|
27267
|
-
return {
|
|
27268
|
-
repo,
|
|
27269
|
-
instanceCount: rollup.instanceCount,
|
|
27270
|
-
maxSeverity: rollup.maxSeverity,
|
|
27271
|
-
latestDetectedAt: rollup.latestDetectedAt,
|
|
27272
|
-
...folded === void 0 ? {} : { status: folded },
|
|
27273
|
-
files: fileRows
|
|
27274
|
-
};
|
|
27275
|
-
});
|
|
27276
|
-
repos.sort(compareLocationOrder);
|
|
28429
|
+
const sorted = [];
|
|
28430
|
+
for (const [repo, files] of byRepo) {
|
|
28431
|
+
for (const [file2, acc] of files) {
|
|
28432
|
+
const status = foldGroupStatus(acc.statuses);
|
|
28433
|
+
sorted.push({
|
|
28434
|
+
id: encodeLocationId(repo, file2),
|
|
28435
|
+
repo,
|
|
28436
|
+
file: file2,
|
|
28437
|
+
instanceCount: acc.instanceCount,
|
|
28438
|
+
maxSeverity: acc.maxSeverity,
|
|
28439
|
+
latestDetectedAt: acc.latestDetectedAt,
|
|
28440
|
+
...status === void 0 ? {} : { status },
|
|
28441
|
+
ruleIds: [...acc.ruleIds]
|
|
28442
|
+
});
|
|
28443
|
+
}
|
|
28444
|
+
}
|
|
28445
|
+
sorted.sort(compareLocationOrder);
|
|
28446
|
+
const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
|
|
28447
|
+
const page = sorted.slice(start, start + limit);
|
|
28448
|
+
const lastOnPage = page.at(-1);
|
|
28449
|
+
const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
|
|
28450
|
+
const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
|
|
27277
28451
|
return Promise.resolve({
|
|
27278
|
-
totals: { findings: total,
|
|
27279
|
-
|
|
27280
|
-
|
|
28452
|
+
totals: { findings: total, locations: sorted.length },
|
|
28453
|
+
facets: accumulator.facets(),
|
|
28454
|
+
items: [...page, ...deepLinked ? [deepLinked] : []],
|
|
28455
|
+
nextCursor
|
|
27281
28456
|
});
|
|
27282
28457
|
}
|
|
27283
|
-
/**
|
|
27284
|
-
* Each group's newest instances, for the table's expanded rows.
|
|
27285
|
-
*
|
|
27286
|
-
* ONE index-ordered scan with early termination, and the shape is the point.
|
|
27287
|
-
* The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
|
|
27288
|
-
* started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
|
|
27289
|
-
* through a temp B-tree to keep a bounded preview of each group, and then
|
|
27290
|
-
* sorts the survivors again for the page order. Both sorts grow with the
|
|
27291
|
-
* store while the answer does not.
|
|
27292
|
-
*
|
|
27293
|
-
* Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
|
|
27294
|
-
* (or the session or window index the scope names — see `findingScanSql`),
|
|
27295
|
-
* which is already the order the page wants, and keeps rows per rule until
|
|
27296
|
-
* each rule has as many as it can show. The aggregate the caller already holds
|
|
27297
|
-
* says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
|
|
27298
|
-
* per rule, summed, is the number of rows this scan has to find, and it stops
|
|
27299
|
-
* on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
|
|
27300
|
-
* (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
|
|
27301
|
-
* store with many firing rules widens it. The bound that DOES hold
|
|
27302
|
-
* unconditionally is the sorted form's floor: this scan visits at most as
|
|
27303
|
-
* many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
|
|
27304
|
-
* sorted, and stops the moment every rule has its cap, where the sorted form
|
|
27305
|
-
* sorts the whole scope regardless. The true worst case — the rarest rule's
|
|
27306
|
-
* wanted instances sitting at the tail of the scope — is one pass over
|
|
27307
|
-
* everything in scope with a block sort of the id tie-break only, never a
|
|
27308
|
-
* sort of the scope, which is still that floor.
|
|
27309
|
-
*
|
|
27310
|
-
* A row whose rule the aggregate did not see is skipped: the two statements
|
|
27311
|
-
* run without a shared snapshot, so a capture landing between them can add a
|
|
27312
|
-
* rule here that has no counts there, and the counts are what the group is
|
|
27313
|
-
* built from.
|
|
27314
|
-
*/
|
|
27315
|
-
previewRows(aggregates, scope) {
|
|
27316
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
27317
|
-
let remaining = 0;
|
|
27318
|
-
for (const [ruleId, agg] of aggregates) {
|
|
27319
|
-
const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
|
|
27320
|
-
wanted.set(ruleId, n);
|
|
27321
|
-
remaining += n;
|
|
27322
|
-
}
|
|
27323
|
-
const rows = [];
|
|
27324
|
-
if (remaining === 0) return rows;
|
|
27325
|
-
const { sql, params } = this.findingScanSql(scope);
|
|
27326
|
-
const taken = /* @__PURE__ */ new Map();
|
|
27327
|
-
for (const r of iterateRows(this.db.prepare(sql), params)) {
|
|
27328
|
-
const want = wanted.get(r.rule_id);
|
|
27329
|
-
if (want === void 0) continue;
|
|
27330
|
-
const have = taken.get(r.rule_id) ?? 0;
|
|
27331
|
-
if (have >= want) continue;
|
|
27332
|
-
taken.set(r.rule_id, have + 1);
|
|
27333
|
-
rows.push(r);
|
|
27334
|
-
remaining -= 1;
|
|
27335
|
-
if (remaining === 0) break;
|
|
27336
|
-
}
|
|
27337
|
-
return rows;
|
|
27338
|
-
}
|
|
27339
28458
|
/**
|
|
27340
28459
|
* Every finding in scope as a FlatFindingRow, newest first, streamed.
|
|
27341
28460
|
*
|
|
@@ -27362,6 +28481,33 @@ var SqliteFindingsRepository = class {
|
|
|
27362
28481
|
yield toFlatFindingRow(r);
|
|
27363
28482
|
}
|
|
27364
28483
|
}
|
|
28484
|
+
/**
|
|
28485
|
+
* One finding by its own id, or null when no such row exists.
|
|
28486
|
+
*
|
|
28487
|
+
* A primary-key seek on `inspection_findings`, so its cost does not grow with
|
|
28488
|
+
* the store — and, unlike anything derived from a list page, it resolves a
|
|
28489
|
+
* finding of ANY age. That is what the Findings page's one-shot `?finding=`
|
|
28490
|
+
* deep link needs: the id it carries may name a finding thousands of rows
|
|
28491
|
+
* older than anything a first page holds.
|
|
28492
|
+
*
|
|
28493
|
+
* Deliberately UNFILTERED — no capture-kind, session or time predicate. It
|
|
28494
|
+
* RESOLVES an id; whether that row would survive the list's current filters is
|
|
28495
|
+
* a different question, and hiding the target because a filter excludes it is
|
|
28496
|
+
* worse than showing it.
|
|
28497
|
+
*
|
|
28498
|
+
* `groupId` on the result IS the rule id, so this one read answers both "which
|
|
28499
|
+
* type should the list select?" and "what does the drawer show?".
|
|
28500
|
+
*/
|
|
28501
|
+
findingInstance(id) {
|
|
28502
|
+
const row = this.db.prepare(
|
|
28503
|
+
`SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
28504
|
+
FROM inspection_findings f
|
|
28505
|
+
JOIN audit_events e ON e.id = f.audit_event_id
|
|
28506
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
28507
|
+
WHERE f.id = ?`
|
|
28508
|
+
).get(id);
|
|
28509
|
+
return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
|
|
28510
|
+
}
|
|
27365
28511
|
/**
|
|
27366
28512
|
* The one statement both instance-level scans run: every finding in scope,
|
|
27367
28513
|
* joined to its event and definition, newest first.
|
|
@@ -27395,17 +28541,7 @@ var SqliteFindingsRepository = class {
|
|
|
27395
28541
|
conditions.push("e.started_at >= ?");
|
|
27396
28542
|
params.push(isoToEpochMillis(scope.from));
|
|
27397
28543
|
}
|
|
27398
|
-
const sql = `SELECT
|
|
27399
|
-
d.severity AS severity, f.masked_match AS masked_match,
|
|
27400
|
-
f.action_taken AS action_taken, f.confidence AS confidence,
|
|
27401
|
-
e.started_at AS occurred_at,
|
|
27402
|
-
e.source_tool AS source_tool,
|
|
27403
|
-
e.repo AS repo,
|
|
27404
|
-
e.file_path AS file,
|
|
27405
|
-
e.tool_name AS tool_name,
|
|
27406
|
-
f.audit_event_id AS event_id, e.root_session_id AS session_id,
|
|
27407
|
-
e.event_type AS kind, f.finding_key AS finding_key,
|
|
27408
|
-
${latestResolutionStatusSql("f")} AS latest_status
|
|
28544
|
+
const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
|
|
27409
28545
|
FROM audit_events e
|
|
27410
28546
|
CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
|
|
27411
28547
|
CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
@@ -27419,6 +28555,26 @@ var SqliteFindingsRepository = class {
|
|
|
27419
28555
|
group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
|
|
27420
28556
|
const rows = this.db.prepare(
|
|
27421
28557
|
`SELECT rule_id,
|
|
28558
|
+
-- BARE columns beside max(latest_at), which is deliberate and
|
|
28559
|
+
-- is SQLite's documented behaviour: with a single min()/max()
|
|
28560
|
+
-- in an aggregate query, every bare column takes its value from
|
|
28561
|
+
-- the row that produced the extremum. So these are the severity
|
|
28562
|
+
-- and category of the definition whose finding is NEWEST, which
|
|
28563
|
+
-- is what the row-based build they replaced read off its first
|
|
28564
|
+
-- (newest-first) row.
|
|
28565
|
+
--
|
|
28566
|
+
-- min() is WRONG here and was the defect: inspection_definitions
|
|
28567
|
+
-- holds one row per rule VERSION (see its writer \u2014 a version bump
|
|
28568
|
+
-- mints a new row), so a rule whose severity moved between
|
|
28569
|
+
-- versions has several, and min() picks the ALPHABETICALLY
|
|
28570
|
+
-- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
|
|
28571
|
+
-- That is arbitrary in direction, and it feeds the badge, the
|
|
28572
|
+
-- filter, the facet counts and the primary sort key.
|
|
28573
|
+
--
|
|
28574
|
+
-- Adding a second min()/max() aggregate here would make these
|
|
28575
|
+
-- bare columns ambiguous again; keep max(latest_at) the only one.
|
|
28576
|
+
severity,
|
|
28577
|
+
category,
|
|
27422
28578
|
sum(tuple_count) AS instance_count,
|
|
27423
28579
|
max(latest_at) AS latest_at,
|
|
27424
28580
|
group_concat(source_tools) AS source_tools,
|
|
@@ -27429,6 +28585,14 @@ var SqliteFindingsRepository = class {
|
|
|
27429
28585
|
group_concat(tool_names) AS tool_names
|
|
27430
28586
|
FROM (
|
|
27431
28587
|
SELECT d.rule_id AS rule_id,
|
|
28588
|
+
-- Severity and category are columns of the DEFINITION, and
|
|
28589
|
+
-- a rule can have SEVERAL definitions (one per version), so
|
|
28590
|
+
-- these are grouped on below and resolved to the newest
|
|
28591
|
+
-- firing version by the outer query's bare-column select.
|
|
28592
|
+
-- They ride the aggregate because the type build has no rows
|
|
28593
|
+
-- to read them off \u2014 see buildFindingTypes.
|
|
28594
|
+
d.severity AS severity,
|
|
28595
|
+
d.category AS category,
|
|
27432
28596
|
e.event_type || '${TUPLE_SEP}' ||
|
|
27433
28597
|
(CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
|
|
27434
28598
|
coalesce(latest.status, '') AS status_tuple,
|
|
@@ -27443,7 +28607,7 @@ var SqliteFindingsRepository = class {
|
|
|
27443
28607
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
27444
28608
|
ON latest.finding_key = f.finding_key
|
|
27445
28609
|
${scope.predicate}
|
|
27446
|
-
GROUP BY d.rule_id, status_tuple
|
|
28610
|
+
GROUP BY d.rule_id, d.severity, d.category, status_tuple
|
|
27447
28611
|
)
|
|
27448
28612
|
GROUP BY rule_id`
|
|
27449
28613
|
).all(scope.params);
|
|
@@ -27452,6 +28616,8 @@ var SqliteFindingsRepository = class {
|
|
|
27452
28616
|
r.rule_id,
|
|
27453
28617
|
{
|
|
27454
28618
|
instanceCount: r.instance_count,
|
|
28619
|
+
severity: r.severity,
|
|
28620
|
+
category: r.category,
|
|
27455
28621
|
sourceTools: splitConcat(r.source_tools),
|
|
27456
28622
|
actionsTaken: splitConcat(r.actions_taken),
|
|
27457
28623
|
statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
|
|
@@ -27468,7 +28634,7 @@ var SqliteFindingsRepository = class {
|
|
|
27468
28634
|
latestDetectedAt: epochMillisToIso(r.latest_at),
|
|
27469
28635
|
// Free text only — joined and substring-matched, so group_concat's
|
|
27470
28636
|
// commas need no unpicking (a repo/path containing one still matches).
|
|
27471
|
-
// Left undefined (not '') when unfetched, so
|
|
28637
|
+
// Left undefined (not '') when unfetched, so buildFindingTypes can
|
|
27472
28638
|
// tell "no q this request" from "a group with no repo/file at all"
|
|
27473
28639
|
// and skip priming a haystack nothing will read.
|
|
27474
28640
|
...withSearchText ? {
|
|
@@ -27496,7 +28662,9 @@ var SqliteFindingsRepository = class {
|
|
|
27496
28662
|
)
|
|
27497
28663
|
);
|
|
27498
28664
|
for (const row of grouped) {
|
|
27499
|
-
if (
|
|
28665
|
+
if (Object.hasOwn(byAction, row.action_taken)) {
|
|
28666
|
+
byAction[row.action_taken] = row.c;
|
|
28667
|
+
}
|
|
27500
28668
|
}
|
|
27501
28669
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
27502
28670
|
const sevRows = allRows(
|
|
@@ -27513,7 +28681,9 @@ var SqliteFindingsRepository = class {
|
|
|
27513
28681
|
)
|
|
27514
28682
|
);
|
|
27515
28683
|
for (const row of sevRows) {
|
|
27516
|
-
if (
|
|
28684
|
+
if (Object.hasOwn(bySeverity, row.severity)) {
|
|
28685
|
+
bySeverity[row.severity] = row.c;
|
|
28686
|
+
}
|
|
27517
28687
|
}
|
|
27518
28688
|
const categories = ENFORCEABLE_CATEGORIES;
|
|
27519
28689
|
const enabledRows = allRows(
|
|
@@ -27562,469 +28732,6 @@ function isoDay(ms) {
|
|
|
27562
28732
|
return new Date(ms).toISOString().slice(0, 10);
|
|
27563
28733
|
}
|
|
27564
28734
|
|
|
27565
|
-
// ../../packages/persistence/src/repositories/history-sync.ts
|
|
27566
|
-
var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
|
|
27567
|
-
var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27568
|
-
var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
|
|
27569
|
-
var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
27570
|
-
var SKIPPED = -1;
|
|
27571
|
-
var ROW_COLUMNS = `id,
|
|
27572
|
-
parent_id AS parentId,
|
|
27573
|
-
root_session_id AS rootSessionId,
|
|
27574
|
-
event_type AS eventType,
|
|
27575
|
-
host_id AS hostId,
|
|
27576
|
-
harness_id AS harnessId,
|
|
27577
|
-
source_project_id AS sourceProjectId,
|
|
27578
|
-
started_at AS startedAt,
|
|
27579
|
-
ended_at AS endedAt,
|
|
27580
|
-
severity,
|
|
27581
|
-
priority,
|
|
27582
|
-
content,
|
|
27583
|
-
content_hash AS contentHash,
|
|
27584
|
-
attributes`;
|
|
27585
|
-
var SqliteHistorySyncRepository = class {
|
|
27586
|
-
constructor(db) {
|
|
27587
|
-
this.db = db;
|
|
27588
|
-
this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
|
|
27589
|
-
this.sessionsStmt = db.prepare(
|
|
27590
|
-
`SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
|
|
27591
|
-
FROM audit_events
|
|
27592
|
-
WHERE synced_at IS NULL
|
|
27593
|
-
AND event_type IN (${TYPE_LIST})
|
|
27594
|
-
AND started_at < :before
|
|
27595
|
-
GROUP BY sessionId
|
|
27596
|
-
ORDER BY earliest
|
|
27597
|
-
LIMIT :limit`
|
|
27598
|
-
);
|
|
27599
|
-
this.rowsStmt = db.prepare(
|
|
27600
|
-
`SELECT ${ROW_COLUMNS}
|
|
27601
|
-
FROM audit_events
|
|
27602
|
-
WHERE synced_at IS NULL
|
|
27603
|
-
AND event_type IN (${TYPE_LIST})
|
|
27604
|
-
AND started_at < :before
|
|
27605
|
-
AND COALESCE(root_session_id, id) = :sessionId
|
|
27606
|
-
ORDER BY (event_type = 'session') DESC, started_at
|
|
27607
|
-
LIMIT :limit`
|
|
27608
|
-
);
|
|
27609
|
-
this.captureRowsStmt = db.prepare(
|
|
27610
|
-
`SELECT ${ROW_COLUMNS}
|
|
27611
|
-
FROM audit_events
|
|
27612
|
-
WHERE synced_at IS NULL
|
|
27613
|
-
AND sync_claimed_at IS NULL
|
|
27614
|
-
AND outbox_owed = 1
|
|
27615
|
-
AND event_type IN (${CAPTURE_TYPE_LIST})
|
|
27616
|
-
AND started_at < :before
|
|
27617
|
-
ORDER BY started_at
|
|
27618
|
-
LIMIT :limit`
|
|
27619
|
-
);
|
|
27620
|
-
this.markOwedStmt = db.prepare(
|
|
27621
|
-
`UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
|
|
27622
|
-
);
|
|
27623
|
-
this.stampStmt = db.prepare(
|
|
27624
|
-
`UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
|
|
27625
|
-
);
|
|
27626
|
-
this.claimRowStmt = db.prepare(
|
|
27627
|
-
`UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
|
|
27628
|
-
);
|
|
27629
|
-
this.releaseRowStmt = db.prepare(
|
|
27630
|
-
`UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
|
|
27631
|
-
);
|
|
27632
|
-
this.releaseStaleClaimsStmt = db.prepare(
|
|
27633
|
-
`UPDATE audit_events SET sync_claimed_at = NULL
|
|
27634
|
-
WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
|
|
27635
|
-
);
|
|
27636
|
-
this.partitionStmt = db.prepare(
|
|
27637
|
-
`SELECT
|
|
27638
|
-
SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
|
|
27639
|
-
SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
|
|
27640
|
-
SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
|
|
27641
|
-
SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
|
|
27642
|
-
COUNT(*) AS total
|
|
27643
|
-
FROM audit_events
|
|
27644
|
-
WHERE event_type IN (${TYPE_LIST})`
|
|
27645
|
-
);
|
|
27646
|
-
this.countsStmt = db.prepare(
|
|
27647
|
-
`SELECT
|
|
27648
|
-
SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
|
|
27649
|
-
SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
|
|
27650
|
-
SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
|
|
27651
|
-
FROM audit_events
|
|
27652
|
-
WHERE event_type IN (${TYPE_LIST})`
|
|
27653
|
-
);
|
|
27654
|
-
this.captureSkipCountStmt = db.prepare(
|
|
27655
|
-
`SELECT COUNT(*) AS skipped
|
|
27656
|
-
FROM audit_events
|
|
27657
|
-
WHERE synced_at = ${String(SKIPPED)}
|
|
27658
|
-
AND event_type IN (${CAPTURE_TYPE_LIST})`
|
|
27659
|
-
);
|
|
27660
|
-
this.fingerprintStmt = db.prepare(
|
|
27661
|
-
`SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
|
|
27662
|
-
FROM history_sync WHERE id = 1`
|
|
27663
|
-
);
|
|
27664
|
-
this.setFingerprintStmt = db.prepare(
|
|
27665
|
-
`UPDATE history_sync
|
|
27666
|
-
SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
|
|
27667
|
-
WHERE id = 1`
|
|
27668
|
-
);
|
|
27669
|
-
this.disownCapturesStmt = db.prepare(
|
|
27670
|
-
`UPDATE audit_events SET outbox_owed = NULL
|
|
27671
|
-
WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
|
|
27672
|
-
);
|
|
27673
|
-
this.rearmStmt = db.prepare(
|
|
27674
|
-
`UPDATE audit_events SET synced_at = NULL
|
|
27675
|
-
WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
|
|
27676
|
-
);
|
|
27677
|
-
this.claimStmt = db.prepare(
|
|
27678
|
-
`UPDATE history_sync
|
|
27679
|
-
SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
|
|
27680
|
-
WHERE id = 1
|
|
27681
|
-
AND (owner_pid IS NULL
|
|
27682
|
-
OR heartbeat_at IS NULL
|
|
27683
|
-
OR heartbeat_at < :staleBefore
|
|
27684
|
-
OR heartbeat_at > :now)`
|
|
27685
|
-
);
|
|
27686
|
-
this.heartbeatStmt = db.prepare(
|
|
27687
|
-
`UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
|
|
27688
|
-
);
|
|
27689
|
-
this.releaseStmt = db.prepare(
|
|
27690
|
-
`UPDATE history_sync
|
|
27691
|
-
SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
|
|
27692
|
-
WHERE id = 1 AND owner_pid = :pid`
|
|
27693
|
-
);
|
|
27694
|
-
this.closeWindowStmt = db.prepare(
|
|
27695
|
-
`UPDATE audit_events SET synced_at = :at
|
|
27696
|
-
WHERE synced_at IS NULL
|
|
27697
|
-
AND event_type IN (${TYPE_LIST})
|
|
27698
|
-
AND started_at >= :attachedAt`
|
|
27699
|
-
);
|
|
27700
|
-
this.releaseBoundaryStmt = db.prepare(
|
|
27701
|
-
`UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
|
|
27702
|
-
);
|
|
27703
|
-
this.freezeBoundaryStmt = db.prepare(
|
|
27704
|
-
`UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
|
|
27705
|
-
);
|
|
27706
|
-
this.leaseStmt = db.prepare(
|
|
27707
|
-
`SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
|
|
27708
|
-
acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
|
|
27709
|
-
FROM history_sync WHERE id = 1`
|
|
27710
|
-
);
|
|
27711
|
-
this.inspectionsStmt = db.prepare(
|
|
27712
|
-
`SELECT d.rule_id AS ruleId,
|
|
27713
|
-
d.name AS ruleName,
|
|
27714
|
-
d.version AS ruleVersion,
|
|
27715
|
-
d.category AS category,
|
|
27716
|
-
d.severity AS severity,
|
|
27717
|
-
f.span_start AS spanStart,
|
|
27718
|
-
f.span_end AS spanEnd,
|
|
27719
|
-
f.masked_match AS maskedMatch,
|
|
27720
|
-
f.action_taken AS actionTaken,
|
|
27721
|
-
f.confidence AS confidence
|
|
27722
|
-
FROM inspection_findings f
|
|
27723
|
-
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
27724
|
-
WHERE f.audit_event_id = :auditEventId
|
|
27725
|
-
ORDER BY f.span_start, f.id`
|
|
27726
|
-
);
|
|
27727
|
-
}
|
|
27728
|
-
db;
|
|
27729
|
-
ensureRowStmt;
|
|
27730
|
-
sessionsStmt;
|
|
27731
|
-
rowsStmt;
|
|
27732
|
-
stampStmt;
|
|
27733
|
-
countsStmt;
|
|
27734
|
-
fingerprintStmt;
|
|
27735
|
-
setFingerprintStmt;
|
|
27736
|
-
rearmStmt;
|
|
27737
|
-
claimStmt;
|
|
27738
|
-
heartbeatStmt;
|
|
27739
|
-
releaseStmt;
|
|
27740
|
-
leaseStmt;
|
|
27741
|
-
inspectionsStmt;
|
|
27742
|
-
closeWindowStmt;
|
|
27743
|
-
releaseBoundaryStmt;
|
|
27744
|
-
freezeBoundaryStmt;
|
|
27745
|
-
captureRowsStmt;
|
|
27746
|
-
markOwedStmt;
|
|
27747
|
-
captureSkipCountStmt;
|
|
27748
|
-
disownCapturesStmt;
|
|
27749
|
-
partitionStmt;
|
|
27750
|
-
claimRowStmt;
|
|
27751
|
-
releaseRowStmt;
|
|
27752
|
-
releaseStaleClaimsStmt;
|
|
27753
|
-
/**
|
|
27754
|
-
* The masked detections recorded against one tool call.
|
|
27755
|
-
*
|
|
27756
|
-
* These travel with the event because a tool call's target is not
|
|
27757
|
-
* re-inspectable from the event alone — unlike a capture, where the text
|
|
27758
|
-
* itself is re-scannable. What crosses is the masked match and the rule that
|
|
27759
|
-
* produced it, never the value.
|
|
27760
|
-
*/
|
|
27761
|
-
inspectionsFor(auditEventId) {
|
|
27762
|
-
return allRows(this.inspectionsStmt, { auditEventId });
|
|
27763
|
-
}
|
|
27764
|
-
/**
|
|
27765
|
-
* Sessions with structural rows still to send, oldest first.
|
|
27766
|
-
*
|
|
27767
|
-
* BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
|
|
27768
|
-
* read. Anything recorded after the machine attached is the live forward
|
|
27769
|
-
* path's to deliver; this drain exists for what was recorded before it, and a
|
|
27770
|
-
* row both paths send is at best a duplicate request and at worst — for a
|
|
27771
|
-
* session root — an overwrite of the inventory ids the live path resolved.
|
|
27772
|
-
*/
|
|
27773
|
-
pendingSessions(limit, before) {
|
|
27774
|
-
return allRows(this.sessionsStmt, { limit, before }).map(
|
|
27775
|
-
(r) => r.sessionId
|
|
27776
|
-
);
|
|
27777
|
-
}
|
|
27778
|
-
/** One session's undelivered structural rows within the backlog, root first. */
|
|
27779
|
-
pendingRows(sessionId, limit, before) {
|
|
27780
|
-
return allRows(this.rowsStmt, { sessionId, limit, before });
|
|
27781
|
-
}
|
|
27782
|
-
/**
|
|
27783
|
-
* Captures this machine still owes the deployment, oldest first.
|
|
27784
|
-
*
|
|
27785
|
-
* Selected by the `outbox_owed` marker the attached forward path writes, not
|
|
27786
|
-
* by a time window — see captureRowsStmt for why a window could not express
|
|
27787
|
-
* this. `before` is the grace window that leaves a just-recorded capture to
|
|
27788
|
-
* the live path.
|
|
27789
|
-
*/
|
|
27790
|
-
pendingCaptureRows(limit, before) {
|
|
27791
|
-
return allRows(this.captureRowsStmt, { limit, before });
|
|
27792
|
-
}
|
|
27793
|
-
/**
|
|
27794
|
-
* Record that a capture is OWED to the deployment.
|
|
27795
|
-
*
|
|
27796
|
-
* Written by the attached forward path when a live send did not confirm
|
|
27797
|
-
* delivery, and read by the drain as the whole of its eligibility test. It is
|
|
27798
|
-
* a fact rather than an inference: the machine was attached, the send did not
|
|
27799
|
-
* land, so the row is owed — which no time window can state, because the same
|
|
27800
|
-
* window that holds the rows a past attachment left owed also holds every
|
|
27801
|
-
* capture recorded while the machine was DETACHED, and those were never
|
|
27802
|
-
* offered to anyone.
|
|
27803
|
-
*
|
|
27804
|
-
* Idempotent, and never un-set: `markSynced` settling the row is what takes it
|
|
27805
|
-
* out of the drain's read.
|
|
27806
|
-
*/
|
|
27807
|
-
markCaptureOwed(id) {
|
|
27808
|
-
this.markOwedStmt.run({ id });
|
|
27809
|
-
}
|
|
27810
|
-
/** Record delivery. Called only AFTER the far side has accepted the rows. */
|
|
27811
|
-
markSynced(ids, atMs) {
|
|
27812
|
-
this.stampAll(ids, atMs);
|
|
27813
|
-
}
|
|
27814
|
-
/**
|
|
27815
|
-
* Record that a row will never be sent.
|
|
27816
|
-
*
|
|
27817
|
-
* Reserved for a local defect — a row that cannot be rebuilt into a valid
|
|
27818
|
-
* payload. A row that merely failed to reach the deployment stays NULL, so it
|
|
27819
|
-
* is retried; marking those would turn one outage into permanent data loss.
|
|
27820
|
-
*/
|
|
27821
|
-
markSkipped(ids) {
|
|
27822
|
-
this.stampAll(ids, SKIPPED);
|
|
27823
|
-
}
|
|
27824
|
-
eachInTransaction(ids, run) {
|
|
27825
|
-
if (ids.length === 0) return;
|
|
27826
|
-
withTransaction(
|
|
27827
|
-
this.db,
|
|
27828
|
-
() => {
|
|
27829
|
-
for (const id of ids) run(id);
|
|
27830
|
-
},
|
|
27831
|
-
"IMMEDIATE"
|
|
27832
|
-
);
|
|
27833
|
-
}
|
|
27834
|
-
stampAll(ids, value) {
|
|
27835
|
-
if (ids.length === 0) return;
|
|
27836
|
-
withTransaction(
|
|
27837
|
-
this.db,
|
|
27838
|
-
() => {
|
|
27839
|
-
for (const id of ids) this.stampStmt.run({ at: value, id });
|
|
27840
|
-
},
|
|
27841
|
-
"IMMEDIATE"
|
|
27842
|
-
);
|
|
27843
|
-
}
|
|
27844
|
-
/**
|
|
27845
|
-
* Claim rows as in-flight.
|
|
27846
|
-
*
|
|
27847
|
-
* Advisory in exactly the sense the lease is: it records that a send is in
|
|
27848
|
-
* progress so a surface can say so, and a lost claim costs a row showing as
|
|
27849
|
-
* queued while it is actually being sent. It is not exclusion — the far side
|
|
27850
|
-
* settles a duplicate on the row id.
|
|
27851
|
-
*/
|
|
27852
|
-
claimRows(ids, atMs) {
|
|
27853
|
-
this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
|
|
27854
|
-
}
|
|
27855
|
-
/** Give back a claim without settling — the send failed, the row is queued again. */
|
|
27856
|
-
releaseRows(ids) {
|
|
27857
|
-
this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
|
|
27858
|
-
}
|
|
27859
|
-
/**
|
|
27860
|
-
* Clear claims older than `staleBefore`, and report how many were cleared.
|
|
27861
|
-
*
|
|
27862
|
-
* A process killed between claiming and settling leaves rows claimed with
|
|
27863
|
-
* nothing left to settle them. Without this they read as "sending" for ever.
|
|
27864
|
-
*/
|
|
27865
|
-
releaseStaleClaims(staleBefore) {
|
|
27866
|
-
return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
|
|
27867
|
-
}
|
|
27868
|
-
/**
|
|
27869
|
-
* Every tracked row in exactly one delivery state.
|
|
27870
|
-
*
|
|
27871
|
-
* Takes no boundary on purpose. The boundary answers "what should the drain
|
|
27872
|
-
* pick up now", which is a different question from "what state is this row
|
|
27873
|
-
* in" — and a machine that has never attached has no boundary to pass, so
|
|
27874
|
-
* requiring one would force a caller to invent one and report the whole store
|
|
27875
|
-
* as queued.
|
|
27876
|
-
*/
|
|
27877
|
-
partition() {
|
|
27878
|
-
const row = getRow(this.partitionStmt, {});
|
|
27879
|
-
return {
|
|
27880
|
-
queued: row?.queued ?? 0,
|
|
27881
|
-
inProgress: row?.inProgress ?? 0,
|
|
27882
|
-
synced: row?.synced ?? 0,
|
|
27883
|
-
failed: row?.failed ?? 0,
|
|
27884
|
-
total: row?.total ?? 0
|
|
27885
|
-
};
|
|
27886
|
-
}
|
|
27887
|
-
/** `pending` counts only what is inside the backlog; sent and skipped are totals. */
|
|
27888
|
-
counts(before) {
|
|
27889
|
-
const row = getRow(
|
|
27890
|
-
this.countsStmt,
|
|
27891
|
-
{ before }
|
|
27892
|
-
);
|
|
27893
|
-
const captures = getRow(this.captureSkipCountStmt);
|
|
27894
|
-
return {
|
|
27895
|
-
pending: row?.pending ?? 0,
|
|
27896
|
-
sent: row?.sent ?? 0,
|
|
27897
|
-
skipped: row?.skipped ?? 0,
|
|
27898
|
-
capturesSkipped: captures?.skipped ?? 0
|
|
27899
|
-
};
|
|
27900
|
-
}
|
|
27901
|
-
/**
|
|
27902
|
-
* The deployment the current stamps were made against, and where its backlog
|
|
27903
|
-
* ends.
|
|
27904
|
-
*
|
|
27905
|
-
* READ-ONLY. An absent row reads as an absent deployment, which is what a
|
|
27906
|
-
* machine that has never drained is — and every writer below seeds the row
|
|
27907
|
-
* before it needs one, so nothing depends on this creating it. Keeping the
|
|
27908
|
-
* write off the gate path matters because the gate runs on every pass while a
|
|
27909
|
-
* write has to take the database's write lock.
|
|
27910
|
-
*/
|
|
27911
|
-
deployment() {
|
|
27912
|
-
const row = getRow(
|
|
27913
|
-
this.fingerprintStmt
|
|
27914
|
-
);
|
|
27915
|
-
return {
|
|
27916
|
-
fingerprint: row?.fingerprint ?? void 0,
|
|
27917
|
-
backlogBefore: row?.backlogBefore ?? void 0
|
|
27918
|
-
};
|
|
27919
|
-
}
|
|
27920
|
-
/**
|
|
27921
|
-
* Point the ledger at a different deployment, discarding what it recorded
|
|
27922
|
-
* about the previous one.
|
|
27923
|
-
*
|
|
27924
|
-
* Delivery is a fact about ONE recipient: rows sent to the deployment a
|
|
27925
|
-
* machine has just left are undelivered as far as the new one is concerned.
|
|
27926
|
-
* All three in one transaction, so a crash between them cannot leave stamps
|
|
27927
|
-
* attributed to the wrong deployment, or a boundary that belongs to another.
|
|
27928
|
-
*
|
|
27929
|
-
* The boundary is written HERE and only here, which is what freezes it: a
|
|
27930
|
-
* re-attach to the SAME deployment (a key rotation) leaves the fingerprint
|
|
27931
|
-
* unchanged, so this never runs and the backlog does not widen back over rows
|
|
27932
|
-
* the live path has since delivered.
|
|
27933
|
-
*/
|
|
27934
|
-
rearmFor(fingerprint, backlogBefore) {
|
|
27935
|
-
this.ensureRowStmt.run();
|
|
27936
|
-
withTransaction(
|
|
27937
|
-
this.db,
|
|
27938
|
-
() => {
|
|
27939
|
-
const previous = getRow(this.fingerprintStmt)?.fingerprint;
|
|
27940
|
-
this.rearmStmt.run();
|
|
27941
|
-
if (previous !== null && previous !== void 0 && previous !== fingerprint) {
|
|
27942
|
-
this.disownCapturesStmt.run();
|
|
27943
|
-
}
|
|
27944
|
-
this.setFingerprintStmt.run({ fingerprint, backlogBefore });
|
|
27945
|
-
},
|
|
27946
|
-
"IMMEDIATE"
|
|
27947
|
-
);
|
|
27948
|
-
}
|
|
27949
|
-
/**
|
|
27950
|
-
* End the attached period: hand its rows to the live path, and release the
|
|
27951
|
-
* boundary so the next attachment can freeze a new one.
|
|
27952
|
-
*
|
|
27953
|
-
* WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
|
|
27954
|
-
* nothing delivers. The fingerprint is unchanged, so the boundary is never
|
|
27955
|
-
* re-frozen and stays at the FIRST attachment — while nothing forwards at all
|
|
27956
|
-
* during the detached period, because the machine is not attached. Rows
|
|
27957
|
-
* recorded in that window sit after the boundary and before the re-attach, so
|
|
27958
|
-
* neither path takes them, and the pending count reports none outstanding.
|
|
27959
|
-
*
|
|
27960
|
-
* Stamping the attached window is not a claim that every one of those rows
|
|
27961
|
-
* reached the deployment — the live path drops on failure and says so
|
|
27962
|
-
* elsewhere. It records that they were ITS to deliver, which is exactly the
|
|
27963
|
-
* status quo: they sit outside the frozen boundary today and are equally never
|
|
27964
|
-
* re-sent. Making it explicit is what lets the boundary move.
|
|
27965
|
-
*
|
|
27966
|
-
* ONE TRANSACTION, so a crash cannot release the boundary while leaving the
|
|
27967
|
-
* window unstamped — that half-state would re-send the whole attached period
|
|
27968
|
-
* on the next attach, which is the failure the boundary exists to prevent.
|
|
27969
|
-
*/
|
|
27970
|
-
closeAttachedWindow(attachedAtMs, atMs) {
|
|
27971
|
-
this.ensureRowStmt.run();
|
|
27972
|
-
withTransaction(
|
|
27973
|
-
this.db,
|
|
27974
|
-
() => {
|
|
27975
|
-
const row = getRow(this.fingerprintStmt);
|
|
27976
|
-
const from = row?.backlogBefore ?? attachedAtMs;
|
|
27977
|
-
this.closeWindowStmt.run({ at: atMs, attachedAt: from });
|
|
27978
|
-
this.releaseBoundaryStmt.run();
|
|
27979
|
-
},
|
|
27980
|
-
"IMMEDIATE"
|
|
27981
|
-
);
|
|
27982
|
-
}
|
|
27983
|
-
/**
|
|
27984
|
-
* Freeze a boundary for the deployment already on file, KEEPING the stamps.
|
|
27985
|
-
*
|
|
27986
|
-
* The re-attach half of the above. Distinct from `rearmFor`, which is for a
|
|
27987
|
-
* different deployment and therefore discards what was delivered to the old
|
|
27988
|
-
* one: here the recipient is the same, so everything already sent to it stays
|
|
27989
|
-
* sent.
|
|
27990
|
-
*/
|
|
27991
|
-
freezeBoundary(backlogBefore) {
|
|
27992
|
-
this.ensureRowStmt.run();
|
|
27993
|
-
this.freezeBoundaryStmt.run({ backlogBefore });
|
|
27994
|
-
}
|
|
27995
|
-
/** Take the claim, or report that someone live already holds it. */
|
|
27996
|
-
claim(pid, host, nowMs, staleAfterMs) {
|
|
27997
|
-
this.ensureRowStmt.run();
|
|
27998
|
-
let taken = false;
|
|
27999
|
-
withTransaction(
|
|
28000
|
-
this.db,
|
|
28001
|
-
() => {
|
|
28002
|
-
const result = this.claimStmt.run({
|
|
28003
|
-
pid,
|
|
28004
|
-
host,
|
|
28005
|
-
now: nowMs,
|
|
28006
|
-
staleBefore: nowMs - staleAfterMs
|
|
28007
|
-
});
|
|
28008
|
-
taken = result.changes === 1;
|
|
28009
|
-
},
|
|
28010
|
-
"IMMEDIATE"
|
|
28011
|
-
);
|
|
28012
|
-
return taken;
|
|
28013
|
-
}
|
|
28014
|
-
/** Say the holder is still alive. A no-op once the claim has moved on. */
|
|
28015
|
-
heartbeat(pid, nowMs) {
|
|
28016
|
-
this.heartbeatStmt.run({ now: nowMs, pid });
|
|
28017
|
-
}
|
|
28018
|
-
/** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
|
|
28019
|
-
release(pid) {
|
|
28020
|
-
this.releaseStmt.run({ pid });
|
|
28021
|
-
}
|
|
28022
|
-
/** Who holds the claim, if anyone. Read-only, for the same reason as above. */
|
|
28023
|
-
lease() {
|
|
28024
|
-
return getRow(this.leaseStmt);
|
|
28025
|
-
}
|
|
28026
|
-
};
|
|
28027
|
-
|
|
28028
28735
|
// ../../packages/persistence/src/repositories/inspection-definitions.ts
|
|
28029
28736
|
var SqliteInspectionDefinitionsRepository = class {
|
|
28030
28737
|
constructor(db) {
|
|
@@ -28215,7 +28922,8 @@ function managedSettingsPaths(platform2 = process.platform) {
|
|
|
28215
28922
|
}
|
|
28216
28923
|
return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
|
|
28217
28924
|
}
|
|
28218
|
-
|
|
28925
|
+
var testOnlyManagedPaths = null;
|
|
28926
|
+
function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
|
|
28219
28927
|
for (const path of paths) {
|
|
28220
28928
|
let text;
|
|
28221
28929
|
try {
|
|
@@ -28250,6 +28958,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
|
|
|
28250
28958
|
if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
|
|
28251
28959
|
if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
|
|
28252
28960
|
if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
|
|
28961
|
+
if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
|
|
28253
28962
|
if (values.vaultConsent !== void 0) {
|
|
28254
28963
|
merged.vaultConsent = values.vaultConsent ? (
|
|
28255
28964
|
// Keep an existing valid grant so its acknowledgedAt survives; mint one
|
|
@@ -30695,7 +31404,7 @@ function toUtcDateString(ms) {
|
|
|
30695
31404
|
return new Date(ms).toISOString().slice(0, 10);
|
|
30696
31405
|
}
|
|
30697
31406
|
function isTimeseriesSeverity(s) {
|
|
30698
|
-
return s === "critical" || s === "high" || s === "medium";
|
|
31407
|
+
return s === "critical" || s === "high" || s === "medium" || s === "low";
|
|
30699
31408
|
}
|
|
30700
31409
|
var SqliteSecurityRepository = class {
|
|
30701
31410
|
constructor(db, now = () => Date.now()) {
|
|
@@ -30757,7 +31466,7 @@ var SqliteSecurityRepository = class {
|
|
|
30757
31466
|
ELSE 0
|
|
30758
31467
|
END) AS open_at_rest
|
|
30759
31468
|
FROM inspection_findings f
|
|
30760
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31469
|
+
JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
|
|
30761
31470
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
30762
31471
|
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
30763
31472
|
ON latest.finding_key = f.finding_key
|
|
@@ -30824,12 +31533,16 @@ var SqliteSecurityRepository = class {
|
|
|
30824
31533
|
const now = this.now();
|
|
30825
31534
|
const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
|
|
30826
31535
|
const rows = this.findingsInRange(windowStart, now);
|
|
30827
|
-
const points = Array.from(
|
|
30828
|
-
|
|
30829
|
-
|
|
30830
|
-
|
|
30831
|
-
|
|
30832
|
-
|
|
31536
|
+
const points = Array.from(
|
|
31537
|
+
{ length: numBuckets },
|
|
31538
|
+
(_, i) => ({
|
|
31539
|
+
timestamp: toUtcDateString(windowStart + i * bucketMs),
|
|
31540
|
+
critical: 0,
|
|
31541
|
+
high: 0,
|
|
31542
|
+
medium: 0,
|
|
31543
|
+
low: 0
|
|
31544
|
+
})
|
|
31545
|
+
);
|
|
30833
31546
|
for (const r of rows) {
|
|
30834
31547
|
const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
|
|
30835
31548
|
const bucket = points[idx];
|
|
@@ -30979,7 +31692,7 @@ var SqliteSecurityRepository = class {
|
|
|
30979
31692
|
this.db.prepare(
|
|
30980
31693
|
`SELECT e.repo AS repo, count(*) AS c
|
|
30981
31694
|
FROM inspection_findings f
|
|
30982
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31695
|
+
JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
|
|
30983
31696
|
WHERE e.started_at >= :from AND e.started_at < :to
|
|
30984
31697
|
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
30985
31698
|
AND e.repo IS NOT NULL
|
|
@@ -31047,6 +31760,7 @@ var SqliteSecurityRepository = class {
|
|
|
31047
31760
|
`SELECT f.finding_key AS finding_key,
|
|
31048
31761
|
d.rule_id AS rule_id,
|
|
31049
31762
|
d.severity AS severity,
|
|
31763
|
+
e.repo AS repo,
|
|
31050
31764
|
e.file_path AS path,
|
|
31051
31765
|
COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
|
|
31052
31766
|
latest.resolved_at AS latest_resolved_at
|
|
@@ -31066,6 +31780,7 @@ var SqliteSecurityRepository = class {
|
|
|
31066
31780
|
const items = rows.map((r) => ({
|
|
31067
31781
|
findingKey: r.finding_key,
|
|
31068
31782
|
ruleId: r.rule_id,
|
|
31783
|
+
repo: r.repo ?? "",
|
|
31069
31784
|
severity: r.severity,
|
|
31070
31785
|
path: r.path ?? "",
|
|
31071
31786
|
resolvedAt: new Date(r.latest_resolved_at).toISOString(),
|
|
@@ -31075,15 +31790,68 @@ var SqliteSecurityRepository = class {
|
|
|
31075
31790
|
}));
|
|
31076
31791
|
return Promise.resolve({ items });
|
|
31077
31792
|
}
|
|
31793
|
+
/**
|
|
31794
|
+
* Per-rule tallies of the findings that are still OPEN, whole-store.
|
|
31795
|
+
*
|
|
31796
|
+
* Scoped by status rather than by time, because the card this feeds is a to-do
|
|
31797
|
+
* list: a secret committed three weeks ago and never rotated is still the most
|
|
31798
|
+
* important thing to fix, and any window hides it. It carried a "newest N
|
|
31799
|
+
* findings" cap and then a range; the first meant a different span on every
|
|
31800
|
+
* machine, and the second reported "no recommendations" over live exposure.
|
|
31801
|
+
*
|
|
31802
|
+
* `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
|
|
31803
|
+
* so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
|
|
31804
|
+
* is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
|
|
31805
|
+
* dismissal is a judgement, not a remediation) and drops untracked legacy rows.
|
|
31806
|
+
* The two answer different questions and only this one has to match a link.
|
|
31807
|
+
*
|
|
31808
|
+
* Aggregated in SQL: the result is O(distinct rule × category × severity), so a
|
|
31809
|
+
* whole-store scope costs a grouped scan rather than a row per finding.
|
|
31810
|
+
*/
|
|
31811
|
+
recommendationInputs() {
|
|
31812
|
+
const rows = allRows(
|
|
31813
|
+
this.db.prepare(
|
|
31814
|
+
`SELECT d.rule_id AS rule_id,
|
|
31815
|
+
d.category AS category,
|
|
31816
|
+
d.severity AS severity,
|
|
31817
|
+
COUNT(*) AS count
|
|
31818
|
+
FROM inspection_findings f
|
|
31819
|
+
JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
|
|
31820
|
+
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31821
|
+
LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
|
|
31822
|
+
ON latest.finding_key = f.finding_key
|
|
31823
|
+
WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
31824
|
+
AND e.event_type = 'code_change'
|
|
31825
|
+
AND (
|
|
31826
|
+
f.finding_key IS NULL
|
|
31827
|
+
OR latest.status IS NULL
|
|
31828
|
+
OR latest.status NOT IN ('resolved', 'dismissed')
|
|
31829
|
+
)
|
|
31830
|
+
GROUP BY d.rule_id, d.category, d.severity`
|
|
31831
|
+
)
|
|
31832
|
+
);
|
|
31833
|
+
return Promise.resolve(
|
|
31834
|
+
rows.map((r) => ({
|
|
31835
|
+
ruleId: r.rule_id,
|
|
31836
|
+
category: r.category,
|
|
31837
|
+
severity: r.severity,
|
|
31838
|
+
count: r.count
|
|
31839
|
+
}))
|
|
31840
|
+
);
|
|
31841
|
+
}
|
|
31078
31842
|
// Findings whose parent event occurred in [fromMs, toMs), with the parent's
|
|
31079
31843
|
// epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
|
|
31080
31844
|
// numeric and the JS aggregations bucket/split on ms directly.
|
|
31081
31845
|
findingsInRange(fromMs, toMs) {
|
|
31082
31846
|
const rows = allRows(
|
|
31083
31847
|
this.db.prepare(
|
|
31084
|
-
`
|
|
31848
|
+
// `rule_id`/`category` cost nothing extra: inspection_definitions is already
|
|
31849
|
+
// joined for `severity`, so they are two more columns off a row this read
|
|
31850
|
+
// already fetches. They feed the recommended-actions rollup.
|
|
31851
|
+
`SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
|
|
31852
|
+
d.rule_id AS rule_id, d.category AS category
|
|
31085
31853
|
FROM inspection_findings f
|
|
31086
|
-
JOIN audit_events e ON e.id = f.audit_event_id
|
|
31854
|
+
JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
|
|
31087
31855
|
JOIN inspection_definitions d ON d.id = f.inspection_definition_id
|
|
31088
31856
|
WHERE e.started_at >= :from AND e.started_at < :to
|
|
31089
31857
|
AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
|
|
@@ -31094,7 +31862,9 @@ var SqliteSecurityRepository = class {
|
|
|
31094
31862
|
return rows.map((r) => ({
|
|
31095
31863
|
occurredAt: r.occurred_at,
|
|
31096
31864
|
severity: r.severity,
|
|
31097
|
-
actionTaken: r.action_taken
|
|
31865
|
+
actionTaken: r.action_taken,
|
|
31866
|
+
ruleId: r.rule_id,
|
|
31867
|
+
category: r.category
|
|
31098
31868
|
}));
|
|
31099
31869
|
}
|
|
31100
31870
|
};
|
|
@@ -31922,6 +32692,7 @@ function openWithPragmas(file2) {
|
|
|
31922
32692
|
db.exec("PRAGMA journal_mode = WAL");
|
|
31923
32693
|
db.exec("PRAGMA busy_timeout = 2000");
|
|
31924
32694
|
db.exec("PRAGMA foreign_keys = ON");
|
|
32695
|
+
registerSqlFunctions(db);
|
|
31925
32696
|
} catch (err) {
|
|
31926
32697
|
closeQuietly(db);
|
|
31927
32698
|
throw err;
|
|
@@ -31951,7 +32722,7 @@ function backupLegacyStore(db, file2) {
|
|
|
31951
32722
|
discardStore(file2, backup);
|
|
31952
32723
|
return backup;
|
|
31953
32724
|
}
|
|
31954
|
-
function openAndInitialize(file2, base) {
|
|
32725
|
+
function openAndInitialize(file2, base, skipTags) {
|
|
31955
32726
|
let db = openWithPragmas(file2);
|
|
31956
32727
|
try {
|
|
31957
32728
|
if (isForeignSqliteLineage(db)) {
|
|
@@ -31961,7 +32732,7 @@ function openAndInitialize(file2, base) {
|
|
|
31961
32732
|
`Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
|
|
31962
32733
|
);
|
|
31963
32734
|
}
|
|
31964
|
-
applyMigrations(db, file2);
|
|
32735
|
+
applyMigrations(db, file2, { skipTags });
|
|
31965
32736
|
tightenPerms(file2);
|
|
31966
32737
|
const policies = new SqlitePoliciesRepository(db);
|
|
31967
32738
|
const installedPacks = new SqliteInstalledPacksRepository(db, base);
|
|
@@ -31976,6 +32747,7 @@ function openAndInitialize(file2, base) {
|
|
|
31976
32747
|
exceptions: new SqliteExceptionsRepository(db),
|
|
31977
32748
|
resolutions: new SqliteResolutionsRepository(db),
|
|
31978
32749
|
ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
|
|
32750
|
+
bodyRetention: new SqliteBodyRetentionRepository(db),
|
|
31979
32751
|
security: new SqliteSecurityRepository(db),
|
|
31980
32752
|
detections: new SqliteDetectionsRepository(db),
|
|
31981
32753
|
shares: new SqliteSharesRepository(db),
|
|
@@ -31998,7 +32770,8 @@ function openAndInitialize(file2, base) {
|
|
|
31998
32770
|
throw err;
|
|
31999
32771
|
}
|
|
32000
32772
|
}
|
|
32001
|
-
|
|
32773
|
+
var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
|
|
32774
|
+
function openLocalDatabase(dir, options = {}) {
|
|
32002
32775
|
ensureDataDirSync(dir);
|
|
32003
32776
|
const file2 = join7(dir, DB_FILENAME);
|
|
32004
32777
|
reapStalePartials(file2);
|
|
@@ -32010,6 +32783,7 @@ function openLocalDatabase(dir) {
|
|
|
32010
32783
|
installedPacks,
|
|
32011
32784
|
scanLedger,
|
|
32012
32785
|
historySync,
|
|
32786
|
+
bodyRetention,
|
|
32013
32787
|
secretVault,
|
|
32014
32788
|
exceptions,
|
|
32015
32789
|
resolutions,
|
|
@@ -32033,7 +32807,8 @@ function openLocalDatabase(dir) {
|
|
|
32033
32807
|
// `dir` is always `<base>/data` — every caller resolves it through
|
|
32034
32808
|
// `dataDir()` — so its parent is the `~/.aka` base the layout splits into
|
|
32035
32809
|
// settings/ and data/, and the pack-policy floor needs both halves.
|
|
32036
|
-
dirname2(dir)
|
|
32810
|
+
dirname2(dir),
|
|
32811
|
+
options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
|
|
32037
32812
|
);
|
|
32038
32813
|
function captureRowId(event) {
|
|
32039
32814
|
return captureId(
|
|
@@ -32226,6 +33001,7 @@ function openLocalDatabase(dir) {
|
|
|
32226
33001
|
installedPacks,
|
|
32227
33002
|
scanLedger,
|
|
32228
33003
|
historySync,
|
|
33004
|
+
bodyRetention,
|
|
32229
33005
|
secretVault,
|
|
32230
33006
|
exceptions,
|
|
32231
33007
|
resolutions,
|
|
@@ -32264,6 +33040,10 @@ function openLocalDatabase(dir) {
|
|
|
32264
33040
|
};
|
|
32265
33041
|
}
|
|
32266
33042
|
|
|
33043
|
+
// ../../packages/persistence/src/egress-wire.ts
|
|
33044
|
+
import { createHash as createHash3 } from "crypto";
|
|
33045
|
+
var SLASH = "/".charCodeAt(0);
|
|
33046
|
+
|
|
32267
33047
|
// ../../packages/persistence/src/exception-policy.ts
|
|
32268
33048
|
var UserGrantPolicyProvider = class {
|
|
32269
33049
|
#exceptions;
|
|
@@ -32285,7 +33065,7 @@ var UserGrantPolicyProvider = class {
|
|
|
32285
33065
|
};
|
|
32286
33066
|
|
|
32287
33067
|
// ../../packages/persistence/src/finding-key.ts
|
|
32288
|
-
import { createHash as
|
|
33068
|
+
import { createHash as createHash4 } from "crypto";
|
|
32289
33069
|
|
|
32290
33070
|
// ../../packages/persistence/src/fingerprint.ts
|
|
32291
33071
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -32410,14 +33190,26 @@ function fingerprintValue(key, raw) {
|
|
|
32410
33190
|
return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
|
|
32411
33191
|
}
|
|
32412
33192
|
|
|
32413
|
-
// ../../packages/persistence/src/
|
|
32414
|
-
import {
|
|
33193
|
+
// ../../packages/persistence/src/forward-health.ts
|
|
33194
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
32415
33195
|
import { join as join9 } from "path";
|
|
33196
|
+
|
|
33197
|
+
// ../../packages/persistence/src/history-backfill.ts
|
|
33198
|
+
import { existsSync as existsSync4 } from "fs";
|
|
33199
|
+
import { join as join10 } from "path";
|
|
33200
|
+
|
|
33201
|
+
// ../../packages/persistence/src/history-preview.ts
|
|
33202
|
+
import { existsSync as existsSync5 } from "fs";
|
|
33203
|
+
import { join as join11 } from "path";
|
|
32416
33204
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
32417
33205
|
|
|
33206
|
+
// ../../packages/persistence/src/history-sync-state.ts
|
|
33207
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
33208
|
+
import { join as join12 } from "path";
|
|
33209
|
+
|
|
32418
33210
|
// ../../packages/persistence/src/store-symlinks.ts
|
|
32419
|
-
import { existsSync as
|
|
32420
|
-
import { dirname as dirname3, join as
|
|
33211
|
+
import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
|
|
33212
|
+
import { dirname as dirname3, join as join13, resolve } from "path";
|
|
32421
33213
|
|
|
32422
33214
|
// ../../packages/persistence/src/vault/crypto.ts
|
|
32423
33215
|
import {
|
|
@@ -32530,8 +33322,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
|
|
|
32530
33322
|
// ../../packages/persistence/src/vault/key-provider.ts
|
|
32531
33323
|
import { execFileSync } from "child_process";
|
|
32532
33324
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
32533
|
-
import { chmodSync as chmodSync3, readFileSync as
|
|
32534
|
-
import { join as
|
|
33325
|
+
import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
33326
|
+
import { join as join14 } from "path";
|
|
32535
33327
|
var VAULT_OCCUPANT_REASON = {
|
|
32536
33328
|
symlink: "the path is a symlink; remove it so a keyring can be created",
|
|
32537
33329
|
gone: "the path was occupied but holds no keyring (removed while it was being created)",
|
|
@@ -32630,7 +33422,7 @@ function claimRotationLock(lock, owner) {
|
|
|
32630
33422
|
throw asError(err);
|
|
32631
33423
|
}
|
|
32632
33424
|
try {
|
|
32633
|
-
writeFileSync3(
|
|
33425
|
+
writeFileSync3(join14(lock, LOCK_OWNER_FILE), `${owner}
|
|
32634
33426
|
`, { mode: DATA_FILE_MODE });
|
|
32635
33427
|
return true;
|
|
32636
33428
|
} catch (err) {
|
|
@@ -32639,7 +33431,7 @@ function claimRotationLock(lock, owner) {
|
|
|
32639
33431
|
}
|
|
32640
33432
|
}
|
|
32641
33433
|
function acquireRotationLock(keysDir2) {
|
|
32642
|
-
const lock =
|
|
33434
|
+
const lock = join14(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
|
|
32643
33435
|
const owner = randomBytes2(16).toString("hex");
|
|
32644
33436
|
if (claimRotationLock(lock, owner)) return { lock, owner };
|
|
32645
33437
|
let held;
|
|
@@ -32666,7 +33458,7 @@ function acquireRotationLock(keysDir2) {
|
|
|
32666
33458
|
}
|
|
32667
33459
|
function releaseRotationLock(lease) {
|
|
32668
33460
|
try {
|
|
32669
|
-
if (
|
|
33461
|
+
if (readFileSync9(join14(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
|
|
32670
33462
|
} catch {
|
|
32671
33463
|
return;
|
|
32672
33464
|
}
|
|
@@ -32687,7 +33479,7 @@ var FileKeyProvider = class {
|
|
|
32687
33479
|
this.#keysDir = keysDir2;
|
|
32688
33480
|
}
|
|
32689
33481
|
get filePath() {
|
|
32690
|
-
return
|
|
33482
|
+
return join14(this.#keysDir, VAULT_KEY_FILENAME);
|
|
32691
33483
|
}
|
|
32692
33484
|
loadOrCreate() {
|
|
32693
33485
|
return asAsync(() => {
|
|
@@ -32717,7 +33509,7 @@ var FileKeyProvider = class {
|
|
|
32717
33509
|
#read() {
|
|
32718
33510
|
let raw;
|
|
32719
33511
|
try {
|
|
32720
|
-
raw =
|
|
33512
|
+
raw = readFileSync9(this.filePath, "utf8");
|
|
32721
33513
|
} catch (err) {
|
|
32722
33514
|
if (err.code === "ENOENT") return null;
|
|
32723
33515
|
throw err instanceof Error ? err : new Error(String(err));
|
|
@@ -33352,8 +34144,8 @@ var SecretVault = class {
|
|
|
33352
34144
|
};
|
|
33353
34145
|
|
|
33354
34146
|
// ../../packages/persistence/src/warn-era-cap.ts
|
|
33355
|
-
import { existsSync as
|
|
33356
|
-
import { join as
|
|
34147
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
34148
|
+
import { join as join15 } from "path";
|
|
33357
34149
|
|
|
33358
34150
|
// ../../packages/plugin-sdk/src/provider-env.ts
|
|
33359
34151
|
var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
|
|
@@ -33407,8 +34199,8 @@ function resolveProvider() {
|
|
|
33407
34199
|
function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
|
|
33408
34200
|
try {
|
|
33409
34201
|
ensureLayoutDirSync(base);
|
|
33410
|
-
const settingsFile =
|
|
33411
|
-
if (
|
|
34202
|
+
const settingsFile = join16(settingsDir(base), "settings.json");
|
|
34203
|
+
if (existsSync8(settingsFile)) tightenFile(settingsFile);
|
|
33412
34204
|
} catch {
|
|
33413
34205
|
}
|
|
33414
34206
|
migrateLegacyLayout(base);
|
|
@@ -33431,9 +34223,9 @@ function resolveProviderSafe(resolveProviderFn) {
|
|
|
33431
34223
|
}
|
|
33432
34224
|
|
|
33433
34225
|
// ../../packages/plugin-sdk/src/config-inventory.ts
|
|
33434
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
34226
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync11, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
|
|
33435
34227
|
import { homedir as homedir2 } from "os";
|
|
33436
|
-
import { basename as basename3, join as
|
|
34228
|
+
import { basename as basename3, join as join18 } from "path";
|
|
33437
34229
|
|
|
33438
34230
|
// ../../packages/detections/src/egress/registry.ts
|
|
33439
34231
|
var EXTRACTOR_VERSION = "1";
|
|
@@ -36391,24 +37183,20 @@ function registerBundledPacks() {
|
|
|
36391
37183
|
}
|
|
36392
37184
|
|
|
36393
37185
|
// ../../packages/plugin-sdk/src/repo.ts
|
|
36394
|
-
import { existsSync as
|
|
36395
|
-
import { basename as basename2, dirname as dirname4, isAbsolute, join as
|
|
37186
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
|
|
37187
|
+
import { basename as basename2, dirname as dirname4, isAbsolute, join as join17, sep as sep2 } from "path";
|
|
36396
37188
|
|
|
36397
37189
|
// ../../packages/plugin-sdk/src/events.ts
|
|
36398
|
-
import { createHash as
|
|
37190
|
+
import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
|
|
36399
37191
|
|
|
36400
37192
|
// ../../packages/plugin-sdk/src/isolated-scan.ts
|
|
36401
|
-
import { existsSync as
|
|
37193
|
+
import { existsSync as existsSync10 } from "fs";
|
|
36402
37194
|
import { fileURLToPath } from "url";
|
|
36403
37195
|
import { Worker } from "worker_threads";
|
|
36404
37196
|
|
|
36405
|
-
// ../../packages/plugin-sdk/src/
|
|
36406
|
-
|
|
36407
|
-
import {
|
|
36408
|
-
import { join as join16 } from "path";
|
|
36409
|
-
|
|
36410
|
-
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
36411
|
-
import { arch, hostname as hostname4, platform, release } from "os";
|
|
37197
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
37198
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
37199
|
+
import { join as join20 } from "path";
|
|
36412
37200
|
|
|
36413
37201
|
// ../../packages/plugin-sdk/src/model-governance.ts
|
|
36414
37202
|
import {
|
|
@@ -36416,24 +37204,50 @@ import {
|
|
|
36416
37204
|
fstatSync,
|
|
36417
37205
|
mkdirSync as mkdirSync2,
|
|
36418
37206
|
openSync as openSync2,
|
|
36419
|
-
readFileSync as
|
|
37207
|
+
readFileSync as readFileSync12,
|
|
36420
37208
|
readSync,
|
|
36421
37209
|
writeFileSync as writeFileSync5
|
|
36422
37210
|
} from "fs";
|
|
36423
|
-
import { join as
|
|
37211
|
+
import { join as join19 } from "path";
|
|
36424
37212
|
var TAIL_BYTES = 256 * 1024;
|
|
36425
37213
|
|
|
37214
|
+
// ../../packages/plugin-sdk/src/host-floor.ts
|
|
37215
|
+
var HOST_FEATURE = {
|
|
37216
|
+
ModelSwitch: "model-switch",
|
|
37217
|
+
VaultPointerDisplay: "vault-pointer-display"
|
|
37218
|
+
};
|
|
37219
|
+
var HOST_FLOORS = {
|
|
37220
|
+
[HOST_FEATURE.ModelSwitch]: {
|
|
37221
|
+
label: "model-switch protection",
|
|
37222
|
+
hookEvents: ["PreModelSwitch", "PostModelSwitch"],
|
|
37223
|
+
since: "2.1.251"
|
|
37224
|
+
},
|
|
37225
|
+
[HOST_FEATURE.VaultPointerDisplay]: {
|
|
37226
|
+
label: "vault pointer display",
|
|
37227
|
+
hookEvents: ["MessageDisplay"],
|
|
37228
|
+
since: "2.1.152"
|
|
37229
|
+
}
|
|
37230
|
+
};
|
|
37231
|
+
|
|
37232
|
+
// ../../packages/plugin-sdk/src/ignore-layers.ts
|
|
37233
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
37234
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
37235
|
+
import { join as join21 } from "path";
|
|
37236
|
+
|
|
37237
|
+
// ../../packages/plugin-sdk/src/inventory-resolver.ts
|
|
37238
|
+
import { arch, hostname as hostname4, platform, release } from "os";
|
|
37239
|
+
|
|
36426
37240
|
// ../../packages/plugin-sdk/src/nudge.ts
|
|
36427
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
36428
|
-
import { join as
|
|
37241
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
|
|
37242
|
+
import { join as join22 } from "path";
|
|
36429
37243
|
|
|
36430
37244
|
// ../../packages/plugin-sdk/src/paths.ts
|
|
36431
37245
|
import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
|
|
36432
37246
|
import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
|
|
36433
37247
|
|
|
36434
37248
|
// ../../packages/plugin-sdk/src/project-files.ts
|
|
36435
|
-
import { existsSync as
|
|
36436
|
-
import { basename as basename5, join as
|
|
37249
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
|
|
37250
|
+
import { basename as basename5, join as join23 } from "path";
|
|
36437
37251
|
|
|
36438
37252
|
// ../../packages/plugin-sdk/src/provider-env-antigravity.ts
|
|
36439
37253
|
var optionalBaseUrl2 = external_exports.preprocess((v) => {
|
|
@@ -36469,7 +37283,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
36469
37283
|
|
|
36470
37284
|
// ../../packages/plugin-sdk/src/throttle.ts
|
|
36471
37285
|
import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
36472
|
-
import { join as
|
|
37286
|
+
import { join as join24 } from "path";
|
|
36473
37287
|
|
|
36474
37288
|
// ../../packages/plugin-sdk/src/tokenize.ts
|
|
36475
37289
|
function redactedPlaceholder(category) {
|
|
@@ -36799,13 +37613,13 @@ function describePointerSafe(token) {
|
|
|
36799
37613
|
import {
|
|
36800
37614
|
mkdirSync as mkdirSync5,
|
|
36801
37615
|
readdirSync as readdirSync5,
|
|
36802
|
-
readFileSync as
|
|
37616
|
+
readFileSync as readFileSync16,
|
|
36803
37617
|
renameSync as renameSync5,
|
|
36804
37618
|
rmSync as rmSync7,
|
|
36805
37619
|
statSync as statSync9,
|
|
36806
37620
|
writeFileSync as writeFileSync8
|
|
36807
37621
|
} from "fs";
|
|
36808
|
-
import { dirname as dirname6, join as
|
|
37622
|
+
import { dirname as dirname6, join as join25 } from "path";
|
|
36809
37623
|
var EMPTY_CARRY = Object.freeze({
|
|
36810
37624
|
tail: "",
|
|
36811
37625
|
fence: null,
|
|
@@ -37002,7 +37816,7 @@ var CARRY_FILE_PREFIX = "display-carry";
|
|
|
37002
37816
|
var STALE_CARRY_MS = 15 * 60 * 1e3;
|
|
37003
37817
|
function carryFilePath(dataDir2, sessionId) {
|
|
37004
37818
|
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
37005
|
-
return
|
|
37819
|
+
return join25(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
|
|
37006
37820
|
}
|
|
37007
37821
|
function parseFence(value) {
|
|
37008
37822
|
if (typeof value !== "object" || value === null) return null;
|
|
@@ -37013,7 +37827,7 @@ function parseFence(value) {
|
|
|
37013
37827
|
}
|
|
37014
37828
|
function loadCarry(file2, keys) {
|
|
37015
37829
|
try {
|
|
37016
|
-
const parsed = JSON.parse(
|
|
37830
|
+
const parsed = JSON.parse(readFileSync16(file2, "utf8"));
|
|
37017
37831
|
if (typeof parsed !== "object" || parsed === null) return EMPTY_CARRY;
|
|
37018
37832
|
const record2 = parsed;
|
|
37019
37833
|
const revealedCount = record2.messageKey === keys.messageKey && typeof record2.revealedCount === "number" && Number.isFinite(record2.revealedCount) ? record2.revealedCount : 0;
|
|
@@ -37066,7 +37880,7 @@ function removeStaleCarryFiles(dir, keep) {
|
|
|
37066
37880
|
const cutoff = Date.now() - STALE_CARRY_MS;
|
|
37067
37881
|
for (const name of readdirSync5(dir)) {
|
|
37068
37882
|
if (!name.startsWith(CARRY_FILE_PREFIX) || !name.endsWith(".json")) continue;
|
|
37069
|
-
const path =
|
|
37883
|
+
const path = join25(dir, name);
|
|
37070
37884
|
if (path === keep) continue;
|
|
37071
37885
|
try {
|
|
37072
37886
|
if (statSync9(path).mtimeMs < cutoff) rmSync7(path, { force: true });
|
|
@@ -37097,7 +37911,7 @@ function finalizeCarry(file2, keys, carry) {
|
|
|
37097
37911
|
}
|
|
37098
37912
|
let owned = true;
|
|
37099
37913
|
try {
|
|
37100
|
-
const parsed = JSON.parse(
|
|
37914
|
+
const parsed = JSON.parse(readFileSync16(file2, "utf8"));
|
|
37101
37915
|
if (typeof parsed === "object" && parsed !== null) {
|
|
37102
37916
|
const record2 = parsed;
|
|
37103
37917
|
owned = record2.blockKey === keys.blockKey || record2.messageKey === keys.messageKey;
|