@bbradar/mcp 0.1.20 → 0.2.1
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/README.md +44 -8
- package/benchmark/mcp-benchmark.mjs +167 -0
- package/dist/bbradarClient.d.ts +51 -2
- package/dist/bbradarClient.js +331 -36
- package/dist/bbradarClient.js.map +1 -1
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +16 -5
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +48 -8
- package/dist/config.js.map +1 -1
- package/dist/cursorStore.d.ts +33 -0
- package/dist/cursorStore.js +123 -0
- package/dist/cursorStore.js.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +164 -7
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +917 -64
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
5
|
+
import { NumericCursorStore } from "./cursorStore.js";
|
|
5
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
6
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
7
|
-
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
8
|
-
const SERVER_VERSION = "0.1
|
|
8
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
+
const SERVER_VERSION = "0.2.1";
|
|
9
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
10
11
|
const MAX_TARGET_EXPORT = 500;
|
|
11
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -29,10 +30,13 @@ const MAX_FILTER_DISCOVERY_PAGES = 5;
|
|
|
29
30
|
const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
|
|
30
31
|
const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
|
|
31
32
|
const MAX_LOCAL_EXPORT_RESOURCES = 25;
|
|
33
|
+
const MAX_LOCAL_EXPORT_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
const LOCAL_EXPORT_TTL_MS = 10 * 60 * 1000;
|
|
32
35
|
const EXPORT_PREVIEW_LIMIT = 25;
|
|
33
36
|
const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
|
|
34
37
|
const STALE_PROGRAM_ID_RESOLVE_BUDGET = 12;
|
|
35
38
|
const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
|
|
39
|
+
const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
36
40
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
37
41
|
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
38
42
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
@@ -45,6 +49,8 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
45
49
|
"get_program_eligible_targets_by_name",
|
|
46
50
|
"get_program_targets",
|
|
47
51
|
"get_recent_changes",
|
|
52
|
+
"get_repository_changes",
|
|
53
|
+
"find_repository_hunt_targets",
|
|
48
54
|
"get_latest_added_targets",
|
|
49
55
|
"get_opportunities",
|
|
50
56
|
"find_programs",
|
|
@@ -54,6 +60,12 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
54
60
|
"get_mcp_status",
|
|
55
61
|
"export_targets"
|
|
56
62
|
]);
|
|
63
|
+
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
64
|
+
get_repository_changes: "repository_changes",
|
|
65
|
+
find_repository_hunt_targets: "repository_changes",
|
|
66
|
+
export_targets: "target_export",
|
|
67
|
+
get_opportunities: "opportunities"
|
|
68
|
+
};
|
|
57
69
|
const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
58
70
|
"HackerOne",
|
|
59
71
|
"BugCrowd",
|
|
@@ -159,6 +171,7 @@ const TARGET_QUERY_STOP_WORDS = new Set([
|
|
|
159
171
|
]);
|
|
160
172
|
const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
|
|
161
173
|
const changeListModeSchema = z.enum(["compact", "full"]);
|
|
174
|
+
const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
|
|
162
175
|
const programListModeSchema = z.enum(["compact", "full"]);
|
|
163
176
|
const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
|
|
164
177
|
const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
|
|
@@ -227,6 +240,8 @@ const pageSchema = z.number().int().min(1).default(1);
|
|
|
227
240
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
228
241
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
229
242
|
const opportunityLevelSchema = z.enum(["elite", "hot", "strong", "potential"]);
|
|
243
|
+
const targetOpportunityLabelSchema = z.enum(["Elite Target", "Strong Target", "Potential Target", "Normal"]);
|
|
244
|
+
const competitionRiskSchema = z.enum(["Low", "Medium", "High", "Unknown"]);
|
|
230
245
|
const programSortSchema = z
|
|
231
246
|
.enum([
|
|
232
247
|
"best_match",
|
|
@@ -278,6 +293,7 @@ function toolTimingOutputSchema() {
|
|
|
278
293
|
function cacheOutputSchema() {
|
|
279
294
|
return z.object({
|
|
280
295
|
hit: z.boolean(),
|
|
296
|
+
stale_while_revalidate: z.boolean().optional(),
|
|
281
297
|
coalesced_live_request: z.boolean().optional(),
|
|
282
298
|
expires_at: z.string().optional()
|
|
283
299
|
});
|
|
@@ -364,10 +380,12 @@ const programCandidatePresetInputSchema = {
|
|
|
364
380
|
output_mode: programListModeSchema.default("compact"),
|
|
365
381
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
|
|
366
382
|
};
|
|
367
|
-
function createProfiledToolRegistrar(server, profile) {
|
|
383
|
+
function createProfiledToolRegistrar(server, profile, client) {
|
|
368
384
|
return (name, toolConfig, callback) => {
|
|
369
385
|
const registeredTool = server.registerTool(name, toolConfig, callback);
|
|
370
|
-
|
|
386
|
+
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
387
|
+
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
388
|
+
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
371
389
|
registeredTool.disable();
|
|
372
390
|
}
|
|
373
391
|
return registeredTool;
|
|
@@ -391,7 +409,9 @@ export function createBbradarServer(client, config) {
|
|
|
391
409
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
392
410
|
"Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
|
|
393
411
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
412
|
+
"Self-Hosted programs use Self-Hosted:slug ids and are available through program search, exact detail, and target tools. Respect data_capabilities; do not infer missing opportunity, report-count, target-change, submission-cost, or repository signals.",
|
|
394
413
|
"Freshness: find_recently_updated_programs, get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
|
|
414
|
+
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
395
415
|
"Discovery: find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, find_low_noise_programs.",
|
|
396
416
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
397
417
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -402,13 +422,26 @@ export function createBbradarServer(client, config) {
|
|
|
402
422
|
}, {
|
|
403
423
|
instructions: instructions.join("\n")
|
|
404
424
|
});
|
|
405
|
-
const registerTool = createProfiledToolRegistrar(server, config.toolProfile);
|
|
425
|
+
const registerTool = createProfiledToolRegistrar(server, config.toolProfile, client);
|
|
406
426
|
const rateLimiter = new FixedWindowRateLimiter();
|
|
407
427
|
const exportStore = new Map();
|
|
428
|
+
const continuationStore = new Map();
|
|
429
|
+
const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
|
|
430
|
+
const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
|
|
431
|
+
const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
432
|
+
const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
433
|
+
const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
434
|
+
const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
|
|
435
|
+
const maxTargetExportProgramIds = client.endpointNumber("target_export", "max_program_ids", 50);
|
|
436
|
+
const programsPageSizeSchema = z.number().int().min(1).max(maxProgramsPageSize).default(Math.min(20, maxProgramsPageSize));
|
|
437
|
+
const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
|
|
438
|
+
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
439
|
+
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
408
440
|
registerTool("search_programs", {
|
|
409
441
|
title: "Search BBRadar Programs",
|
|
410
|
-
description: `
|
|
442
|
+
description: `Search or list active platform and Self-Hosted programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
|
|
411
443
|
inputSchema: {
|
|
444
|
+
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
412
445
|
platforms: platformListSchema(),
|
|
413
446
|
tags: tagListSchema(),
|
|
414
447
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
@@ -505,8 +538,8 @@ export function createBbradarServer(client, config) {
|
|
|
505
538
|
language_tags: languageTagListSchema(),
|
|
506
539
|
include_out_of_scope: z.boolean().default(false),
|
|
507
540
|
include_ineligible: z.boolean().default(false),
|
|
508
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
509
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
541
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
542
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
510
543
|
group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
|
|
511
544
|
target_list_mode: targetListModeSchema.default("identifiers"),
|
|
512
545
|
max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
@@ -549,6 +582,7 @@ export function createBbradarServer(client, config) {
|
|
|
549
582
|
resolved_program: jsonRecordSchema().optional(),
|
|
550
583
|
program_id: z.string().optional(),
|
|
551
584
|
targets: z.array(z.unknown()).optional(),
|
|
585
|
+
continuation: jsonRecordSchema().optional(),
|
|
552
586
|
warnings: z.array(z.string()).optional(),
|
|
553
587
|
upstream_budget: jsonRecordSchema().optional(),
|
|
554
588
|
ranking_scope: jsonRecordSchema().optional(),
|
|
@@ -558,14 +592,19 @@ export function createBbradarServer(client, config) {
|
|
|
558
592
|
}, (args) => runTool("get_program_eligible_targets_by_name", config, rateLimiter, async () => getProgramEligibleTargetsByName(client, config, args)));
|
|
559
593
|
registerTool("get_program_targets", {
|
|
560
594
|
title: "Get Program Targets",
|
|
561
|
-
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${
|
|
595
|
+
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${maxProgramTargetsPageSize}.`,
|
|
562
596
|
inputSchema: {
|
|
563
597
|
program_id: programIdentifierSchema,
|
|
598
|
+
search: searchTextSchema.optional(),
|
|
599
|
+
target_type: filterValueSchema().optional(),
|
|
600
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
601
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
602
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
564
603
|
include_out_of_scope: z.boolean().default(false),
|
|
565
604
|
include_ineligible: z.boolean().default(false),
|
|
566
605
|
strict_scope_filter: z.boolean().default(true),
|
|
567
606
|
offset: z.number().int().min(0).default(0),
|
|
568
|
-
limit: z.number().int().min(1).max(
|
|
607
|
+
limit: z.number().int().min(1).max(maxProgramTargetsPageSize).default(Math.min(100, maxProgramTargetsPageSize)),
|
|
569
608
|
output_mode: targetListModeSchema.default("compact")
|
|
570
609
|
},
|
|
571
610
|
outputSchema: {
|
|
@@ -576,13 +615,21 @@ export function createBbradarServer(client, config) {
|
|
|
576
615
|
},
|
|
577
616
|
annotations: readOnlyAnnotations
|
|
578
617
|
}, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
|
|
579
|
-
|
|
618
|
+
const payload = await runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
|
|
619
|
+
return applyCollectionContinuation(payload, "targets", "program targets", continuationStore, config);
|
|
580
620
|
}));
|
|
581
621
|
registerTool("get_recent_changes", {
|
|
582
622
|
title: "Get Recent Target Changes",
|
|
583
623
|
description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
|
|
584
624
|
inputSchema: {
|
|
585
625
|
change_type: z.enum(["added", "updated", "removed"]).optional(),
|
|
626
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
|
|
627
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
628
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
629
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
630
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
631
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
632
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
586
633
|
include_removed: z.boolean().default(false),
|
|
587
634
|
include_ineligible: z.boolean().default(false),
|
|
588
635
|
include_out_of_scope: z.boolean().default(false),
|
|
@@ -597,26 +644,148 @@ export function createBbradarServer(client, config) {
|
|
|
597
644
|
...apiEnvelopeOutputShape(),
|
|
598
645
|
changes: z.array(jsonRecordSchema()).optional(),
|
|
599
646
|
meta: jsonRecordSchema().optional(),
|
|
600
|
-
facets: jsonRecordSchema().optional()
|
|
647
|
+
facets: jsonRecordSchema().optional(),
|
|
648
|
+
cursor: jsonRecordSchema().optional(),
|
|
649
|
+
continuation: jsonRecordSchema().optional()
|
|
601
650
|
},
|
|
602
651
|
annotations: readOnlyAnnotations
|
|
603
652
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
604
|
-
const { output_mode, ...filters } = args;
|
|
605
|
-
const
|
|
606
|
-
const
|
|
653
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
654
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
655
|
+
const { min_target_score, target_opportunity_label, competition_risk, ...upstreamFilters } = filters;
|
|
656
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRecentChanges(toQuery({
|
|
657
|
+
...upstreamFilters,
|
|
658
|
+
minTargetScore: min_target_score,
|
|
659
|
+
targetOpportunityLabel: target_opportunity_label,
|
|
660
|
+
competitionRisk: competition_risk,
|
|
661
|
+
after_id: afterId,
|
|
662
|
+
page: afterId === undefined ? filters.page : 1
|
|
663
|
+
})));
|
|
664
|
+
const api = collected.lastApi;
|
|
665
|
+
const data = collected.lastData;
|
|
607
666
|
const sanitizedMeta = readObject(sanitizeJson(data?.meta));
|
|
608
|
-
const changes =
|
|
667
|
+
const changes = collected.results
|
|
609
668
|
.map((change) => sanitizeChange(change, config.webBaseUrl))
|
|
610
669
|
.map((change) => formatChange(change, output_mode));
|
|
611
|
-
return withApiMetadata(api, {
|
|
670
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
612
671
|
changes,
|
|
613
672
|
meta: stripUndefined({
|
|
614
673
|
...(sanitizedMeta ?? {}),
|
|
615
|
-
output_mode
|
|
674
|
+
output_mode,
|
|
675
|
+
pages_fetched: collected.pagesFetched,
|
|
676
|
+
returned_across_pages: changes.length,
|
|
677
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
678
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
616
679
|
}),
|
|
617
|
-
facets: sanitizeJson(data?.facets)
|
|
618
|
-
|
|
680
|
+
facets: sanitizeJson(data?.facets),
|
|
681
|
+
cursor: collected.cursor,
|
|
682
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
683
|
+
}), "changes", "target changes", continuationStore, config);
|
|
684
|
+
}));
|
|
685
|
+
registerTool("get_repository_changes", {
|
|
686
|
+
title: "Get Repository Changes",
|
|
687
|
+
description: "Read-only repository push/change feed for direct inspection or polling. Defaults to lightweight program summaries; use after_id for efficient incremental polling. Use find_repository_hunt_targets instead when reward or target-quality ranking is also required.",
|
|
688
|
+
inputSchema: {
|
|
689
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
690
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
691
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
692
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
693
|
+
include_targets: z.boolean().default(false).describe("Include matched targets. False is faster and returns affected programs only."),
|
|
694
|
+
search: searchTextSchema.optional(),
|
|
695
|
+
platforms: platformListSchema(),
|
|
696
|
+
tags: tagListSchema(),
|
|
697
|
+
free_submission_only: z.boolean().default(false),
|
|
698
|
+
include_removed: z.boolean().default(false),
|
|
699
|
+
include_ineligible: z.boolean().default(false),
|
|
700
|
+
include_out_of_scope: z.boolean().default(false),
|
|
701
|
+
page: pageSchema,
|
|
702
|
+
page_size: repositoryChangesPageSizeSchema,
|
|
703
|
+
output_mode: repositoryChangeListModeSchema.default("compact")
|
|
704
|
+
},
|
|
705
|
+
outputSchema: {
|
|
706
|
+
...apiEnvelopeOutputShape(),
|
|
707
|
+
repository_changes: z.array(jsonRecordSchema()).optional(),
|
|
708
|
+
meta: jsonRecordSchema().optional(),
|
|
709
|
+
facets: jsonRecordSchema().optional(),
|
|
710
|
+
available_facets: jsonRecordSchema().optional(),
|
|
711
|
+
cursor: jsonRecordSchema().optional(),
|
|
712
|
+
continuation: jsonRecordSchema().optional()
|
|
713
|
+
},
|
|
714
|
+
annotations: readOnlyAnnotations
|
|
715
|
+
}, (args) => runTool("get_repository_changes", config, rateLimiter, async () => {
|
|
716
|
+
if (!client.supportsEndpoint("repository_changes")) {
|
|
717
|
+
throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
|
|
718
|
+
}
|
|
719
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
720
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
721
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRepositoryChanges(toQuery({
|
|
722
|
+
...filters,
|
|
723
|
+
after_id: afterId,
|
|
724
|
+
page: afterId === undefined ? filters.page : 1
|
|
725
|
+
})));
|
|
726
|
+
const api = collected.lastApi;
|
|
727
|
+
const data = collected.lastData;
|
|
728
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
729
|
+
repository_changes: collected.results.map((change) => sanitizeRepositoryChange(change, config.webBaseUrl)).map((change) => formatRepositoryChange(change, output_mode)),
|
|
730
|
+
meta: stripUndefined({
|
|
731
|
+
...(readObject(sanitizeJson(data?.meta)) ?? {}),
|
|
732
|
+
output_mode,
|
|
733
|
+
pages_fetched: collected.pagesFetched,
|
|
734
|
+
returned_across_pages: collected.results.length,
|
|
735
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
736
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
737
|
+
}),
|
|
738
|
+
facets: sanitizeJson(data?.facets),
|
|
739
|
+
available_facets: sanitizeJson(data?.available_facets),
|
|
740
|
+
cursor: collected.cursor,
|
|
741
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
742
|
+
}), "repository_changes", "repository changes", continuationStore, config);
|
|
619
743
|
}));
|
|
744
|
+
registerTool("find_repository_hunt_targets", {
|
|
745
|
+
title: "Find Repository-Aware Hunt Targets",
|
|
746
|
+
description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary platforms, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria.",
|
|
747
|
+
inputSchema: {
|
|
748
|
+
platforms: platformListSchema(),
|
|
749
|
+
exclude_platforms: stringListSchema("Platforms to exclude from final matches."),
|
|
750
|
+
tags: tagListSchema(),
|
|
751
|
+
scope_tags: scopeTagListSchema(),
|
|
752
|
+
target_types: targetTypeListSchema(),
|
|
753
|
+
language_tags: languageTagListSchema(),
|
|
754
|
+
surface_mode: z.enum(["any", "web2", "web3"]).default("any"),
|
|
755
|
+
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
756
|
+
min_bounty_max: z.number().int().min(0).optional(),
|
|
757
|
+
require_bounty: z.boolean().default(true),
|
|
758
|
+
max_public_report_count: z.number().int().min(0).optional(),
|
|
759
|
+
include_unknown_report_count: z.boolean().default(true),
|
|
760
|
+
min_eligible_targets: z.number().int().min(0).default(1),
|
|
761
|
+
min_total_targets: z.number().int().min(0).optional(),
|
|
762
|
+
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
763
|
+
min_added_7d: z.number().int().min(0).optional(),
|
|
764
|
+
changed_since: isoDateTimeSchema.optional(),
|
|
765
|
+
free_submission_only: z.boolean().default(false),
|
|
766
|
+
min_target_score: z.number().int().min(0).max(100).default(60),
|
|
767
|
+
target_opportunity_labels: z.array(targetOpportunityLabelSchema).max(4).default([]),
|
|
768
|
+
competition_risks: z.array(competitionRiskSchema).max(4).default([]),
|
|
769
|
+
max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(3),
|
|
770
|
+
max_program_candidates: z.number().int().min(1).max(MAX_FIND_PROGRAM_RESULTS).default(MAX_FIND_PROGRAM_RESULTS),
|
|
771
|
+
repository_page_size: z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(100, maxRepositoryChangesPageSize)),
|
|
772
|
+
max_results: z.number().int().min(1).max(25).default(10),
|
|
773
|
+
max_targets_per_result: z.number().int().min(1).max(20).default(5),
|
|
774
|
+
ranking_mode: z.enum(["balanced", "highest_reward", "best_target", "newest"]).default("balanced"),
|
|
775
|
+
output_mode: repositoryChangeListModeSchema.default("compact"),
|
|
776
|
+
upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(10)
|
|
777
|
+
},
|
|
778
|
+
outputSchema: {
|
|
779
|
+
...apiEnvelopeOutputShape(),
|
|
780
|
+
matches: z.array(jsonRecordSchema()).optional(),
|
|
781
|
+
programs: z.array(jsonRecordSchema()).optional(),
|
|
782
|
+
filters: jsonRecordSchema().optional(),
|
|
783
|
+
upstream_budget: jsonRecordSchema().optional(),
|
|
784
|
+
ranking_scope: jsonRecordSchema().optional(),
|
|
785
|
+
meta: jsonRecordSchema().optional()
|
|
786
|
+
},
|
|
787
|
+
annotations: readOnlyAnnotations
|
|
788
|
+
}, (args) => runTool("find_repository_hunt_targets", config, rateLimiter, async () => findRepositoryHuntTargets(client, config, args)));
|
|
620
789
|
registerTool("get_latest_added_targets", {
|
|
621
790
|
title: "Get Latest Added Targets",
|
|
622
791
|
description: "Latest added targets by type with optional platform, reward, and report-count filters. For fast wildcard discovery use target_type=wildcard and include_full_target_list=false.",
|
|
@@ -630,7 +799,7 @@ export function createBbradarServer(client, config) {
|
|
|
630
799
|
include_unknown_report_count: z.boolean().default(true),
|
|
631
800
|
include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
|
|
632
801
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
633
|
-
recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
802
|
+
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
634
803
|
max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
|
|
635
804
|
include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program's complete target list. Set false for the fastest cross-program discovery."),
|
|
636
805
|
full_target_list_mode: targetListModeSchema.default("identifiers"),
|
|
@@ -701,8 +870,8 @@ export function createBbradarServer(client, config) {
|
|
|
701
870
|
include_removed: z.boolean().default(true),
|
|
702
871
|
include_out_of_scope: z.boolean().default(false),
|
|
703
872
|
include_ineligible: z.boolean().default(false),
|
|
704
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
705
|
-
max_targets: acceptedTargetLimitSchema.default(50),
|
|
873
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
874
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(50, maxProgramTargetsPageSize)),
|
|
706
875
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
707
876
|
},
|
|
708
877
|
outputSchema: {
|
|
@@ -728,7 +897,7 @@ export function createBbradarServer(client, config) {
|
|
|
728
897
|
platforms: platformListSchema(),
|
|
729
898
|
tags: tagListSchema(),
|
|
730
899
|
language_tags: languageTagListSchema(),
|
|
731
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
900
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
732
901
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
733
902
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
734
903
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -749,8 +918,8 @@ export function createBbradarServer(client, config) {
|
|
|
749
918
|
since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
750
919
|
target_type: filterValueSchema().optional(),
|
|
751
920
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
752
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
753
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
921
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
922
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
754
923
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
755
924
|
},
|
|
756
925
|
outputSchema: {
|
|
@@ -774,7 +943,7 @@ export function createBbradarServer(client, config) {
|
|
|
774
943
|
since: isoDateTimeSchema.optional(),
|
|
775
944
|
target_type: filterValueSchema().optional(),
|
|
776
945
|
include_ineligible: z.boolean().default(false),
|
|
777
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
946
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
778
947
|
max_targets_per_program: z.number().int().min(1).max(50).default(10),
|
|
779
948
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
780
949
|
},
|
|
@@ -820,14 +989,16 @@ export function createBbradarServer(client, config) {
|
|
|
820
989
|
}));
|
|
821
990
|
registerTool("find_programs", {
|
|
822
991
|
title: "Find BBRadar Programs",
|
|
823
|
-
description: "Ranked program discovery with filters and compact output.",
|
|
992
|
+
description: "Ranked program discovery with positive and negative platform/surface filters and compact output. Strict-filter retries reuse cached upstream pages, so relax one local filter at a time when a narrow query returns no candidates.",
|
|
824
993
|
inputSchema: {
|
|
825
994
|
platforms: platformListSchema(),
|
|
995
|
+
exclude_platforms: stringListSchema("Platforms to exclude after retrieval. Use list_filters first when the user asks for a geographic/platform exclusion without naming exact platform values."),
|
|
826
996
|
tags: tagListSchema(),
|
|
827
997
|
scope_tags: scopeTagListSchema(),
|
|
828
998
|
target_types: targetTypeListSchema(),
|
|
829
999
|
language_tags: languageTagListSchema(),
|
|
830
1000
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
1001
|
+
exclude_web3: z.boolean().default(false).describe("Exclude programs with Web3, blockchain, smart-contract, crypto, DeFi, NFT, Solidity, Ethereum, or Solana signals. Use this for Web2-only discovery."),
|
|
831
1002
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
832
1003
|
updated_since: isoDateTimeSchema.optional(),
|
|
833
1004
|
min_bounty_min: z.number().int().min(0).optional(),
|
|
@@ -1211,7 +1382,7 @@ export function createBbradarServer(client, config) {
|
|
|
1211
1382
|
platforms: platformListSchema(),
|
|
1212
1383
|
tags: tagListSchema(),
|
|
1213
1384
|
language_tags: languageTagListSchema(),
|
|
1214
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
1385
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
1215
1386
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
1216
1387
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
1217
1388
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -1387,7 +1558,7 @@ export function createBbradarServer(client, config) {
|
|
|
1387
1558
|
annotations: readOnlyAnnotations
|
|
1388
1559
|
}, (args) => runTool("get_bbradar_guide", config, rateLimiter, async () => ({
|
|
1389
1560
|
request_id: randomUUID(),
|
|
1390
|
-
guide: bbradarGuide(args.topic, config.toolProfile)
|
|
1561
|
+
guide: bbradarGuide(args.topic, config.toolProfile, client)
|
|
1391
1562
|
})));
|
|
1392
1563
|
registerTool("get_mcp_status", {
|
|
1393
1564
|
title: "Get BBRadar MCP Status",
|
|
@@ -1417,6 +1588,13 @@ export function createBbradarServer(client, config) {
|
|
|
1417
1588
|
compact_tool_text: config.compactToolText,
|
|
1418
1589
|
tool_profile: config.toolProfile,
|
|
1419
1590
|
request_timeout_ms: config.requestTimeoutMs,
|
|
1591
|
+
feed_cursors: feedCursorStore.stats(),
|
|
1592
|
+
continuations: {
|
|
1593
|
+
entries: continuationStore.size,
|
|
1594
|
+
max_entries: MAX_LOCAL_EXPORT_RESOURCES,
|
|
1595
|
+
inline_limit: config.continuationInlineLimit ?? 50,
|
|
1596
|
+
ttl_ms: config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS
|
|
1597
|
+
},
|
|
1420
1598
|
validate_on_startup: config.validateOnStartup,
|
|
1421
1599
|
transport: "stdio",
|
|
1422
1600
|
remote_streamable_http: "not_enabled_for_local_setup",
|
|
@@ -1626,7 +1804,7 @@ export function createBbradarServer(client, config) {
|
|
|
1626
1804
|
include_removed: z.boolean().default(true),
|
|
1627
1805
|
include_ineligible: z.boolean().default(false),
|
|
1628
1806
|
include_out_of_scope: z.boolean().default(false),
|
|
1629
|
-
page_size: recentChangesPageSizeSchema.default(50)
|
|
1807
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(50, maxTargetChangesPageSize))
|
|
1630
1808
|
},
|
|
1631
1809
|
outputSchema: {
|
|
1632
1810
|
...apiEnvelopeOutputShape(),
|
|
@@ -1638,16 +1816,16 @@ export function createBbradarServer(client, config) {
|
|
|
1638
1816
|
}, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
|
|
1639
1817
|
registerTool("export_targets", {
|
|
1640
1818
|
title: "Export BBRadar Targets",
|
|
1641
|
-
description: `Read-only target export. limit max ${
|
|
1819
|
+
description: `Read-only target export. limit max ${maxTargetExport}.`,
|
|
1642
1820
|
inputSchema: {
|
|
1643
|
-
program_ids: z.array(programIdentifierSchema).max(
|
|
1821
|
+
program_ids: z.array(programIdentifierSchema).max(maxTargetExportProgramIds).default([]),
|
|
1644
1822
|
platforms: platformListSchema(),
|
|
1645
1823
|
tags: tagListSchema(),
|
|
1646
1824
|
opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
|
|
1647
1825
|
include_out_of_scope: z.boolean().default(false),
|
|
1648
1826
|
include_ineligible: z.boolean().default(false),
|
|
1649
1827
|
format: z.enum(["json", "csv"]).default("json"),
|
|
1650
|
-
limit: z.number().int().min(1).max(
|
|
1828
|
+
limit: z.number().int().min(1).max(maxTargetExport).default(maxTargetExport)
|
|
1651
1829
|
},
|
|
1652
1830
|
outputSchema: {
|
|
1653
1831
|
...apiEnvelopeOutputShape(),
|
|
@@ -1694,12 +1872,12 @@ export function createBbradarServer(client, config) {
|
|
|
1694
1872
|
}
|
|
1695
1873
|
}));
|
|
1696
1874
|
registerPrompts(server, config.toolProfile);
|
|
1697
|
-
registerResources(server, client, config, exportStore);
|
|
1875
|
+
registerResources(server, client, config, exportStore, continuationStore);
|
|
1698
1876
|
return server;
|
|
1699
1877
|
}
|
|
1700
|
-
function bbradarGuide(topic, profile) {
|
|
1878
|
+
function bbradarGuide(topic, profile, client) {
|
|
1701
1879
|
if (profile === "core") {
|
|
1702
|
-
return coreBbradarGuide(topic);
|
|
1880
|
+
return coreBbradarGuide(topic, client);
|
|
1703
1881
|
}
|
|
1704
1882
|
const primaryEntryPoints = [
|
|
1705
1883
|
{
|
|
@@ -1708,6 +1886,12 @@ function bbradarGuide(topic, profile) {
|
|
|
1708
1886
|
minimum_arguments: {},
|
|
1709
1887
|
useful_arguments: ["platforms", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "fresh_launch_days", "has_api_scope"]
|
|
1710
1888
|
},
|
|
1889
|
+
{
|
|
1890
|
+
tool: "find_repository_hunt_targets",
|
|
1891
|
+
use_when: "The request combines repository activity with reward, target-quality, platform, technology, or freshness filters.",
|
|
1892
|
+
minimum_arguments: {},
|
|
1893
|
+
useful_arguments: ["platforms", "exclude_platforms", "surface_mode", "min_bounty_max", "min_target_score", "changed_since", "ranking_mode"]
|
|
1894
|
+
},
|
|
1711
1895
|
{
|
|
1712
1896
|
tool: "run_program_name_action",
|
|
1713
1897
|
use_when: "The user names one program and asks for a brief, targets, new targets, scope delta, or target breakdown.",
|
|
@@ -1732,6 +1916,11 @@ function bbradarGuide(topic, profile) {
|
|
|
1732
1916
|
first_tool: "find_program_candidates",
|
|
1733
1917
|
then: "Use get_program_brief on the best program_id if the user wants details."
|
|
1734
1918
|
},
|
|
1919
|
+
{
|
|
1920
|
+
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
1921
|
+
first_tool: "list_filters",
|
|
1922
|
+
then: "Resolve the excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. If strict local filters return zero, relax one threshold and retry; cached source pages make the retry cheap."
|
|
1923
|
+
},
|
|
1735
1924
|
{
|
|
1736
1925
|
user_goal: "named program summary",
|
|
1737
1926
|
first_tool: "run_program_name_action",
|
|
@@ -1752,6 +1941,18 @@ function bbradarGuide(topic, profile) {
|
|
|
1752
1941
|
first_tool: "find_recently_updated_programs",
|
|
1753
1942
|
then: "Use get_latest_added_targets for global added targets, or check_program_new_targets when the user names one program."
|
|
1754
1943
|
},
|
|
1944
|
+
{
|
|
1945
|
+
user_goal: "repository changes or repository polling",
|
|
1946
|
+
first_tool: "get_repository_changes",
|
|
1947
|
+
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
1948
|
+
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
1952
|
+
first_tool: "find_repository_hunt_targets",
|
|
1953
|
+
arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
|
|
1954
|
+
then: "Use this single composite call instead of manually chaining program discovery and repository tools. Omit or change any example filter the user did not request."
|
|
1955
|
+
},
|
|
1755
1956
|
{
|
|
1756
1957
|
user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
|
|
1757
1958
|
first_tool: "get_latest_added_targets",
|
|
@@ -1797,10 +1998,12 @@ function bbradarGuide(topic, profile) {
|
|
|
1797
1998
|
};
|
|
1798
1999
|
const filters = {
|
|
1799
2000
|
platforms: "Only set when the user explicitly names a platform. Do not infer Immunefi, HackerOne, etc.",
|
|
2001
|
+
exclude_platforms: "Use exact observed values from list_filters for user-requested platform or geographic exclusions.",
|
|
1800
2002
|
tags: "Broad combined tag filter across scope, language, and target-type signals.",
|
|
1801
2003
|
scope_tags: "Scope category signals such as domain, api, mobile, smart-contract, wildcard.",
|
|
1802
2004
|
target_types: "Target-kind signals such as domain, api, url, mobile, smart-contract.",
|
|
1803
2005
|
language_tags: "Technology/language signals such as javascript, solidity, web3, php, python.",
|
|
2006
|
+
exclude_web3: "Set true for Web2-only requests.",
|
|
1804
2007
|
vdp_mode: "Default exclude. Include VDP/no-bounty data only when asked."
|
|
1805
2008
|
};
|
|
1806
2009
|
const examples = [
|
|
@@ -1851,24 +2054,20 @@ function bbradarGuide(topic, profile) {
|
|
|
1851
2054
|
require_bounty: true
|
|
1852
2055
|
},
|
|
1853
2056
|
primary_entry_points: topic === "start" ? primaryEntryPoints : primaryEntryPoints.filter((entry) => entry.tool !== "get_bbradar_guide"),
|
|
1854
|
-
decision_tree: decisionTree.filter((entry) =>
|
|
2057
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2058
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
1855
2059
|
filters,
|
|
1856
2060
|
advanced_tools: advancedTools,
|
|
1857
2061
|
examples
|
|
1858
2062
|
};
|
|
1859
2063
|
}
|
|
1860
|
-
function coreBbradarGuide(topic) {
|
|
2064
|
+
function coreBbradarGuide(topic, client) {
|
|
1861
2065
|
const decisionTree = [
|
|
1862
2066
|
{
|
|
1863
2067
|
user_goal: "find programs to hunt",
|
|
1864
2068
|
first_tool: "find_programs",
|
|
1865
2069
|
arguments: { max_results: 10, output_mode: "compact" }
|
|
1866
2070
|
},
|
|
1867
|
-
{
|
|
1868
|
-
user_goal: "named program summary or action",
|
|
1869
|
-
first_tool: "run_program_name_action",
|
|
1870
|
-
arguments: { query: "program name", action: "brief" }
|
|
1871
|
-
},
|
|
1872
2071
|
{
|
|
1873
2072
|
user_goal: "named program targets",
|
|
1874
2073
|
first_tool: "get_program_eligible_targets_by_name",
|
|
@@ -1884,6 +2083,17 @@ function coreBbradarGuide(topic) {
|
|
|
1884
2083
|
first_tool: "get_recent_changes",
|
|
1885
2084
|
then: "Use get_latest_added_targets when only newly added targets are needed."
|
|
1886
2085
|
},
|
|
2086
|
+
{
|
|
2087
|
+
user_goal: "repository changes or repository polling",
|
|
2088
|
+
first_tool: "get_repository_changes",
|
|
2089
|
+
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2093
|
+
first_tool: "find_repository_hunt_targets",
|
|
2094
|
+
arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
|
|
2095
|
+
then: "Use this one call instead of manually chaining program and repository tools. Omit or change any example filter the user did not request."
|
|
2096
|
+
},
|
|
1887
2097
|
{
|
|
1888
2098
|
user_goal: "export targets",
|
|
1889
2099
|
first_tool: "export_targets",
|
|
@@ -1909,15 +2119,21 @@ function coreBbradarGuide(topic) {
|
|
|
1909
2119
|
},
|
|
1910
2120
|
primary_entry_points: [
|
|
1911
2121
|
"find_programs",
|
|
2122
|
+
"find_repository_hunt_targets",
|
|
1912
2123
|
"run_program_name_action",
|
|
1913
2124
|
"get_program_eligible_targets_by_name",
|
|
1914
2125
|
"find_programs_by_target",
|
|
1915
2126
|
"get_bbradar_guide"
|
|
1916
2127
|
],
|
|
1917
|
-
decision_tree: decisionTree.filter((entry) =>
|
|
1918
|
-
|
|
2128
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2129
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2130
|
+
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
|
|
1919
2131
|
};
|
|
1920
2132
|
}
|
|
2133
|
+
function toolAvailableForContract(client, toolName) {
|
|
2134
|
+
const endpoint = TOOL_ENDPOINT_REQUIREMENTS[toolName];
|
|
2135
|
+
return endpoint === undefined || client.supportsEndpoint(endpoint);
|
|
2136
|
+
}
|
|
1921
2137
|
function guideEntryMatchesTopic(topic, entry) {
|
|
1922
2138
|
const goal = typeof entry.user_goal === "string" ? entry.user_goal : "";
|
|
1923
2139
|
const firstTool = typeof entry.first_tool === "string" ? entry.first_tool : "";
|
|
@@ -1944,15 +2160,52 @@ function guideEntryMatchesTopic(topic, entry) {
|
|
|
1944
2160
|
}
|
|
1945
2161
|
return true;
|
|
1946
2162
|
}
|
|
1947
|
-
function registerResources(server, client, config, exportStore) {
|
|
2163
|
+
function registerResources(server, client, config, exportStore, continuationStore) {
|
|
2164
|
+
server.registerResource("continuation", new ResourceTemplate("bbradar://continuations{?continuation_id,offset,limit}", { list: undefined }), {
|
|
2165
|
+
title: "BBRadar Result Continuation",
|
|
2166
|
+
description: "Bounded, short-lived in-memory continuation page for a large tool result.",
|
|
2167
|
+
mimeType: "application/json"
|
|
2168
|
+
}, async (uri) => {
|
|
2169
|
+
const continuationId = uri.searchParams.get("continuation_id") ?? "";
|
|
2170
|
+
pruneExpiredContinuations(continuationStore);
|
|
2171
|
+
const stored = continuationStore.get(continuationId);
|
|
2172
|
+
if (!stored) {
|
|
2173
|
+
return jsonResource(uri.toString(), {
|
|
2174
|
+
request_id: randomUUID(),
|
|
2175
|
+
error: { message: "Continuation not found or expired from local memory." }
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
const offset = boundedResourceInteger(uri.searchParams.get("offset"), 0, 0, stored.items.length);
|
|
2179
|
+
const limit = boundedResourceInteger(uri.searchParams.get("limit"), config.continuationInlineLimit ?? 50, 1, 100);
|
|
2180
|
+
const items = stored.items.slice(offset, offset + limit);
|
|
2181
|
+
const nextOffset = offset + items.length;
|
|
2182
|
+
const hasMore = nextOffset < stored.items.length;
|
|
2183
|
+
return jsonResource(uri.toString(), {
|
|
2184
|
+
request_id: randomUUID(),
|
|
2185
|
+
continuation_id: continuationId,
|
|
2186
|
+
label: stored.label,
|
|
2187
|
+
items,
|
|
2188
|
+
meta: {
|
|
2189
|
+
offset,
|
|
2190
|
+
limit,
|
|
2191
|
+
returned: items.length,
|
|
2192
|
+
total: stored.items.length,
|
|
2193
|
+
has_more: hasMore,
|
|
2194
|
+
next_resource_uri: hasMore
|
|
2195
|
+
? continuationResourceUri(continuationId, nextOffset, limit)
|
|
2196
|
+
: undefined
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
});
|
|
1948
2200
|
server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
|
|
1949
2201
|
title: "BBRadar Target Export",
|
|
1950
|
-
description: "
|
|
2202
|
+
description: "Bounded, short-lived in-memory target export payload by export_id query parameter.",
|
|
1951
2203
|
mimeType: "application/json"
|
|
1952
2204
|
}, async (uri) => {
|
|
1953
2205
|
const exportId = uri.searchParams.get("export_id") ?? "";
|
|
1954
|
-
|
|
1955
|
-
|
|
2206
|
+
pruneExpiredExports(exportStore);
|
|
2207
|
+
const storedExport = exportStore.get(exportId);
|
|
2208
|
+
if (!storedExport) {
|
|
1956
2209
|
return jsonResource(uri.toString(), {
|
|
1957
2210
|
request_id: randomUUID(),
|
|
1958
2211
|
error: {
|
|
@@ -1963,7 +2216,7 @@ function registerResources(server, client, config, exportStore) {
|
|
|
1963
2216
|
return jsonResource(uri.toString(), {
|
|
1964
2217
|
request_id: randomUUID(),
|
|
1965
2218
|
export_id: exportId,
|
|
1966
|
-
export: payload
|
|
2219
|
+
export: storedExport.payload
|
|
1967
2220
|
});
|
|
1968
2221
|
});
|
|
1969
2222
|
server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
|
|
@@ -3498,6 +3751,10 @@ function programsByTargetResult(input, warnings, sourceRequests, matches, budget
|
|
|
3498
3751
|
});
|
|
3499
3752
|
}
|
|
3500
3753
|
async function searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests) {
|
|
3754
|
+
if (!client.supportsEndpoint("target_search")) {
|
|
3755
|
+
warnings.push("The negotiated API contract does not expose direct target search; using sampled program target lists.");
|
|
3756
|
+
return undefined;
|
|
3757
|
+
}
|
|
3501
3758
|
const searchQueries = targetApiSearchQueries(input.target_query);
|
|
3502
3759
|
const grouped = new Map();
|
|
3503
3760
|
let rawMatchesScanned = 0;
|
|
@@ -3519,7 +3776,7 @@ async function searchProgramsByTargetApi(client, config, input, budget, warnings
|
|
|
3519
3776
|
include_out_of_scope: input.include_out_of_scope,
|
|
3520
3777
|
include_ineligible: input.include_ineligible,
|
|
3521
3778
|
page: 1,
|
|
3522
|
-
page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
|
|
3779
|
+
page_size: Math.min(client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM), Math.max(input.max_results * input.max_targets_per_program, input.max_results))
|
|
3523
3780
|
});
|
|
3524
3781
|
refundBudgetIfNoUpstreamRequest(budget, api);
|
|
3525
3782
|
}
|
|
@@ -4096,8 +4353,18 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4096
4353
|
return true;
|
|
4097
4354
|
}
|
|
4098
4355
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4099
|
-
const api = await client.getProgramTargets(programId
|
|
4356
|
+
const api = await client.getProgramTargets(programId, {
|
|
4357
|
+
search: input.search,
|
|
4358
|
+
target_type: input.target_type,
|
|
4359
|
+
eligibility: input.include_ineligible ? "all" : "eligible",
|
|
4360
|
+
minTargetScore: input.min_target_score,
|
|
4361
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
4362
|
+
competitionRisk: input.competition_risk,
|
|
4363
|
+
page: 1,
|
|
4364
|
+
page_size: client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM)
|
|
4365
|
+
});
|
|
4100
4366
|
const data = readObject(api.data);
|
|
4367
|
+
const upstreamMeta = readObject(data?.meta);
|
|
4101
4368
|
const rawTargets = readArray(data?.targets);
|
|
4102
4369
|
const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
|
|
4103
4370
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
@@ -4108,10 +4375,13 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4108
4375
|
offset: input.offset,
|
|
4109
4376
|
limit: input.limit,
|
|
4110
4377
|
output_mode: input.output_mode,
|
|
4111
|
-
total_active_targets: rawTargets.length,
|
|
4378
|
+
total_active_targets: readNumber(upstreamMeta?.total) ?? rawTargets.length,
|
|
4112
4379
|
total_after_filters: sanitizedTargets.length,
|
|
4113
4380
|
returned: targets.length,
|
|
4114
|
-
has_more: input.offset + input.limit < sanitizedTargets.length,
|
|
4381
|
+
has_more: input.offset + input.limit < sanitizedTargets.length || readBoolean(upstreamMeta?.has_more) === true,
|
|
4382
|
+
upstream_has_more: readBoolean(upstreamMeta?.has_more),
|
|
4383
|
+
possibly_incomplete: readBoolean(upstreamMeta?.has_more) === true,
|
|
4384
|
+
data_capabilities: readObject(upstreamMeta?.data_capabilities),
|
|
4115
4385
|
target_source: "program_targets"
|
|
4116
4386
|
}
|
|
4117
4387
|
});
|
|
@@ -5196,6 +5466,79 @@ function programTargetsResourceUri(programId) {
|
|
|
5196
5466
|
function exportResourceUri(exportId) {
|
|
5197
5467
|
return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
|
|
5198
5468
|
}
|
|
5469
|
+
function continuationResourceUri(continuationId, offset, limit) {
|
|
5470
|
+
return `bbradar://continuations?continuation_id=${encodeURIComponent(continuationId)}&offset=${offset}&limit=${limit}`;
|
|
5471
|
+
}
|
|
5472
|
+
function applyCollectionContinuation(payload, field, label, store, config) {
|
|
5473
|
+
const items = Array.isArray(payload[field]) ? payload[field] : undefined;
|
|
5474
|
+
const inlineLimit = Math.min(100, Math.max(1, config.continuationInlineLimit ?? 50));
|
|
5475
|
+
if (!items || items.length <= inlineLimit) {
|
|
5476
|
+
return payload;
|
|
5477
|
+
}
|
|
5478
|
+
const continuationId = randomUUID();
|
|
5479
|
+
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5480
|
+
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
5481
|
+
if (!stored) {
|
|
5482
|
+
return {
|
|
5483
|
+
...payload,
|
|
5484
|
+
[field]: items.slice(0, inlineLimit),
|
|
5485
|
+
warnings: uniqueStrings([
|
|
5486
|
+
...readArray(payload.warnings).filter((entry) => typeof entry === "string"),
|
|
5487
|
+
"The complete result exceeded the local continuation-store byte limit; only the inline prefix is available."
|
|
5488
|
+
])
|
|
5489
|
+
};
|
|
5490
|
+
}
|
|
5491
|
+
return {
|
|
5492
|
+
...payload,
|
|
5493
|
+
[field]: items.slice(0, inlineLimit),
|
|
5494
|
+
continuation: {
|
|
5495
|
+
continuation_id: continuationId,
|
|
5496
|
+
resource_uri: continuationResourceUri(continuationId, inlineLimit, inlineLimit),
|
|
5497
|
+
collection: field,
|
|
5498
|
+
total: items.length,
|
|
5499
|
+
inline_returned: inlineLimit,
|
|
5500
|
+
remaining: items.length - inlineLimit,
|
|
5501
|
+
expires_at: new Date(expiresAt).toISOString()
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
}
|
|
5505
|
+
function rememberContinuation(store, id, label, items, expiresAt) {
|
|
5506
|
+
const byteLength = Buffer.byteLength(JSON.stringify(items), "utf8");
|
|
5507
|
+
pruneExpiredContinuations(store);
|
|
5508
|
+
if (byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5509
|
+
return false;
|
|
5510
|
+
}
|
|
5511
|
+
while (store.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5512
|
+
retainedContinuationBytes(store) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5513
|
+
const oldest = store.keys().next().value;
|
|
5514
|
+
if (oldest === undefined) {
|
|
5515
|
+
break;
|
|
5516
|
+
}
|
|
5517
|
+
store.delete(oldest);
|
|
5518
|
+
}
|
|
5519
|
+
store.set(id, { items, label, byteLength, expiresAt });
|
|
5520
|
+
return true;
|
|
5521
|
+
}
|
|
5522
|
+
function pruneExpiredContinuations(store, now = Date.now()) {
|
|
5523
|
+
for (const [id, value] of store) {
|
|
5524
|
+
if (value.expiresAt <= now) {
|
|
5525
|
+
store.delete(id);
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
function retainedContinuationBytes(store) {
|
|
5530
|
+
let total = 0;
|
|
5531
|
+
for (const value of store.values()) {
|
|
5532
|
+
total += value.byteLength;
|
|
5533
|
+
}
|
|
5534
|
+
return total;
|
|
5535
|
+
}
|
|
5536
|
+
function boundedResourceInteger(raw, fallback, minimum, maximum) {
|
|
5537
|
+
const parsed = raw === null ? fallback : Number.parseInt(raw, 10);
|
|
5538
|
+
return Number.isFinite(parsed)
|
|
5539
|
+
? Math.min(maximum, Math.max(minimum, parsed))
|
|
5540
|
+
: fallback;
|
|
5541
|
+
}
|
|
5199
5542
|
function jsonResource(uri, payload) {
|
|
5200
5543
|
return {
|
|
5201
5544
|
contents: [
|
|
@@ -5208,41 +5551,86 @@ function jsonResource(uri, payload) {
|
|
|
5208
5551
|
};
|
|
5209
5552
|
}
|
|
5210
5553
|
function rememberExport(exportStore, exportId, payload) {
|
|
5554
|
+
const now = Date.now();
|
|
5555
|
+
const byteLength = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
5556
|
+
pruneExpiredExports(exportStore, now);
|
|
5211
5557
|
if (exportStore.has(exportId)) {
|
|
5212
5558
|
exportStore.delete(exportId);
|
|
5213
5559
|
}
|
|
5214
|
-
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES
|
|
5560
|
+
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5561
|
+
retainedExportBytes(exportStore) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5215
5562
|
const oldestKey = exportStore.keys().next().value;
|
|
5216
5563
|
if (oldestKey === undefined) {
|
|
5217
5564
|
break;
|
|
5218
5565
|
}
|
|
5219
5566
|
exportStore.delete(oldestKey);
|
|
5220
5567
|
}
|
|
5221
|
-
exportStore.set(exportId,
|
|
5568
|
+
exportStore.set(exportId, {
|
|
5569
|
+
payload,
|
|
5570
|
+
byteLength,
|
|
5571
|
+
expiresAt: now + LOCAL_EXPORT_TTL_MS
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5574
|
+
function pruneExpiredExports(exportStore, now = Date.now()) {
|
|
5575
|
+
for (const [exportId, storedExport] of exportStore) {
|
|
5576
|
+
if (storedExport.expiresAt <= now) {
|
|
5577
|
+
exportStore.delete(exportId);
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
function retainedExportBytes(exportStore) {
|
|
5582
|
+
let total = 0;
|
|
5583
|
+
for (const storedExport of exportStore.values()) {
|
|
5584
|
+
total += storedExport.byteLength;
|
|
5585
|
+
}
|
|
5586
|
+
return total;
|
|
5222
5587
|
}
|
|
5223
5588
|
function previewExportPayload(payload) {
|
|
5589
|
+
if (typeof payload === "string") {
|
|
5590
|
+
return previewExportText(payload);
|
|
5591
|
+
}
|
|
5224
5592
|
if (Array.isArray(payload)) {
|
|
5225
5593
|
return payload.slice(0, EXPORT_PREVIEW_LIMIT);
|
|
5226
5594
|
}
|
|
5227
|
-
|
|
5595
|
+
const object = readObject(payload);
|
|
5596
|
+
if (object && typeof object.content === "string") {
|
|
5228
5597
|
return {
|
|
5229
|
-
...
|
|
5230
|
-
|
|
5598
|
+
...object,
|
|
5599
|
+
content: previewExportText(object.content)
|
|
5600
|
+
};
|
|
5601
|
+
}
|
|
5602
|
+
if (object && Array.isArray(object.targets)) {
|
|
5603
|
+
return {
|
|
5604
|
+
...object,
|
|
5605
|
+
targets: object.targets.slice(0, EXPORT_PREVIEW_LIMIT)
|
|
5231
5606
|
};
|
|
5232
5607
|
}
|
|
5233
5608
|
return payload;
|
|
5234
5609
|
}
|
|
5235
5610
|
function previewExportCount(payload) {
|
|
5611
|
+
if (typeof payload === "string") {
|
|
5612
|
+
return Math.min(Math.max(0, payload.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5613
|
+
}
|
|
5236
5614
|
if (Array.isArray(payload)) {
|
|
5237
5615
|
return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
|
|
5238
5616
|
}
|
|
5239
5617
|
const object = readObject(payload);
|
|
5618
|
+
if (typeof object?.content === "string") {
|
|
5619
|
+
return Math.min(Math.max(0, object.content.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5620
|
+
}
|
|
5240
5621
|
const targets = object?.targets;
|
|
5241
5622
|
if (Array.isArray(targets)) {
|
|
5242
5623
|
return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
|
|
5243
5624
|
}
|
|
5244
5625
|
return payload === undefined || payload === null ? 0 : 1;
|
|
5245
5626
|
}
|
|
5627
|
+
function previewExportText(payload) {
|
|
5628
|
+
const lines = payload.split(/\r?\n/);
|
|
5629
|
+
const preview = lines.slice(0, EXPORT_PREVIEW_LIMIT + 1).join("\n");
|
|
5630
|
+
return lines.length > EXPORT_PREVIEW_LIMIT + 1
|
|
5631
|
+
? `${preview}\n[preview truncated; read the export resource for the complete payload]`
|
|
5632
|
+
: preview;
|
|
5633
|
+
}
|
|
5246
5634
|
function collectResourceLinks(payload) {
|
|
5247
5635
|
const links = new Map();
|
|
5248
5636
|
for (const program of extractProgramsFromPayload(payload)) {
|
|
@@ -5284,6 +5672,19 @@ function collectResourceLinks(payload) {
|
|
|
5284
5672
|
mimeType: "application/json"
|
|
5285
5673
|
});
|
|
5286
5674
|
}
|
|
5675
|
+
const continuation = readObject(payload.continuation);
|
|
5676
|
+
const continuationUri = stringField(continuation, "resource_uri");
|
|
5677
|
+
const continuationId = stringField(continuation, "continuation_id");
|
|
5678
|
+
if (continuationUri && continuationId) {
|
|
5679
|
+
links.set(continuationUri, {
|
|
5680
|
+
type: "resource_link",
|
|
5681
|
+
uri: continuationUri,
|
|
5682
|
+
name: `${continuationId} continuation`,
|
|
5683
|
+
title: "Result continuation",
|
|
5684
|
+
description: "Read the next bounded page of this local in-memory result.",
|
|
5685
|
+
mimeType: "application/json"
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5287
5688
|
return [...links.values()];
|
|
5288
5689
|
}
|
|
5289
5690
|
function extractProgramsFromPayload(payload) {
|
|
@@ -5333,9 +5734,18 @@ function compactProgram(program) {
|
|
|
5333
5734
|
handle: stringField(program, "handle"),
|
|
5334
5735
|
name: stringField(program, "name"),
|
|
5335
5736
|
platform: stringField(program, "platform"),
|
|
5737
|
+
catalog: stringField(program, "catalog"),
|
|
5738
|
+
program_type: stringField(program, "program_type"),
|
|
5336
5739
|
platform_url: stringField(program, "platform_url"),
|
|
5337
5740
|
bbradar_url: stringField(program, "bbradar_url"),
|
|
5741
|
+
policy_url: stringField(program, "policy_url"),
|
|
5742
|
+
reporting_url: stringField(program, "reporting_url"),
|
|
5743
|
+
reward_status: stringField(program, "reward_status"),
|
|
5744
|
+
data_capabilities: readObject(program.data_capabilities),
|
|
5338
5745
|
opportunity_tag: readObject(program.opportunity_tag),
|
|
5746
|
+
competition_risk: stringField(program, "competition_risk"),
|
|
5747
|
+
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5748
|
+
submission_cost: readObject(program.submission_cost),
|
|
5339
5749
|
reward_range: readObject(program.reward_range),
|
|
5340
5750
|
public_report_count: readNumber(program.public_report_count),
|
|
5341
5751
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
@@ -5385,6 +5795,30 @@ function formatChange(change, mode) {
|
|
|
5385
5795
|
target: target ? compactTarget(target) : undefined
|
|
5386
5796
|
});
|
|
5387
5797
|
}
|
|
5798
|
+
function formatRepositoryChange(change, mode) {
|
|
5799
|
+
if (mode === "full") {
|
|
5800
|
+
return change;
|
|
5801
|
+
}
|
|
5802
|
+
const repository = readObject(change.repository);
|
|
5803
|
+
return stripUndefined({
|
|
5804
|
+
id: change.id,
|
|
5805
|
+
event_type: stringField(change, "event_type"),
|
|
5806
|
+
ref_name: stringField(change, "ref_name"),
|
|
5807
|
+
observed_at: stringField(change, "observed_at"),
|
|
5808
|
+
repository: repository
|
|
5809
|
+
? stripUndefined({
|
|
5810
|
+
provider: stringField(repository, "provider"),
|
|
5811
|
+
full_name: stringField(repository, "full_name"),
|
|
5812
|
+
url: stringField(repository, "url")
|
|
5813
|
+
})
|
|
5814
|
+
: undefined,
|
|
5815
|
+
changes_url: stringField(change, "changes_url"),
|
|
5816
|
+
programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
|
|
5817
|
+
targets: Array.isArray(change.targets)
|
|
5818
|
+
? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
|
|
5819
|
+
: undefined
|
|
5820
|
+
});
|
|
5821
|
+
}
|
|
5388
5822
|
function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
|
|
5389
5823
|
const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
|
|
5390
5824
|
const groups = {
|
|
@@ -5636,6 +6070,14 @@ function candidateLooksContestLike(candidate) {
|
|
|
5636
6070
|
return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
|
|
5637
6071
|
}
|
|
5638
6072
|
function candidateHasKnownNoBounty(candidate) {
|
|
6073
|
+
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
6074
|
+
const rewardStatus = normalizeTag(stringField(candidate.program, "reward_status") ?? "");
|
|
6075
|
+
if (programType === "vdp" || rewardStatus === "no_reward" || rewardStatus === "no reward") {
|
|
6076
|
+
return true;
|
|
6077
|
+
}
|
|
6078
|
+
if (programType === "paid" || rewardStatus === "paid") {
|
|
6079
|
+
return false;
|
|
6080
|
+
}
|
|
5639
6081
|
if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
|
|
5640
6082
|
return false;
|
|
5641
6083
|
}
|
|
@@ -5824,6 +6266,16 @@ function normalizeFindProgramsInput(input) {
|
|
|
5824
6266
|
const warnings = [];
|
|
5825
6267
|
const normalized = { ...input };
|
|
5826
6268
|
normalized.vdp_mode ??= "exclude";
|
|
6269
|
+
normalized.exclude_platforms = uniqueStrings(normalized.exclude_platforms ?? []);
|
|
6270
|
+
if (normalized.web3 && normalized.exclude_web3) {
|
|
6271
|
+
normalized.web3 = false;
|
|
6272
|
+
warnings.push("exclude_web3=true took precedence over the contradictory web3=true filter.");
|
|
6273
|
+
}
|
|
6274
|
+
const includedPlatforms = new Set(normalized.platforms.map(normalizeTag));
|
|
6275
|
+
const overlappingPlatforms = normalized.exclude_platforms.filter((platform) => includedPlatforms.has(normalizeTag(platform)));
|
|
6276
|
+
if (overlappingPlatforms.length > 0) {
|
|
6277
|
+
warnings.push(`Excluded platform filters take precedence over included values: ${overlappingPlatforms.join(", ")}.`);
|
|
6278
|
+
}
|
|
5827
6279
|
normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
|
|
5828
6280
|
normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
|
|
5829
6281
|
normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
|
|
@@ -5842,6 +6294,285 @@ function clampLimit(name, value, max, warnings) {
|
|
|
5842
6294
|
warnings.push(`${name} was clamped from ${value} to ${max}.`);
|
|
5843
6295
|
return max;
|
|
5844
6296
|
}
|
|
6297
|
+
async function findRepositoryHuntTargets(client, config, input) {
|
|
6298
|
+
const warnings = [];
|
|
6299
|
+
const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
|
|
6300
|
+
const programRequestBudget = Math.max(1, input.upstream_request_budget - repositoryRequestReserve);
|
|
6301
|
+
const programInput = {
|
|
6302
|
+
platforms: input.platforms,
|
|
6303
|
+
exclude_platforms: input.exclude_platforms,
|
|
6304
|
+
tags: input.tags,
|
|
6305
|
+
scope_tags: input.scope_tags,
|
|
6306
|
+
target_types: input.target_types,
|
|
6307
|
+
language_tags: input.language_tags,
|
|
6308
|
+
web3: input.surface_mode === "web3",
|
|
6309
|
+
exclude_web3: input.surface_mode === "web2",
|
|
6310
|
+
opportunity_levels: input.opportunity_levels,
|
|
6311
|
+
updated_since: undefined,
|
|
6312
|
+
min_bounty_min: undefined,
|
|
6313
|
+
min_bounty_max: input.min_bounty_max,
|
|
6314
|
+
max_bounty_max: undefined,
|
|
6315
|
+
require_bounty: input.require_bounty,
|
|
6316
|
+
max_public_report_count: input.max_public_report_count,
|
|
6317
|
+
require_known_report_count: false,
|
|
6318
|
+
include_unknown_report_count: input.include_unknown_report_count,
|
|
6319
|
+
exclude_ctf: true,
|
|
6320
|
+
exclude_mobile_only: false,
|
|
6321
|
+
exclude_non_web: input.surface_mode === "web2",
|
|
6322
|
+
has_api_scope: false,
|
|
6323
|
+
contest_like: false,
|
|
6324
|
+
vdp_mode: "exclude",
|
|
6325
|
+
fresh_launch_days: undefined,
|
|
6326
|
+
min_wildcards: input.min_wildcards,
|
|
6327
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6328
|
+
min_total_targets: input.min_total_targets,
|
|
6329
|
+
min_added_24h: undefined,
|
|
6330
|
+
min_added_7d: input.min_added_7d,
|
|
6331
|
+
max_pages: input.max_program_pages,
|
|
6332
|
+
max_results: input.max_program_candidates,
|
|
6333
|
+
sort_by: "highest_reward",
|
|
6334
|
+
include_target_samples: false,
|
|
6335
|
+
target_sample_programs: 0,
|
|
6336
|
+
target_sample_size: 1,
|
|
6337
|
+
only_in_scope_targets: true,
|
|
6338
|
+
only_bounty_eligible_targets: true,
|
|
6339
|
+
target_samples_only_wildcards: false,
|
|
6340
|
+
output_mode: "compact",
|
|
6341
|
+
upstream_request_budget: programRequestBudget
|
|
6342
|
+
};
|
|
6343
|
+
const [programPayload, repository] = await Promise.all([
|
|
6344
|
+
findPrograms(client, config, programInput),
|
|
6345
|
+
fetchRepositoryHuntPage(client, input, warnings, repositoryRequestReserve)
|
|
6346
|
+
]);
|
|
6347
|
+
const candidatePrograms = readArray(programPayload.programs)
|
|
6348
|
+
.map(readObject)
|
|
6349
|
+
.filter((program) => program !== undefined);
|
|
6350
|
+
const candidatesById = new Map(candidatePrograms
|
|
6351
|
+
.map((program) => [stringField(program, "id"), program])
|
|
6352
|
+
.filter((entry) => entry[0] !== undefined));
|
|
6353
|
+
const changedSinceTime = input.changed_since ? Date.parse(input.changed_since) : undefined;
|
|
6354
|
+
const rankedMatches = [];
|
|
6355
|
+
let qualifyingTargetCount = 0;
|
|
6356
|
+
for (const rawChange of readArray(readObject(repository.api.data)?.results)) {
|
|
6357
|
+
const change = sanitizeRepositoryChange(rawChange, config.webBaseUrl);
|
|
6358
|
+
const observedAtText = stringField(change, "observed_at");
|
|
6359
|
+
const observedAt = observedAtText ? Date.parse(observedAtText) : 0;
|
|
6360
|
+
if (changedSinceTime !== undefined && (!Number.isFinite(observedAt) || observedAt < changedSinceTime)) {
|
|
6361
|
+
continue;
|
|
6362
|
+
}
|
|
6363
|
+
const programs = readArray(change.programs)
|
|
6364
|
+
.map((program) => stringField(readObject(program), "id"))
|
|
6365
|
+
.filter((id) => id !== undefined)
|
|
6366
|
+
.map((id) => candidatesById.get(id))
|
|
6367
|
+
.filter((program) => program !== undefined);
|
|
6368
|
+
if (programs.length === 0) {
|
|
6369
|
+
continue;
|
|
6370
|
+
}
|
|
6371
|
+
const targets = readArray(change.targets)
|
|
6372
|
+
.map(readObject)
|
|
6373
|
+
.filter((target) => target !== undefined)
|
|
6374
|
+
.filter((target) => repositoryHuntTargetMatches(target, input))
|
|
6375
|
+
.sort((left, right) => (readNumber(right.target_opportunity_score) ?? 0) - (readNumber(left.target_opportunity_score) ?? 0));
|
|
6376
|
+
if (targets.length === 0) {
|
|
6377
|
+
continue;
|
|
6378
|
+
}
|
|
6379
|
+
qualifyingTargetCount += targets.length;
|
|
6380
|
+
const maxReward = Math.max(0, ...programs.map(programMaximumReward));
|
|
6381
|
+
const maxTargetScore = Math.max(0, ...targets.map((target) => readNumber(target.target_opportunity_score) ?? 0));
|
|
6382
|
+
const balancedScore = repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt);
|
|
6383
|
+
const formattedChange = formatRepositoryChange(change, input.output_mode);
|
|
6384
|
+
const { programs: _eventPrograms, targets: _eventTargets, ...repositoryChange } = formattedChange;
|
|
6385
|
+
rankedMatches.push({
|
|
6386
|
+
balancedScore,
|
|
6387
|
+
maxReward,
|
|
6388
|
+
maxTargetScore,
|
|
6389
|
+
observedAt,
|
|
6390
|
+
payload: stripUndefined({
|
|
6391
|
+
repository_change: repositoryChange,
|
|
6392
|
+
programs,
|
|
6393
|
+
targets: targets.slice(0, input.max_targets_per_result).map((target) => input.output_mode === "full" ? target : compactTarget(target)),
|
|
6394
|
+
target_count: targets.length,
|
|
6395
|
+
max_target_score: maxTargetScore,
|
|
6396
|
+
max_program_reward: maxReward,
|
|
6397
|
+
match_score: balancedScore,
|
|
6398
|
+
why_ranked_here: repositoryHuntReasons(maxReward, maxTargetScore, observedAt)
|
|
6399
|
+
})
|
|
6400
|
+
});
|
|
6401
|
+
}
|
|
6402
|
+
rankedMatches.sort((left, right) => compareRepositoryHuntMatches(left, right, input.ranking_mode));
|
|
6403
|
+
const matches = rankedMatches.slice(0, input.max_results).map((match, index) => ({
|
|
6404
|
+
rank: index + 1,
|
|
6405
|
+
...match.payload
|
|
6406
|
+
}));
|
|
6407
|
+
const matchedPrograms = uniqueProgramsFromRepositoryMatches(matches);
|
|
6408
|
+
const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
|
|
6409
|
+
if (candidatePrograms.length === 0) {
|
|
6410
|
+
warnings.push("No programs matched the reward and program-level filters. Relax one program filter and retry.");
|
|
6411
|
+
}
|
|
6412
|
+
else if (matches.length === 0) {
|
|
6413
|
+
warnings.push("Programs matched, but no recent repository event also contained a target that passed the target-quality filters. Relax one target or freshness filter and retry.");
|
|
6414
|
+
}
|
|
6415
|
+
const programBudget = readObject(programPayload.upstream_budget);
|
|
6416
|
+
const usedRequests = (readNumber(programBudget?.used) ?? 0) + repository.attempts;
|
|
6417
|
+
return stripUndefined({
|
|
6418
|
+
request_id: randomUUID(),
|
|
6419
|
+
fetched_at: new Date().toISOString(),
|
|
6420
|
+
source_requests: [
|
|
6421
|
+
...readArray(programPayload.source_requests),
|
|
6422
|
+
{
|
|
6423
|
+
source: "repository_changes",
|
|
6424
|
+
request_id: repository.api.requestId,
|
|
6425
|
+
upstream_request_id: repository.api.upstreamRequestId,
|
|
6426
|
+
page: 1,
|
|
6427
|
+
page_size: repository.pageSize,
|
|
6428
|
+
...apiSourceMetadata(repository.api)
|
|
6429
|
+
}
|
|
6430
|
+
],
|
|
6431
|
+
warnings: uniqueStrings([...programWarnings, ...warnings]),
|
|
6432
|
+
filters: stripUndefined({
|
|
6433
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6434
|
+
exclude_platforms: input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
6435
|
+
tags: input.tags.length > 0 ? input.tags : undefined,
|
|
6436
|
+
scope_tags: input.scope_tags.length > 0 ? input.scope_tags : undefined,
|
|
6437
|
+
target_types: input.target_types.length > 0 ? input.target_types : undefined,
|
|
6438
|
+
language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
|
|
6439
|
+
surface_mode: input.surface_mode,
|
|
6440
|
+
opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
|
|
6441
|
+
min_bounty_max: input.min_bounty_max,
|
|
6442
|
+
require_bounty: input.require_bounty,
|
|
6443
|
+
max_public_report_count: input.max_public_report_count,
|
|
6444
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6445
|
+
min_total_targets: input.min_total_targets,
|
|
6446
|
+
min_wildcards: input.min_wildcards,
|
|
6447
|
+
min_added_7d: input.min_added_7d,
|
|
6448
|
+
changed_since: input.changed_since,
|
|
6449
|
+
free_submission_only: input.free_submission_only,
|
|
6450
|
+
min_target_score: input.min_target_score,
|
|
6451
|
+
target_opportunity_labels: input.target_opportunity_labels.length > 0 ? input.target_opportunity_labels : undefined,
|
|
6452
|
+
competition_risks: input.competition_risks.length > 0 ? input.competition_risks : undefined,
|
|
6453
|
+
ranking_mode: input.ranking_mode,
|
|
6454
|
+
output_mode: input.output_mode
|
|
6455
|
+
}),
|
|
6456
|
+
upstream_budget: {
|
|
6457
|
+
initial: input.upstream_request_budget,
|
|
6458
|
+
used: usedRequests,
|
|
6459
|
+
remaining: Math.max(0, input.upstream_request_budget - usedRequests)
|
|
6460
|
+
},
|
|
6461
|
+
ranking_scope: readObject(programPayload.ranking_scope),
|
|
6462
|
+
programs: matchedPrograms,
|
|
6463
|
+
matches,
|
|
6464
|
+
meta: {
|
|
6465
|
+
returned: matches.length,
|
|
6466
|
+
matched_programs: matchedPrograms.length,
|
|
6467
|
+
program_candidates: candidatePrograms.length,
|
|
6468
|
+
repository_events_scanned: readArray(readObject(repository.api.data)?.results).length,
|
|
6469
|
+
qualifying_targets_before_result_limit: qualifyingTargetCount,
|
|
6470
|
+
repository_page_size_used: repository.pageSize,
|
|
6471
|
+
repository_request_attempts: repository.attempts,
|
|
6472
|
+
parallel_source_fetch: true
|
|
6473
|
+
}
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
async function fetchRepositoryHuntPage(client, input, warnings, maxAttempts) {
|
|
6477
|
+
let pageSize = input.repository_page_size;
|
|
6478
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
6479
|
+
try {
|
|
6480
|
+
const api = await client.getRepositoryChanges(toQuery({
|
|
6481
|
+
include_targets: true,
|
|
6482
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6483
|
+
tags: input.tags.length > 0 ? input.tags : undefined,
|
|
6484
|
+
free_submission_only: input.free_submission_only,
|
|
6485
|
+
include_removed: false,
|
|
6486
|
+
include_ineligible: false,
|
|
6487
|
+
include_out_of_scope: false,
|
|
6488
|
+
page: 1,
|
|
6489
|
+
page_size: pageSize
|
|
6490
|
+
}));
|
|
6491
|
+
return { api, pageSize, attempts: attempt };
|
|
6492
|
+
}
|
|
6493
|
+
catch (error) {
|
|
6494
|
+
if (!(error instanceof BBRadarApiError) || error.status !== 413 || attempt >= maxAttempts || pageSize <= 10) {
|
|
6495
|
+
throw error;
|
|
6496
|
+
}
|
|
6497
|
+
const reducedPageSize = Math.max(10, Math.floor(pageSize / 2));
|
|
6498
|
+
warnings.push(`Repository page_size was reduced from ${pageSize} to ${reducedPageSize} after a 413 association-limit response.`);
|
|
6499
|
+
pageSize = reducedPageSize;
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
throw new Error("No repository-change response was collected.");
|
|
6503
|
+
}
|
|
6504
|
+
function repositoryHuntTargetMatches(target, input) {
|
|
6505
|
+
if (readBoolean(target.in_scope) !== true || readBoolean(target.eligible_for_bounty) !== true) {
|
|
6506
|
+
return false;
|
|
6507
|
+
}
|
|
6508
|
+
const score = readNumber(target.target_opportunity_score);
|
|
6509
|
+
if (score === undefined || score < input.min_target_score) {
|
|
6510
|
+
return false;
|
|
6511
|
+
}
|
|
6512
|
+
if (input.target_opportunity_labels.length > 0 &&
|
|
6513
|
+
!input.target_opportunity_labels.includes(stringField(target, "target_opportunity_label"))) {
|
|
6514
|
+
return false;
|
|
6515
|
+
}
|
|
6516
|
+
if (input.competition_risks.length > 0 &&
|
|
6517
|
+
!input.competition_risks.includes(stringField(target, "competition_risk"))) {
|
|
6518
|
+
return false;
|
|
6519
|
+
}
|
|
6520
|
+
if (input.target_types.length > 0 && !input.target_types.some((targetType) => targetMatchesRequestedType(target, targetType))) {
|
|
6521
|
+
return false;
|
|
6522
|
+
}
|
|
6523
|
+
if (input.scope_tags.length > 0 && !targetMatchesAnyTags(target, input.scope_tags, "scope_tags")) {
|
|
6524
|
+
return false;
|
|
6525
|
+
}
|
|
6526
|
+
if (input.language_tags.length > 0 && !targetMatchesAnyTags(target, input.language_tags, "language_tags")) {
|
|
6527
|
+
return false;
|
|
6528
|
+
}
|
|
6529
|
+
return true;
|
|
6530
|
+
}
|
|
6531
|
+
function targetMatchesAnyTags(target, requested, field) {
|
|
6532
|
+
const actual = new Set(readStringArrayField(target, field).map(normalizeTag));
|
|
6533
|
+
return requested.map(normalizeTag).some((value) => actual.has(value));
|
|
6534
|
+
}
|
|
6535
|
+
function programMaximumReward(program) {
|
|
6536
|
+
return readNumber(readObject(program.reward_range)?.max) ?? 0;
|
|
6537
|
+
}
|
|
6538
|
+
function repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt) {
|
|
6539
|
+
const rewardScore = Math.min(100, Math.log10(Math.max(1, maxReward)) / 7 * 100);
|
|
6540
|
+
const ageDays = observedAt > 0 ? Math.max(0, Date.now() - observedAt) / (24 * 60 * 60 * 1000) : 30;
|
|
6541
|
+
const freshnessScore = Math.max(0, 100 - Math.min(30, ageDays) / 30 * 100);
|
|
6542
|
+
return Math.round((maxTargetScore * 0.45 + rewardScore * 0.35 + freshnessScore * 0.2) * 100) / 100;
|
|
6543
|
+
}
|
|
6544
|
+
function repositoryHuntReasons(maxReward, maxTargetScore, observedAt) {
|
|
6545
|
+
const reasons = [`target score up to ${maxTargetScore}`, `maximum reward ${maxReward}`];
|
|
6546
|
+
if (observedAt > 0) {
|
|
6547
|
+
reasons.push(`repository activity observed ${new Date(observedAt).toISOString()}`);
|
|
6548
|
+
}
|
|
6549
|
+
return reasons;
|
|
6550
|
+
}
|
|
6551
|
+
function compareRepositoryHuntMatches(left, right, mode) {
|
|
6552
|
+
if (mode === "highest_reward") {
|
|
6553
|
+
return right.maxReward - left.maxReward || right.maxTargetScore - left.maxTargetScore || right.observedAt - left.observedAt;
|
|
6554
|
+
}
|
|
6555
|
+
if (mode === "best_target") {
|
|
6556
|
+
return right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward || right.observedAt - left.observedAt;
|
|
6557
|
+
}
|
|
6558
|
+
if (mode === "newest") {
|
|
6559
|
+
return right.observedAt - left.observedAt || right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward;
|
|
6560
|
+
}
|
|
6561
|
+
return right.balancedScore - left.balancedScore || right.observedAt - left.observedAt;
|
|
6562
|
+
}
|
|
6563
|
+
function uniqueProgramsFromRepositoryMatches(matches) {
|
|
6564
|
+
const programs = new Map();
|
|
6565
|
+
for (const match of matches) {
|
|
6566
|
+
for (const value of readArray(match.programs)) {
|
|
6567
|
+
const program = readObject(value);
|
|
6568
|
+
const id = stringField(program, "id");
|
|
6569
|
+
if (program && id && !programs.has(id)) {
|
|
6570
|
+
programs.set(id, program);
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
return [...programs.values()];
|
|
6575
|
+
}
|
|
5845
6576
|
async function findPrograms(client, config, input) {
|
|
5846
6577
|
const normalized = normalizeFindProgramsInput(input);
|
|
5847
6578
|
const warnings = normalized.warnings;
|
|
@@ -5890,8 +6621,10 @@ async function findPrograms(client, config, input) {
|
|
|
5890
6621
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
5891
6622
|
filters: stripUndefined({
|
|
5892
6623
|
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6624
|
+
exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
5893
6625
|
tags: upstreamTags.length > 0 ? upstreamTags : undefined,
|
|
5894
6626
|
web3: input.web3 ? true : undefined,
|
|
6627
|
+
exclude_web3: input.exclude_web3 ? true : undefined,
|
|
5895
6628
|
opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
|
|
5896
6629
|
updated_since: input.updated_since,
|
|
5897
6630
|
min_bounty_min: input.min_bounty_min,
|
|
@@ -6423,12 +7156,19 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6423
7156
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
6424
7157
|
return false;
|
|
6425
7158
|
}
|
|
7159
|
+
if ((filters.exclude_platforms?.length ?? 0) > 0 &&
|
|
7160
|
+
filters.exclude_platforms?.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
7161
|
+
return false;
|
|
7162
|
+
}
|
|
6426
7163
|
if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
|
|
6427
7164
|
return false;
|
|
6428
7165
|
}
|
|
6429
7166
|
if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
|
|
6430
7167
|
return false;
|
|
6431
7168
|
}
|
|
7169
|
+
if (filters.exclude_web3 && candidateHasWeb3Surface(candidate)) {
|
|
7170
|
+
return false;
|
|
7171
|
+
}
|
|
6432
7172
|
if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
|
|
6433
7173
|
return false;
|
|
6434
7174
|
}
|
|
@@ -6441,7 +7181,9 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6441
7181
|
if (candidate.wildcardCount < filters.min_wildcards) {
|
|
6442
7182
|
return false;
|
|
6443
7183
|
}
|
|
6444
|
-
|
|
7184
|
+
const explicitlyPaid = normalizeTag(stringField(candidate.program, "program_type") ?? "") === "paid" ||
|
|
7185
|
+
normalizeTag(stringField(candidate.program, "reward_status") ?? "") === "paid";
|
|
7186
|
+
if (filters.require_bounty && !explicitlyPaid && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
|
|
6445
7187
|
return false;
|
|
6446
7188
|
}
|
|
6447
7189
|
if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
|
|
@@ -6812,6 +7554,111 @@ The plan must:
|
|
|
6812
7554
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
6813
7555
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
6814
7556
|
}
|
|
7557
|
+
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
7558
|
+
const normalizedFilters = stableStringify(normalizeFeedCursorFilters(filters));
|
|
7559
|
+
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
7560
|
+
const explicitAfterId = readNumber(filters.after_id);
|
|
7561
|
+
if (explicitAfterId !== undefined) {
|
|
7562
|
+
return { endpoint, key, afterId: explicitAfterId, resumed: false };
|
|
7563
|
+
}
|
|
7564
|
+
if (!resume) {
|
|
7565
|
+
return { endpoint, key, afterId: undefined, resumed: false };
|
|
7566
|
+
}
|
|
7567
|
+
const stored = store.get(key);
|
|
7568
|
+
if (stored === undefined) {
|
|
7569
|
+
throw new Error(`No in-memory ${endpoint} cursor exists for this filter set. Provide after_id once to seed it.`);
|
|
7570
|
+
}
|
|
7571
|
+
return { endpoint, key, afterId: stored, resumed: true };
|
|
7572
|
+
}
|
|
7573
|
+
function advanceFeedCursor(store, cursor, rawMeta) {
|
|
7574
|
+
if (cursor.afterId === undefined) {
|
|
7575
|
+
return undefined;
|
|
7576
|
+
}
|
|
7577
|
+
const meta = readObject(rawMeta);
|
|
7578
|
+
const nextAfterId = readNumber(meta?.next_after_id) ?? cursor.afterId;
|
|
7579
|
+
store.set(cursor.key, nextAfterId);
|
|
7580
|
+
return {
|
|
7581
|
+
endpoint: cursor.endpoint,
|
|
7582
|
+
mode: cursor.resumed ? "resumed" : "seeded",
|
|
7583
|
+
used_after_id: cursor.afterId,
|
|
7584
|
+
next_after_id: nextAfterId,
|
|
7585
|
+
has_more: readBoolean(meta?.has_more) ?? false,
|
|
7586
|
+
stored_in_memory: true
|
|
7587
|
+
};
|
|
7588
|
+
}
|
|
7589
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
|
|
7590
|
+
const startedAt = Date.now();
|
|
7591
|
+
const results = [];
|
|
7592
|
+
const sourceRequests = [];
|
|
7593
|
+
let currentCursor = initialCursor;
|
|
7594
|
+
let lastApi;
|
|
7595
|
+
let lastData;
|
|
7596
|
+
let cursorPayload;
|
|
7597
|
+
let pagesFetched = 0;
|
|
7598
|
+
let hasMore = false;
|
|
7599
|
+
let pageBudgetExhausted = false;
|
|
7600
|
+
let timeBudgetExhausted = false;
|
|
7601
|
+
do {
|
|
7602
|
+
const api = await fetchPage(currentCursor.afterId);
|
|
7603
|
+
const data = readObject(api.data);
|
|
7604
|
+
const pageResults = readArray(data?.results);
|
|
7605
|
+
lastApi = api;
|
|
7606
|
+
lastData = data;
|
|
7607
|
+
results.push(...pageResults);
|
|
7608
|
+
pagesFetched += 1;
|
|
7609
|
+
sourceRequests.push({
|
|
7610
|
+
source: currentCursor.endpoint,
|
|
7611
|
+
page: pagesFetched,
|
|
7612
|
+
request_id: api.requestId,
|
|
7613
|
+
upstream_request_id: api.upstreamRequestId,
|
|
7614
|
+
...apiSourceMetadata(api)
|
|
7615
|
+
});
|
|
7616
|
+
cursorPayload = advanceFeedCursor(store, currentCursor, data?.meta);
|
|
7617
|
+
hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
|
|
7618
|
+
if (currentCursor.afterId === undefined || !hasMore) {
|
|
7619
|
+
break;
|
|
7620
|
+
}
|
|
7621
|
+
if (pagesFetched >= maxPages) {
|
|
7622
|
+
pageBudgetExhausted = true;
|
|
7623
|
+
break;
|
|
7624
|
+
}
|
|
7625
|
+
if (Date.now() - startedAt >= timeBudgetMs) {
|
|
7626
|
+
timeBudgetExhausted = true;
|
|
7627
|
+
break;
|
|
7628
|
+
}
|
|
7629
|
+
currentCursor = {
|
|
7630
|
+
...currentCursor,
|
|
7631
|
+
afterId: readNumber(cursorPayload?.next_after_id) ?? currentCursor.afterId,
|
|
7632
|
+
resumed: true
|
|
7633
|
+
};
|
|
7634
|
+
} while (true);
|
|
7635
|
+
if (!lastApi) {
|
|
7636
|
+
throw new Error(`No ${initialCursor.endpoint} response was collected.`);
|
|
7637
|
+
}
|
|
7638
|
+
return {
|
|
7639
|
+
lastApi,
|
|
7640
|
+
lastData,
|
|
7641
|
+
results,
|
|
7642
|
+
cursor: cursorPayload,
|
|
7643
|
+
pagesFetched,
|
|
7644
|
+
pageBudgetExhausted,
|
|
7645
|
+
timeBudgetExhausted,
|
|
7646
|
+
sourceRequests
|
|
7647
|
+
};
|
|
7648
|
+
}
|
|
7649
|
+
function normalizeFeedCursorFilters(filters) {
|
|
7650
|
+
return Object.fromEntries(Object.entries(filters)
|
|
7651
|
+
.filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
|
|
7652
|
+
.map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
|
|
7653
|
+
}
|
|
7654
|
+
function normalizeFeedCursorFilterValue(value) {
|
|
7655
|
+
if (Array.isArray(value)) {
|
|
7656
|
+
return value
|
|
7657
|
+
.map((entry) => normalizeFeedCursorFilterValue(entry))
|
|
7658
|
+
.sort((left, right) => String(left).localeCompare(String(right)));
|
|
7659
|
+
}
|
|
7660
|
+
return typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
7661
|
+
}
|
|
6815
7662
|
async function runTool(toolName, config, rateLimiter, callback) {
|
|
6816
7663
|
const startedAt = Date.now();
|
|
6817
7664
|
const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
|
|
@@ -6870,7 +7717,8 @@ function compactToolText(payload, isError) {
|
|
|
6870
7717
|
[
|
|
6871
7718
|
readArray(payload.programs).length,
|
|
6872
7719
|
readArray(payload.targets).length,
|
|
6873
|
-
readArray(payload.changes).length
|
|
7720
|
+
readArray(payload.changes).length,
|
|
7721
|
+
readArray(payload.repository_changes).length
|
|
6874
7722
|
].find((count) => count > 0);
|
|
6875
7723
|
return JSON.stringify(stripUndefined({
|
|
6876
7724
|
request_id: payload.request_id,
|
|
@@ -6901,6 +7749,7 @@ function withApiMetadata(api, payload) {
|
|
|
6901
7749
|
upstream_request_id: api.upstreamRequestId,
|
|
6902
7750
|
cache: stripUndefined({
|
|
6903
7751
|
hit: api.cached,
|
|
7752
|
+
stale_while_revalidate: api.stale,
|
|
6904
7753
|
coalesced_live_request: api.coalesced,
|
|
6905
7754
|
expires_at: api.cacheExpiresAt
|
|
6906
7755
|
}),
|
|
@@ -7053,7 +7902,11 @@ function compactTarget(target) {
|
|
|
7053
7902
|
target_type: stringField(target, "target_type"),
|
|
7054
7903
|
in_scope: readBoolean(target.in_scope),
|
|
7055
7904
|
eligible_for_bounty: readBoolean(target.eligible_for_bounty),
|
|
7056
|
-
wildcard: readBoolean(target.wildcard)
|
|
7905
|
+
wildcard: readBoolean(target.wildcard),
|
|
7906
|
+
target_opportunity_score: readNumber(target.target_opportunity_score),
|
|
7907
|
+
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
7908
|
+
competition_risk: stringField(target, "competition_risk"),
|
|
7909
|
+
why_this_target: stringField(target, "why_this_target")
|
|
7057
7910
|
});
|
|
7058
7911
|
}
|
|
7059
7912
|
function targetIdentifier(target) {
|