@bbradar/mcp 0.2.3 → 0.2.6
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/CHANGELOG.md +13 -0
- package/README.md +74 -10
- package/benchmark/compare-revisions.mjs +164 -0
- package/benchmark/pro-benchmark.mjs +84 -0
- package/dist/bbradarClient.d.ts +22 -4
- package/dist/bbradarClient.js +253 -48
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +59 -12
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +619 -88
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
package/dist/server.js
CHANGED
|
@@ -5,8 +5,8 @@ import { BBRadarApiError } from "./bbradarClient.js";
|
|
|
5
5
|
import { NumericCursorStore } from "./cursorStore.js";
|
|
6
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
7
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
8
|
-
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
-
const SERVER_VERSION = "0.2.
|
|
8
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
+
const SERVER_VERSION = "0.2.6";
|
|
10
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
11
11
|
const MAX_TARGET_EXPORT = 500;
|
|
12
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -27,6 +27,7 @@ const MAX_HUNT_TARGETS_PER_RESULT = 10;
|
|
|
27
27
|
const MAX_BULK_PROGRAM_RESOURCE_LINKS = 5;
|
|
28
28
|
const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 12;
|
|
29
29
|
const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
|
|
30
|
+
const MAX_UPSTREAM_PAGE = 500;
|
|
30
31
|
const PROGRAM_COLLECTION_CONCURRENCY = 4;
|
|
31
32
|
const TARGET_SAMPLE_CONCURRENCY = 3;
|
|
32
33
|
const DEFAULT_FILTER_DISCOVERY_PAGES = 2;
|
|
@@ -63,9 +64,17 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
63
64
|
"list_filters",
|
|
64
65
|
"get_bbradar_guide",
|
|
65
66
|
"get_mcp_status",
|
|
66
|
-
"export_targets"
|
|
67
|
+
"export_targets",
|
|
68
|
+
"get_history_changes",
|
|
69
|
+
"get_history_change_detail",
|
|
70
|
+
"get_program_history",
|
|
71
|
+
"get_program_activity",
|
|
72
|
+
"get_analytics_platform_series",
|
|
73
|
+
"get_analytics_program_activity",
|
|
74
|
+
"get_analytics_program_detail"
|
|
67
75
|
]);
|
|
68
76
|
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
77
|
+
search_targets: "target_search",
|
|
69
78
|
get_repository_changes: "repository_changes",
|
|
70
79
|
find_repository_hunt_targets: "repository_changes",
|
|
71
80
|
export_targets: "target_export",
|
|
@@ -95,7 +104,6 @@ const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
|
95
104
|
];
|
|
96
105
|
const PRIMARY_PROGRAM_ID_PLATFORM_GUESSES = PROGRAM_ID_PLATFORM_GUESSES.slice(0, 8);
|
|
97
106
|
const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
|
|
98
|
-
const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
|
|
99
107
|
const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
|
|
100
108
|
const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
|
|
101
109
|
"program",
|
|
@@ -183,6 +191,8 @@ const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_n
|
|
|
183
191
|
const targetMatchModeSchema = z
|
|
184
192
|
.enum(["smart", "contains", "exact", "suffix", "wildcard"])
|
|
185
193
|
.describe("How to match target_query. smart is forgiving for URLs, wildcards, target names, and partial words.");
|
|
194
|
+
const apiTargetMatchModeSchema = z.enum(["contains", "exact", "suffix", "wildcard"]);
|
|
195
|
+
const targetChangeTypeSchema = z.enum(["added", "readded", "updated", "removed"]);
|
|
186
196
|
const programNameActionSchema = z.enum(["new_targets", "targets", "scope_delta", "brief", "target_breakdown"]);
|
|
187
197
|
const guideTopicSchema = z.enum(["start", "program", "discovery", "freshness", "targets", "compare", "export", "filters"]).default("start");
|
|
188
198
|
const readOnlyAnnotations = {
|
|
@@ -241,6 +251,18 @@ const targetQuerySchema = z
|
|
|
241
251
|
})
|
|
242
252
|
.describe("Target identifier, domain, URL, wildcard, app/package/repo name, or partial target text. URLs and wildcards are normalized automatically.");
|
|
243
253
|
const isoDateTimeSchema = z.string().datetime({ offset: true });
|
|
254
|
+
const stableSnapshotSchema = z
|
|
255
|
+
.string()
|
|
256
|
+
.trim()
|
|
257
|
+
.min(1)
|
|
258
|
+
.max(1_024)
|
|
259
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
260
|
+
message: "Snapshot tokens cannot contain control characters."
|
|
261
|
+
})
|
|
262
|
+
.describe("Optional stable-page token. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Snapshot creation is consistency-first and can be slower; omit it for the fastest live result.");
|
|
263
|
+
const opaqueCursorSchema = z.string().min(1).max(4096).describe("Opaque five-minute cursor. Reuse with identical filters and page size; restart the query on expiry.");
|
|
264
|
+
const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
|
|
265
|
+
const proLimitSchema = z.number().int().min(1).max(100);
|
|
244
266
|
const pageSchema = z.number().int().min(1).default(1);
|
|
245
267
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
246
268
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
@@ -265,7 +287,7 @@ const programIdSchema = z
|
|
|
265
287
|
.string()
|
|
266
288
|
.trim()
|
|
267
289
|
.min(3)
|
|
268
|
-
.max(
|
|
290
|
+
.max(620)
|
|
269
291
|
.regex(/^[^:/?#\u0000-\u001F\u007F]+:[^?#\u0000-\u001F\u007F]+$/, "Use platform:handle, for example HackerOne:example-handle.")
|
|
270
292
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
271
293
|
message: "program_id cannot contain control characters."
|
|
@@ -300,6 +322,7 @@ function cacheOutputSchema() {
|
|
|
300
322
|
hit: z.boolean(),
|
|
301
323
|
stale_while_revalidate: z.boolean().optional(),
|
|
302
324
|
coalesced_live_request: z.boolean().optional(),
|
|
325
|
+
no_store: z.boolean().optional(),
|
|
303
326
|
expires_at: z.string().optional()
|
|
304
327
|
});
|
|
305
328
|
}
|
|
@@ -325,6 +348,7 @@ function apiEnvelopeOutputShape() {
|
|
|
325
348
|
mcp_timing: toolTimingOutputSchema().optional(),
|
|
326
349
|
warnings: z.array(z.string()).optional(),
|
|
327
350
|
program_id_resolution: jsonRecordSchema().optional(),
|
|
351
|
+
recovery: jsonRecordSchema().optional(),
|
|
328
352
|
error: errorOutputSchema().optional()
|
|
329
353
|
};
|
|
330
354
|
}
|
|
@@ -387,7 +411,11 @@ const programCandidatePresetInputSchema = {
|
|
|
387
411
|
};
|
|
388
412
|
function createProfiledToolRegistrar(server, profile, client) {
|
|
389
413
|
return (name, toolConfig, callback) => {
|
|
390
|
-
const
|
|
414
|
+
const scopedCallback = ((...args) => {
|
|
415
|
+
const extra = args[args.length - 1];
|
|
416
|
+
return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
|
|
417
|
+
});
|
|
418
|
+
const registeredTool = server.registerTool(name, toolConfig, scopedCallback);
|
|
391
419
|
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
392
420
|
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
393
421
|
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
@@ -404,6 +432,7 @@ export function createBbradarServer(client, config) {
|
|
|
404
432
|
"Use resolve_program or run_program_name_action for a named program.",
|
|
405
433
|
"Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
|
|
406
434
|
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
435
|
+
"For semantic changes use get_history_changes or get_program_history, then get_history_change_detail only when before/after items are needed. For exact activity use get_program_activity; for ranked activity use get_analytics_program_activity. Use daily analytics tools directly for date series.",
|
|
407
436
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
408
437
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
409
438
|
]
|
|
@@ -419,6 +448,7 @@ export function createBbradarServer(client, config) {
|
|
|
419
448
|
"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.",
|
|
420
449
|
"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.",
|
|
421
450
|
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
451
|
+
"Semantic History: get_history_changes globally or get_program_history for one exact ID, especially Self-Hosted. Fetch detail lazily with get_history_change_detail. Exact recorded signals: get_program_activity. Server-side rankings: get_analytics_program_activity. Daily series: get_analytics_platform_series or get_analytics_program_detail. Reuse opaque cursors only with unchanged filters and page size.",
|
|
422
452
|
"Discovery: find_hunt_targets is the one-call default; 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, and find_low_noise_programs are lower-level alternatives.",
|
|
423
453
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
424
454
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -436,6 +466,7 @@ export function createBbradarServer(client, config) {
|
|
|
436
466
|
const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
|
|
437
467
|
const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
|
|
438
468
|
const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
469
|
+
const maxTargetSearchPageSize = client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
439
470
|
const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
440
471
|
const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
441
472
|
const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
|
|
@@ -444,14 +475,93 @@ export function createBbradarServer(client, config) {
|
|
|
444
475
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
|
|
445
476
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
446
477
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
478
|
+
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
479
|
+
const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
|
|
480
|
+
const analyticsOutput = { ...apiEnvelopeOutputShape(), platform: z.string().optional(), program: jsonRecordSchema().optional(), programs: z.array(jsonRecordSchema()).optional(), series: z.array(jsonRecordSchema()).optional(), observations: jsonRecordSchema().optional(), category: z.string().optional(), population: z.string().optional(), ranking_definition: z.string().optional(), inventory_definition: z.string().optional(), meta: jsonRecordSchema().optional() };
|
|
481
|
+
registerTool("get_history_changes", {
|
|
482
|
+
title: "Get Program History Changes",
|
|
483
|
+
description: "One page of semantic program History. Use get_program_history for an exact program, especially Self-Hosted. History may be partial or unavailable; inspect coverage.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90),
|
|
486
|
+
platform: z.string().min(1).max(100).optional(), program_id: programIdSchema.optional(),
|
|
487
|
+
category: z.enum(["scope", "rewards", "rules", "program", "repositories"]).optional(),
|
|
488
|
+
search: z.string().min(1).max(120).optional(), limit: proLimitSchema.default(50)
|
|
489
|
+
}, outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
490
|
+
}, (args) => runTool("get_history_changes", config, rateLimiter, async () => {
|
|
491
|
+
assertProFeature(client, "history_changes", "history_capture_enabled");
|
|
492
|
+
if (args.days === 0 && !args.program_id)
|
|
493
|
+
throw new Error("days=0 requires program_id; use get_program_history for Self-Hosted catalog IDs.");
|
|
494
|
+
const api = await client.getHistoryChanges(toQuery(args));
|
|
495
|
+
return proPage(api, "changes");
|
|
496
|
+
}));
|
|
497
|
+
registerTool("get_history_change_detail", {
|
|
498
|
+
title: "Get History Change Detail",
|
|
499
|
+
description: "Fetch one semantic History change's paginated before/after items only when needed.",
|
|
500
|
+
inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
501
|
+
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
502
|
+
}, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
|
|
503
|
+
assertProFeature(client, "history_change_detail");
|
|
504
|
+
assertHistoryCapture(client);
|
|
505
|
+
return proPage(await client.getHistoryChangeDetail(args.change_id, toQuery({ cursor: args.cursor, limit: args.limit })), "items");
|
|
506
|
+
}));
|
|
507
|
+
registerTool("get_program_history", {
|
|
508
|
+
title: "Get Program History",
|
|
509
|
+
description: "One page of exact semantic History for a platform:handle or Self-Hosted:slug catalog ID. Preserve archive id and catalog_id separately.",
|
|
510
|
+
inputSchema: { program_id: programIdSchema, cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90), limit: proLimitSchema.default(50) },
|
|
511
|
+
outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
512
|
+
}, (args) => runTool("get_program_history", config, rateLimiter, async () => {
|
|
513
|
+
assertProFeature(client, "program_history");
|
|
514
|
+
assertHistoryCapture(client);
|
|
515
|
+
return proPage(await client.getProgramHistory(args.program_id, toQuery({ cursor: args.cursor, days: args.days, limit: args.limit })), "changes");
|
|
516
|
+
}));
|
|
517
|
+
registerTool("get_program_activity", {
|
|
518
|
+
title: "Get Exact Program Activity",
|
|
519
|
+
description: "Read the authoritative activity_summary for one exact program ID.",
|
|
520
|
+
inputSchema: { program_id: programIdSchema },
|
|
521
|
+
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
522
|
+
}, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
|
|
523
|
+
assertProFeature(client, "program_activity");
|
|
524
|
+
return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
|
|
525
|
+
}));
|
|
526
|
+
registerTool("get_analytics_platform_series", {
|
|
527
|
+
title: "Get Platform Daily Analytics",
|
|
528
|
+
description: "One page of inclusive UTC daily platform analytics; inspect generation, projection_pending, and tracking_since.",
|
|
529
|
+
inputSchema: { platform: z.string().min(1).max(100), start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
530
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
531
|
+
}, (args) => runTool("get_analytics_platform_series", config, rateLimiter, async () => {
|
|
532
|
+
assertProFeature(client, "analytics_platform_series", "enabled");
|
|
533
|
+
const { platform, ...query } = args;
|
|
534
|
+
return proPage(await client.getAnalyticsPlatformSeries(platform, toQuery(query)), "series");
|
|
535
|
+
}));
|
|
536
|
+
registerTool("get_analytics_program_activity", {
|
|
537
|
+
title: "Rank Programs by Recorded Activity",
|
|
538
|
+
description: "Server-side daily activity ranking. One page; semantic activity excludes baselines and corrections.",
|
|
539
|
+
inputSchema: { start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), platform: z.string().min(1).max(100).optional(), category: z.enum(["all", "scope", "rewards", "rules", "schedule", "costs", "lifecycle"]).default("all"), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
540
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
541
|
+
}, (args) => runTool("get_analytics_program_activity", config, rateLimiter, async () => {
|
|
542
|
+
assertProFeature(client, "analytics_program_activity", "enabled");
|
|
543
|
+
return proPage(await client.getAnalyticsProgramActivity(toQuery(args)), "programs");
|
|
544
|
+
}));
|
|
545
|
+
registerTool("get_analytics_program_detail", {
|
|
546
|
+
title: "Get Program Daily Analytics",
|
|
547
|
+
description: "One page of program daily metrics, carried inventory state, report observations, and source coverage.",
|
|
548
|
+
inputSchema: { program_id: programIdSchema, start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
549
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
550
|
+
}, (args) => runTool("get_analytics_program_detail", config, rateLimiter, async () => {
|
|
551
|
+
assertProFeature(client, "analytics_program_detail", "enabled");
|
|
552
|
+
const { program_id, ...query } = args;
|
|
553
|
+
return proPage(await client.getAnalyticsProgramDetail(program_id, toQuery(query)), "series");
|
|
554
|
+
}));
|
|
447
555
|
registerTool("search_programs", {
|
|
448
556
|
title: "Search BBRadar Programs",
|
|
449
|
-
description: `Search or list active platform
|
|
557
|
+
description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in because creating one can be slower than live indexed pagination.`,
|
|
450
558
|
inputSchema: {
|
|
451
559
|
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
560
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional(),
|
|
452
561
|
platforms: platformListSchema(),
|
|
453
562
|
tags: tagListSchema(),
|
|
454
563
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
564
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
455
565
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
456
566
|
page: pageSchema,
|
|
457
567
|
page_size: programsPageSizeSchema
|
|
@@ -460,6 +570,7 @@ export function createBbradarServer(client, config) {
|
|
|
460
570
|
annotations: readOnlyAnnotations
|
|
461
571
|
}, (args) => runTool("search_programs", config, rateLimiter, async () => {
|
|
462
572
|
const { vdp_mode, ...filters } = args;
|
|
573
|
+
assertStableSnapshotSupported(client, "programs", filters.snapshot);
|
|
463
574
|
const api = await client.listPrograms(toQuery(filters));
|
|
464
575
|
const data = readObject(api.data);
|
|
465
576
|
const programs = readArray(data?.programs)
|
|
@@ -471,6 +582,47 @@ export function createBbradarServer(client, config) {
|
|
|
471
582
|
meta: sanitizeJson(data?.meta)
|
|
472
583
|
});
|
|
473
584
|
}));
|
|
585
|
+
registerTool("search_targets", {
|
|
586
|
+
title: "Search BBRadar Targets",
|
|
587
|
+
description: `Low-level reverse target search and current target-inventory paging. q may be omitted for inventory traversal. page_size max ${maxTargetSearchPageSize}. Stable snapshots are opt-in and intentionally not used by find_hunt_targets.`,
|
|
588
|
+
inputSchema: {
|
|
589
|
+
q: targetQuerySchema.optional(),
|
|
590
|
+
match_mode: apiTargetMatchModeSchema.default("contains"),
|
|
591
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
592
|
+
platforms: platformListSchema(),
|
|
593
|
+
tags: tagListSchema(),
|
|
594
|
+
include_out_of_scope: z.boolean().default(false),
|
|
595
|
+
include_ineligible: z.boolean().default(false),
|
|
596
|
+
page: pageSchema,
|
|
597
|
+
page_size: targetSearchPageSizeSchema,
|
|
598
|
+
output_mode: targetListModeSchema.default("compact")
|
|
599
|
+
},
|
|
600
|
+
outputSchema: {
|
|
601
|
+
...apiEnvelopeOutputShape(),
|
|
602
|
+
results: z.array(jsonRecordSchema()).optional(),
|
|
603
|
+
meta: jsonRecordSchema().optional(),
|
|
604
|
+
continuation: jsonRecordSchema().optional()
|
|
605
|
+
},
|
|
606
|
+
annotations: readOnlyAnnotations
|
|
607
|
+
}, (args) => runTool("search_targets", config, rateLimiter, async () => {
|
|
608
|
+
const { output_mode, ...filters } = args;
|
|
609
|
+
assertStableSnapshotSupported(client, "target_search", filters.snapshot);
|
|
610
|
+
const api = await client.searchTargets(toQuery(filters));
|
|
611
|
+
const data = readObject(api.data);
|
|
612
|
+
const results = readArray(data?.results).map((value) => {
|
|
613
|
+
const row = readObject(value) ?? {};
|
|
614
|
+
const program = sanitizeProgram(row.program, config.webBaseUrl);
|
|
615
|
+
const target = sanitizeTarget(row.target);
|
|
616
|
+
return stripUndefined({
|
|
617
|
+
program: formatProgram(program, output_mode === "full" ? "full" : "compact"),
|
|
618
|
+
target: output_mode === "full" ? target : compactTarget(target)
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
622
|
+
results,
|
|
623
|
+
meta: sanitizeJson(data?.meta)
|
|
624
|
+
}), "results", "target search results", continuationStore, config);
|
|
625
|
+
}));
|
|
474
626
|
registerTool("get_program", {
|
|
475
627
|
title: "Get BBRadar Program",
|
|
476
628
|
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
|
|
@@ -629,7 +781,8 @@ export function createBbradarServer(client, config) {
|
|
|
629
781
|
title: "Get Recent Target Changes",
|
|
630
782
|
description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
|
|
631
783
|
inputSchema: {
|
|
632
|
-
change_type:
|
|
784
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
785
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
633
786
|
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
|
|
634
787
|
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
635
788
|
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
@@ -658,16 +811,24 @@ export function createBbradarServer(client, config) {
|
|
|
658
811
|
annotations: readOnlyAnnotations
|
|
659
812
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
660
813
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
const
|
|
814
|
+
assertStableSnapshotSupported(client, "target_changes", filters.snapshot);
|
|
815
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor, config);
|
|
816
|
+
const { min_target_score, target_opportunity_label, competition_risk, snapshot, ...upstreamFilters } = filters;
|
|
817
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRecentChanges(toQuery({
|
|
664
818
|
...upstreamFilters,
|
|
665
819
|
minTargetScore: min_target_score,
|
|
666
820
|
targetOpportunityLabel: target_opportunity_label,
|
|
667
821
|
competitionRisk: competition_risk,
|
|
822
|
+
snapshot: activeSnapshot,
|
|
668
823
|
after_id: afterId,
|
|
669
|
-
page: afterId === undefined ? filters.page : 1
|
|
670
|
-
})))
|
|
824
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
825
|
+
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.searchTargets(toQuery({
|
|
826
|
+
q: upstreamFilters.search,
|
|
827
|
+
platforms: upstreamFilters.platforms,
|
|
828
|
+
tags: upstreamFilters.tags,
|
|
829
|
+
page,
|
|
830
|
+
page_size: client.endpointNumber("target_search", "max_page_size", 100)
|
|
831
|
+
}), pageProbe ? { cache: false, coalesce: false } : {}), "results", max_cursor_pages, cursor_time_budget_ms, startPage, recoveryProbe));
|
|
671
832
|
const api = collected.lastApi;
|
|
672
833
|
const data = collected.lastData;
|
|
673
834
|
const sanitizedMeta = readObject(sanitizeJson(data?.meta));
|
|
@@ -682,7 +843,9 @@ export function createBbradarServer(client, config) {
|
|
|
682
843
|
pages_fetched: collected.pagesFetched,
|
|
683
844
|
returned_across_pages: changes.length,
|
|
684
845
|
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
685
|
-
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
846
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted,
|
|
847
|
+
history_gap: collected.gapRecovery !== undefined,
|
|
848
|
+
inventory_baseline: collected.gapRecovery
|
|
686
849
|
}),
|
|
687
850
|
facets: sanitizeJson(data?.facets),
|
|
688
851
|
cursor: collected.cursor,
|
|
@@ -694,6 +857,7 @@ export function createBbradarServer(client, config) {
|
|
|
694
857
|
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.",
|
|
695
858
|
inputSchema: {
|
|
696
859
|
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
860
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
697
861
|
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
698
862
|
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
699
863
|
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
@@ -724,12 +888,20 @@ export function createBbradarServer(client, config) {
|
|
|
724
888
|
throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
|
|
725
889
|
}
|
|
726
890
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
891
|
+
assertStableSnapshotSupported(client, "repository_changes", filters.snapshot);
|
|
892
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor, config);
|
|
893
|
+
const { snapshot, ...upstreamFilters } = filters;
|
|
894
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRepositoryChanges(toQuery({
|
|
895
|
+
...upstreamFilters,
|
|
896
|
+
snapshot: activeSnapshot,
|
|
730
897
|
after_id: afterId,
|
|
731
|
-
page: afterId === undefined ? filters.page : 1
|
|
732
|
-
})))
|
|
898
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
899
|
+
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.getRepositoryChanges(toQuery({
|
|
900
|
+
...upstreamFilters,
|
|
901
|
+
after_id: undefined,
|
|
902
|
+
page,
|
|
903
|
+
page_size: client.endpointNumber("repository_changes", "max_page_size", 100)
|
|
904
|
+
}), pageProbe ? { cache: false, coalesce: false } : {}), "results", max_cursor_pages, cursor_time_budget_ms, startPage, recoveryProbe));
|
|
733
905
|
const api = collected.lastApi;
|
|
734
906
|
const data = collected.lastData;
|
|
735
907
|
return applyCollectionContinuation(withApiMetadata(api, {
|
|
@@ -740,7 +912,9 @@ export function createBbradarServer(client, config) {
|
|
|
740
912
|
pages_fetched: collected.pagesFetched,
|
|
741
913
|
returned_across_pages: collected.results.length,
|
|
742
914
|
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
743
|
-
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
915
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted,
|
|
916
|
+
history_gap: collected.gapRecovery !== undefined,
|
|
917
|
+
inventory_baseline: collected.gapRecovery
|
|
744
918
|
}),
|
|
745
919
|
facets: sanitizeJson(data?.facets),
|
|
746
920
|
available_facets: sanitizeJson(data?.available_facets),
|
|
@@ -750,7 +924,7 @@ export function createBbradarServer(client, config) {
|
|
|
750
924
|
}));
|
|
751
925
|
registerTool("find_hunt_targets", {
|
|
752
926
|
title: "Find Best Hunt Targets",
|
|
753
|
-
description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, and recently added targets. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
|
|
927
|
+
description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, and recently added targets. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Self-Hosted is supported through current inventory and automatically skips its unavailable change feed. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
|
|
754
928
|
inputSchema: {
|
|
755
929
|
platforms: platformListSchema(),
|
|
756
930
|
exclude_platforms: stringListSchema("Platforms to exclude from the final shortlist."),
|
|
@@ -795,7 +969,7 @@ export function createBbradarServer(client, config) {
|
|
|
795
969
|
}, (args) => runTool("find_hunt_targets", config, rateLimiter, async () => findHuntTargets(client, config, args)));
|
|
796
970
|
registerTool("find_repository_hunt_targets", {
|
|
797
971
|
title: "Find Repository-Aware Hunt Targets",
|
|
798
|
-
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
|
|
972
|
+
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 supported platform catalogs, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria. Self-Hosted has no repository stream and is rejected without upstream work.",
|
|
799
973
|
inputSchema: {
|
|
800
974
|
platforms: platformListSchema(),
|
|
801
975
|
exclude_platforms: stringListSchema("Platforms to exclude from final matches."),
|
|
@@ -916,7 +1090,7 @@ export function createBbradarServer(client, config) {
|
|
|
916
1090
|
inputSchema: {
|
|
917
1091
|
program_id: programIdentifierSchema,
|
|
918
1092
|
since: isoDateTimeSchema.optional(),
|
|
919
|
-
change_type:
|
|
1093
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
920
1094
|
target_type: filterValueSchema().optional(),
|
|
921
1095
|
language_tags: languageTagListSchema(),
|
|
922
1096
|
include_removed: z.boolean().default(true),
|
|
@@ -940,7 +1114,7 @@ export function createBbradarServer(client, config) {
|
|
|
940
1114
|
title: "Get Recent Target Activity",
|
|
941
1115
|
description: "Recent target changes grouped by program.",
|
|
942
1116
|
inputSchema: {
|
|
943
|
-
change_type:
|
|
1117
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
944
1118
|
target_type: filterValueSchema().optional(),
|
|
945
1119
|
include_removed: z.boolean().default(true),
|
|
946
1120
|
include_out_of_scope: z.boolean().default(false),
|
|
@@ -1050,6 +1224,7 @@ export function createBbradarServer(client, config) {
|
|
|
1050
1224
|
target_types: targetTypeListSchema(),
|
|
1051
1225
|
language_tags: languageTagListSchema(),
|
|
1052
1226
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
1227
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional().describe("Source-classified program kind; sent to the Pro program list."),
|
|
1053
1228
|
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."),
|
|
1054
1229
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
1055
1230
|
updated_since: isoDateTimeSchema.optional(),
|
|
@@ -1064,7 +1239,7 @@ export function createBbradarServer(client, config) {
|
|
|
1064
1239
|
exclude_mobile_only: z.boolean().default(false),
|
|
1065
1240
|
exclude_non_web: z.boolean().default(false),
|
|
1066
1241
|
has_api_scope: z.boolean().default(false),
|
|
1067
|
-
contest_like: z.boolean().default(false).describe("Require
|
|
1242
|
+
contest_like: z.boolean().default(false).describe("Require source-classified contests."),
|
|
1068
1243
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
1069
1244
|
fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
|
|
1070
1245
|
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
@@ -1426,7 +1601,7 @@ export function createBbradarServer(client, config) {
|
|
|
1426
1601
|
description: "Recent changes by target type grouped by program.",
|
|
1427
1602
|
inputSchema: {
|
|
1428
1603
|
target_type: filterValueSchema(),
|
|
1429
|
-
change_type:
|
|
1604
|
+
change_type: targetChangeTypeSchema.default("added"),
|
|
1430
1605
|
include_removed: z.boolean().default(false),
|
|
1431
1606
|
include_out_of_scope: z.boolean().default(false),
|
|
1432
1607
|
include_ineligible: z.boolean().default(false),
|
|
@@ -1692,7 +1867,8 @@ export function createBbradarServer(client, config) {
|
|
|
1692
1867
|
...apiEnvelopeOutputShape(),
|
|
1693
1868
|
program: programOutputSchema().optional(),
|
|
1694
1869
|
activity: jsonRecordSchema().optional(),
|
|
1695
|
-
changes: z.array(jsonRecordSchema()).optional()
|
|
1870
|
+
changes: z.array(jsonRecordSchema()).optional(),
|
|
1871
|
+
meta: jsonRecordSchema().optional()
|
|
1696
1872
|
},
|
|
1697
1873
|
annotations: readOnlyAnnotations
|
|
1698
1874
|
}, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
|
|
@@ -1896,13 +2072,15 @@ export function createBbradarServer(client, config) {
|
|
|
1896
2072
|
const exportId = randomUUID();
|
|
1897
2073
|
const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
|
|
1898
2074
|
const resourceUri = exportResourceUri(exportId);
|
|
1899
|
-
|
|
2075
|
+
if (!api.noStore)
|
|
2076
|
+
rememberExport(exportStore, exportId, sanitizedExport);
|
|
1900
2077
|
return withApiMetadata(api, {
|
|
1901
2078
|
source_requests: resolvedProgramIds.sourceRequests.length > 0 ? resolvedProgramIds.sourceRequests : undefined,
|
|
1902
2079
|
warnings: resolvedProgramIds.warnings.length > 0 ? resolvedProgramIds.warnings : undefined,
|
|
1903
2080
|
export: {
|
|
1904
|
-
export_id: exportId,
|
|
1905
|
-
resource_uri: resourceUri,
|
|
2081
|
+
export_id: api.noStore ? undefined : exportId,
|
|
2082
|
+
resource_uri: api.noStore ? undefined : resourceUri,
|
|
2083
|
+
data: api.noStore ? sanitizedExport : undefined,
|
|
1906
2084
|
preview: previewExportPayload(sanitizedExport),
|
|
1907
2085
|
returned_preview_count: previewExportCount(sanitizedExport),
|
|
1908
2086
|
limit: args.limit
|
|
@@ -1999,6 +2177,9 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1999
2177
|
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
2000
2178
|
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
2001
2179
|
},
|
|
2180
|
+
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch get_history_change_detail only for a selected change." },
|
|
2181
|
+
{ user_goal: "most active programs in a date range", first_tool: "get_analytics_program_activity", arguments: {}, then: "Use this server-side ranking rather than sampling the program catalog." },
|
|
2182
|
+
{ user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2002
2183
|
{
|
|
2003
2184
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2004
2185
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2141,6 +2322,9 @@ function coreBbradarGuide(topic, client) {
|
|
|
2141
2322
|
first_tool: "get_repository_changes",
|
|
2142
2323
|
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2143
2324
|
},
|
|
2325
|
+
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
|
|
2326
|
+
{ user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
|
|
2327
|
+
{ user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2144
2328
|
{
|
|
2145
2329
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2146
2330
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2518,7 +2702,7 @@ function getCachedProgramFallback(config, requestedProgramId) {
|
|
|
2518
2702
|
return cloneProgramFallbackForRequest(cached.fallback, requestedProgramId, true);
|
|
2519
2703
|
}
|
|
2520
2704
|
function rememberProgramFallback(config, fallback) {
|
|
2521
|
-
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
|
|
2705
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(fallback.sourceRequests)) {
|
|
2522
2706
|
return;
|
|
2523
2707
|
}
|
|
2524
2708
|
pruneProgramFallbackCache();
|
|
@@ -2543,7 +2727,13 @@ function pruneProgramFallbackCache() {
|
|
|
2543
2727
|
}
|
|
2544
2728
|
}
|
|
2545
2729
|
function programFallbackCacheKey(config, requestedProgramId) {
|
|
2546
|
-
return `${config.apiBaseUrl}\n${normalizeTag(requestedProgramId)}`;
|
|
2730
|
+
return `${config.apiBaseUrl}\n${credentialCacheScope(config)}\n${isStrictProgramId(requestedProgramId) ? requestedProgramId : normalizeTag(requestedProgramId)}`;
|
|
2731
|
+
}
|
|
2732
|
+
function credentialCacheScope(config) {
|
|
2733
|
+
return createHash("sha256").update(config.apiKey).digest("hex");
|
|
2734
|
+
}
|
|
2735
|
+
function hasNoStoreSource(sourceRequests) {
|
|
2736
|
+
return sourceRequests.some((request) => readBoolean(request.no_store) === true);
|
|
2547
2737
|
}
|
|
2548
2738
|
function cloneProgramFallbackForRequest(fallback, requestedProgramId, fromCache) {
|
|
2549
2739
|
const warnings = fromCache
|
|
@@ -2997,7 +3187,8 @@ function getCachedResolveProgram(config, input) {
|
|
|
2997
3187
|
});
|
|
2998
3188
|
}
|
|
2999
3189
|
function rememberResolveProgram(config, input, payload) {
|
|
3000
|
-
|
|
3190
|
+
const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
|
|
3191
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(sourceRequests)) {
|
|
3001
3192
|
return;
|
|
3002
3193
|
}
|
|
3003
3194
|
pruneResolveProgramCache();
|
|
@@ -3023,7 +3214,8 @@ function pruneResolveProgramCache() {
|
|
|
3023
3214
|
function resolveProgramCacheKey(config, input) {
|
|
3024
3215
|
return stableStringify({
|
|
3025
3216
|
apiBaseUrl: config.apiBaseUrl,
|
|
3026
|
-
|
|
3217
|
+
credentialScope: credentialCacheScope(config),
|
|
3218
|
+
query: isStrictProgramId(input.query) ? input.query : normalizeTag(input.query),
|
|
3027
3219
|
platforms: input.platforms.map(normalizeTag).sort(),
|
|
3028
3220
|
tags: input.tags.map(normalizeTag).sort(),
|
|
3029
3221
|
opportunity_levels: [...input.opportunity_levels].sort(),
|
|
@@ -4231,8 +4423,9 @@ async function comparePrograms(client, config, input) {
|
|
|
4231
4423
|
});
|
|
4232
4424
|
}
|
|
4233
4425
|
async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
|
|
4234
|
-
const [programApi, changes] = await Promise.all([
|
|
4426
|
+
const [programApi, activityApi, changes] = await Promise.all([
|
|
4235
4427
|
client.getProgram(programId),
|
|
4428
|
+
client.supportsEndpoint("program_activity") ? client.getProgramActivity(programId) : Promise.resolve(undefined),
|
|
4236
4429
|
recentChangesLimit > 0
|
|
4237
4430
|
? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
|
|
4238
4431
|
: Promise.resolve(undefined)
|
|
@@ -4249,15 +4442,18 @@ async function summarizeProgramActivity(client, config, programId, recentChanges
|
|
|
4249
4442
|
upstream_request_id: programApi.upstreamRequestId,
|
|
4250
4443
|
...apiSourceMetadata(programApi)
|
|
4251
4444
|
},
|
|
4445
|
+
...(activityApi ? [{ source: "program_activity", request_id: activityApi.requestId, upstream_request_id: activityApi.upstreamRequestId, ...apiSourceMetadata(activityApi) }] : []),
|
|
4252
4446
|
...(changes?.sourceRequests ?? [])
|
|
4253
4447
|
],
|
|
4254
4448
|
program,
|
|
4255
4449
|
activity: stripUndefined({
|
|
4450
|
+
activity_summary: activityApi ? sanitizeProPayload(readObject(activityApi.data)?.activity_summary) : undefined,
|
|
4256
4451
|
target_counts: readObject(scopeSummary?.target_counts),
|
|
4257
4452
|
target_activity: targetActivity,
|
|
4258
4453
|
recent_change_count: changes?.changes.length
|
|
4259
4454
|
}),
|
|
4260
|
-
changes: changes?.changes ?? []
|
|
4455
|
+
changes: changes?.changes ?? [],
|
|
4456
|
+
meta: { target_change_samples_are_semantic_history: false, activity_source: activityApi ? "pro_activity_endpoint" : "legacy_program_projection" }
|
|
4261
4457
|
};
|
|
4262
4458
|
}
|
|
4263
4459
|
async function getLatestAddedTargets(client, config, input) {
|
|
@@ -4407,21 +4603,50 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4407
4603
|
return true;
|
|
4408
4604
|
}
|
|
4409
4605
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4410
|
-
const
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4606
|
+
const pageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
4607
|
+
// `eligibility=eligible` is an API-level in-scope bounty-eligibility filter,
|
|
4608
|
+
// so the common eligible-only path needs no second local scope pass.
|
|
4609
|
+
const directOffsetPaging = !input.include_ineligible || (!input.strict_scope_filter && input.include_out_of_scope);
|
|
4610
|
+
const collected = [];
|
|
4611
|
+
let matchedSeen = directOffsetPaging ? Math.floor(input.offset / pageSize) * pageSize : 0;
|
|
4612
|
+
let page = directOffsetPaging ? Math.floor(input.offset / pageSize) + 1 : 1;
|
|
4613
|
+
const firstPage = page;
|
|
4614
|
+
let api;
|
|
4615
|
+
let upstreamMeta;
|
|
4616
|
+
let upstreamHasMore = false;
|
|
4617
|
+
do {
|
|
4618
|
+
api = await client.getProgramTargets(programId, {
|
|
4619
|
+
search: input.search,
|
|
4620
|
+
target_type: input.target_type,
|
|
4621
|
+
eligibility: input.include_ineligible ? "all" : "eligible",
|
|
4622
|
+
minTargetScore: input.min_target_score,
|
|
4623
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
4624
|
+
competitionRisk: input.competition_risk,
|
|
4625
|
+
page,
|
|
4626
|
+
page_size: pageSize
|
|
4627
|
+
});
|
|
4628
|
+
const data = readObject(api.data);
|
|
4629
|
+
upstreamMeta = readObject(data?.meta);
|
|
4630
|
+
for (const target of readArray(data?.targets).map(sanitizeTarget)) {
|
|
4631
|
+
if (!targetHasAllowedScope(target, input))
|
|
4632
|
+
continue;
|
|
4633
|
+
if (matchedSeen >= input.offset && collected.length < input.limit)
|
|
4634
|
+
collected.push(target);
|
|
4635
|
+
matchedSeen += 1;
|
|
4636
|
+
}
|
|
4637
|
+
upstreamHasMore = readBoolean(upstreamMeta?.has_more) === true;
|
|
4638
|
+
const totalPages = readNumber(upstreamMeta?.total_pages);
|
|
4639
|
+
page += 1;
|
|
4640
|
+
if (totalPages !== undefined && page > totalPages) {
|
|
4641
|
+
upstreamHasMore = false;
|
|
4642
|
+
}
|
|
4643
|
+
} while (upstreamHasMore &&
|
|
4644
|
+
page <= MAX_UPSTREAM_PAGE &&
|
|
4645
|
+
collected.length < input.limit);
|
|
4646
|
+
if (!api) {
|
|
4647
|
+
throw new Error("No program target response was collected.");
|
|
4648
|
+
}
|
|
4649
|
+
const targets = collected;
|
|
4425
4650
|
return withApiMetadata(api, {
|
|
4426
4651
|
program_id: programId,
|
|
4427
4652
|
targets: formatTargetList(targets, input.output_mode),
|
|
@@ -4429,12 +4654,16 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4429
4654
|
offset: input.offset,
|
|
4430
4655
|
limit: input.limit,
|
|
4431
4656
|
output_mode: input.output_mode,
|
|
4432
|
-
total_active_targets: readNumber(upstreamMeta?.total) ??
|
|
4433
|
-
total_after_filters:
|
|
4657
|
+
total_active_targets: readNumber(upstreamMeta?.total) ?? matchedSeen,
|
|
4658
|
+
total_after_filters: upstreamHasMore ? undefined : matchedSeen,
|
|
4434
4659
|
returned: targets.length,
|
|
4435
|
-
has_more: input.offset +
|
|
4436
|
-
upstream_has_more:
|
|
4437
|
-
possibly_incomplete:
|
|
4660
|
+
has_more: upstreamHasMore || matchedSeen > input.offset + targets.length,
|
|
4661
|
+
upstream_has_more: upstreamHasMore,
|
|
4662
|
+
possibly_incomplete: upstreamHasMore,
|
|
4663
|
+
upstream_pages_fetched: page - firstPage,
|
|
4664
|
+
upstream_start_page: firstPage,
|
|
4665
|
+
direct_offset_paging: directOffsetPaging,
|
|
4666
|
+
upstream_page_limit_reached: upstreamHasMore && page > MAX_UPSTREAM_PAGE,
|
|
4438
4667
|
data_capabilities: readObject(upstreamMeta?.data_capabilities),
|
|
4439
4668
|
target_source: "program_targets"
|
|
4440
4669
|
}
|
|
@@ -4480,10 +4709,12 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
|
4480
4709
|
requestId: api.requestId,
|
|
4481
4710
|
upstreamRequestId: api.upstreamRequestId,
|
|
4482
4711
|
fetchedAt: api.fetchedAt,
|
|
4712
|
+
noStore: api.noStore,
|
|
4483
4713
|
cache: readObject(stripUndefined({
|
|
4484
4714
|
hit: api.cached,
|
|
4485
4715
|
coalesced_live_request: api.coalesced,
|
|
4486
|
-
expires_at: api.cacheExpiresAt
|
|
4716
|
+
expires_at: api.cacheExpiresAt,
|
|
4717
|
+
no_store: api.noStore
|
|
4487
4718
|
})),
|
|
4488
4719
|
source: "program_targets",
|
|
4489
4720
|
rawTargets,
|
|
@@ -4541,7 +4772,7 @@ function getCachedProgramTargetsFallback(client, programId) {
|
|
|
4541
4772
|
}
|
|
4542
4773
|
function rememberProgramTargetsFallback(client, programId, lookup) {
|
|
4543
4774
|
const diagnostics = client.diagnostics();
|
|
4544
|
-
if (!diagnostics.response_cache.enabled || lookup.unavailable) {
|
|
4775
|
+
if (!diagnostics.response_cache.enabled || lookup.unavailable || lookup.noStore) {
|
|
4545
4776
|
return;
|
|
4546
4777
|
}
|
|
4547
4778
|
const cache = programTargetsFallbackCacheForClient(client);
|
|
@@ -4579,7 +4810,7 @@ function pruneProgramTargetsFallbackCache(cache) {
|
|
|
4579
4810
|
}
|
|
4580
4811
|
}
|
|
4581
4812
|
function programTargetsFallbackCacheKey(programId) {
|
|
4582
|
-
return
|
|
4813
|
+
return programId;
|
|
4583
4814
|
}
|
|
4584
4815
|
function cacheTtlMsFromDiagnostics(cache) {
|
|
4585
4816
|
return cache.enabled ? cache.ttl_ms : 0;
|
|
@@ -4598,10 +4829,12 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
4598
4829
|
requestId: api.requestId,
|
|
4599
4830
|
upstreamRequestId: api.upstreamRequestId,
|
|
4600
4831
|
fetchedAt: api.fetchedAt,
|
|
4832
|
+
noStore: api.noStore,
|
|
4601
4833
|
cache: readObject(stripUndefined({
|
|
4602
4834
|
hit: api.cached,
|
|
4603
4835
|
coalesced_live_request: api.coalesced,
|
|
4604
|
-
expires_at: api.cacheExpiresAt
|
|
4836
|
+
expires_at: api.cacheExpiresAt,
|
|
4837
|
+
no_store: api.noStore
|
|
4605
4838
|
})),
|
|
4606
4839
|
source: "target_export",
|
|
4607
4840
|
rawTargets,
|
|
@@ -4648,7 +4881,8 @@ async function getProgramFromTargetsOnlyFallback(client, programId, programError
|
|
|
4648
4881
|
cache: readObject(stripUndefined({
|
|
4649
4882
|
hit: targetsApi.cached,
|
|
4650
4883
|
coalesced_live_request: targetsApi.coalesced,
|
|
4651
|
-
expires_at: targetsApi.cacheExpiresAt
|
|
4884
|
+
expires_at: targetsApi.cacheExpiresAt,
|
|
4885
|
+
no_store: targetsApi.noStore
|
|
4652
4886
|
})),
|
|
4653
4887
|
source_requests: [
|
|
4654
4888
|
stripUndefined({
|
|
@@ -4793,8 +5027,6 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4793
5027
|
generated_at: new Date().toISOString()
|
|
4794
5028
|
});
|
|
4795
5029
|
const exportId = randomUUID();
|
|
4796
|
-
const resourceUri = exportResourceUri(exportId);
|
|
4797
|
-
rememberExport(exportStore, exportId, exportPayload);
|
|
4798
5030
|
return stripUndefined({
|
|
4799
5031
|
request_id: randomUUID(),
|
|
4800
5032
|
source_requests: sourceRequests,
|
|
@@ -4802,7 +5034,7 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4802
5034
|
errors: errors.length > 0 ? errors : undefined,
|
|
4803
5035
|
export: {
|
|
4804
5036
|
export_id: exportId,
|
|
4805
|
-
|
|
5037
|
+
data: exportPayload,
|
|
4806
5038
|
preview: previewExportPayload(exportPayload),
|
|
4807
5039
|
returned_preview_count: previewExportCount(exportPayload),
|
|
4808
5040
|
limit: input.limit
|
|
@@ -5529,6 +5761,10 @@ function applyCollectionContinuation(payload, field, label, store, config) {
|
|
|
5529
5761
|
if (!items || items.length <= inlineLimit) {
|
|
5530
5762
|
return payload;
|
|
5531
5763
|
}
|
|
5764
|
+
// A no-store page stays only in this tool result, never in a reusable resource.
|
|
5765
|
+
if (readBoolean(readObject(payload.cache)?.no_store) === true) {
|
|
5766
|
+
return payload;
|
|
5767
|
+
}
|
|
5532
5768
|
const continuationId = randomUUID();
|
|
5533
5769
|
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5534
5770
|
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
@@ -5785,28 +6021,41 @@ function formatProgram(program, mode) {
|
|
|
5785
6021
|
function compactProgram(program) {
|
|
5786
6022
|
return stripUndefined({
|
|
5787
6023
|
id: stringField(program, "id"),
|
|
6024
|
+
catalog_id: stringField(program, "catalog_id"),
|
|
5788
6025
|
handle: stringField(program, "handle"),
|
|
5789
6026
|
name: stringField(program, "name"),
|
|
5790
6027
|
platform: stringField(program, "platform"),
|
|
5791
6028
|
catalog: stringField(program, "catalog"),
|
|
6029
|
+
program_kind: stringField(program, "program_kind"),
|
|
6030
|
+
program_status: stringField(program, "program_status"),
|
|
6031
|
+
is_active: readBoolean(program.is_active),
|
|
5792
6032
|
program_type: stringField(program, "program_type"),
|
|
5793
6033
|
platform_url: stringField(program, "platform_url"),
|
|
5794
6034
|
bbradar_url: stringField(program, "bbradar_url"),
|
|
5795
6035
|
policy_url: stringField(program, "policy_url"),
|
|
5796
6036
|
reporting_url: stringField(program, "reporting_url"),
|
|
5797
6037
|
reward_status: stringField(program, "reward_status"),
|
|
5798
|
-
data_capabilities:
|
|
5799
|
-
opportunity_tag:
|
|
6038
|
+
data_capabilities: program.data_capabilities,
|
|
6039
|
+
opportunity_tag: program.opportunity_tag,
|
|
5800
6040
|
competition_risk: stringField(program, "competition_risk"),
|
|
5801
6041
|
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5802
|
-
submission_cost:
|
|
5803
|
-
reward_range:
|
|
5804
|
-
|
|
6042
|
+
submission_cost: program.submission_cost,
|
|
6043
|
+
reward_range: program.reward_range,
|
|
6044
|
+
reward_range_label: stringField(program, "reward_range_label"),
|
|
6045
|
+
public_report_count: program.public_report_count,
|
|
5805
6046
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
5806
6047
|
first_seen_date: stringField(program, "first_seen_date"),
|
|
6048
|
+
launch_at: program.launch_at,
|
|
6049
|
+
launch_at_precision: stringField(program, "launch_at_precision"),
|
|
6050
|
+
ends_at: program.ends_at,
|
|
6051
|
+
ends_on: program.ends_on,
|
|
6052
|
+
verification: program.verification,
|
|
5807
6053
|
last_updated: stringField(program, "last_updated"),
|
|
5808
6054
|
targets_updated_at: stringField(program, "targets_updated_at"),
|
|
5809
6055
|
scope_summary: readObject(program.scope_summary),
|
|
6056
|
+
match_qualities: Array.isArray(program.match_qualities)
|
|
6057
|
+
? readStringArrayField(program, "match_qualities")
|
|
6058
|
+
: undefined,
|
|
5810
6059
|
resource_links: readObject(program.resource_links)
|
|
5811
6060
|
});
|
|
5812
6061
|
}
|
|
@@ -5867,6 +6116,7 @@ function formatRepositoryChange(change, mode) {
|
|
|
5867
6116
|
})
|
|
5868
6117
|
: undefined,
|
|
5869
6118
|
changes_url: stringField(change, "changes_url"),
|
|
6119
|
+
comparison: sanitizeJson(change.comparison),
|
|
5870
6120
|
programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
|
|
5871
6121
|
targets: Array.isArray(change.targets)
|
|
5872
6122
|
? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
|
|
@@ -6121,7 +6371,7 @@ function candidateLooksLikeCtf(candidate) {
|
|
|
6121
6371
|
return candidate.looksLikeCtf;
|
|
6122
6372
|
}
|
|
6123
6373
|
function candidateLooksContestLike(candidate) {
|
|
6124
|
-
return
|
|
6374
|
+
return stringField(candidate.program, "program_kind") === "contest";
|
|
6125
6375
|
}
|
|
6126
6376
|
function candidateHasKnownNoBounty(candidate) {
|
|
6127
6377
|
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
@@ -6351,7 +6601,11 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6351
6601
|
async function findHuntTargets(client, config, input) {
|
|
6352
6602
|
const warnings = [];
|
|
6353
6603
|
const errors = [];
|
|
6354
|
-
const
|
|
6604
|
+
const selfHostedOnly = isSelfHostedOnlyPlatformSelection(input.platforms);
|
|
6605
|
+
const targetChangesEndpointSupported = client.supportsEndpoint("target_changes");
|
|
6606
|
+
const recentChangesEnabled = input.include_recent_changes &&
|
|
6607
|
+
!selfHostedOnly &&
|
|
6608
|
+
targetChangesEndpointSupported;
|
|
6355
6609
|
const desiredRecentChangeQueries = recentChangesEnabled ? huntRecentChangeQueries(input) : [];
|
|
6356
6610
|
const recentChangeQueries = desiredRecentChangeQueries.slice(0, Math.max(0, input.upstream_request_budget - 2));
|
|
6357
6611
|
const recentRequestReserve = recentChangeQueries.length;
|
|
@@ -6399,7 +6653,10 @@ async function findHuntTargets(client, config, input) {
|
|
|
6399
6653
|
output_mode: "compact",
|
|
6400
6654
|
upstream_request_budget: programRequestBudget
|
|
6401
6655
|
};
|
|
6402
|
-
if (input.include_recent_changes &&
|
|
6656
|
+
if (input.include_recent_changes && selfHostedOnly) {
|
|
6657
|
+
warnings.push("Self-Hosted does not expose target-change history; recent target evidence was derived only from current target-inventory timestamps.");
|
|
6658
|
+
}
|
|
6659
|
+
else if (input.include_recent_changes && !recentChangesEnabled) {
|
|
6403
6660
|
warnings.push("The negotiated API contract does not expose target changes; ranked current programs without recent-change enrichment.");
|
|
6404
6661
|
}
|
|
6405
6662
|
else if (recentChangeQueries.length < desiredRecentChangeQueries.length) {
|
|
@@ -6502,6 +6759,7 @@ async function findHuntTargets(client, config, input) {
|
|
|
6502
6759
|
exact_target_programs_sampled: targetResult.targetsByProgram.size,
|
|
6503
6760
|
recent_changes_scanned: recentResult.changes.length,
|
|
6504
6761
|
recent_programs_matched: recentByProgram.size,
|
|
6762
|
+
recent_change_catalog_supported: !selfHostedOnly && targetChangesEndpointSupported,
|
|
6505
6763
|
model_round_trips_expected: 1,
|
|
6506
6764
|
parallel_recent_change_fetch: recentChangeQueries.length > 0,
|
|
6507
6765
|
bulk_resource_links_suppressed: true,
|
|
@@ -6747,7 +7005,9 @@ function buildHuntTargetMatch(entry, targetsByProgram, recentByProgram, input) {
|
|
|
6747
7005
|
(readNumber(rankFactors.target_surface_score) ?? 0) * 0.25);
|
|
6748
7006
|
const freshnessScore = huntFreshnessScore(latestRecentAt, recentTargets.length, readNumber(rankFactors.added_7d));
|
|
6749
7007
|
const publicReports = readNumber(rankFactors.public_report_count);
|
|
6750
|
-
|
|
7008
|
+
// Missing report evidence is unknown, not low competition. Keep the
|
|
7009
|
+
// internal ranking conservative and do not expose a made-up numeric signal.
|
|
7010
|
+
const competitionScore = publicReports === undefined ? 20 : Math.max(0, 100 - Math.min(100, publicReports / 10));
|
|
6751
7011
|
const bestTargetScore = Math.max(0, ...targetSamples.map((target) => readNumber(target.target_opportunity_score) ?? 0));
|
|
6752
7012
|
const baseHuntScore = readNumber(rankFactors.hunt_value_score) ?? 0;
|
|
6753
7013
|
const compositeScore = huntCompositeScore(input.ranking_mode, baseHuntScore, rewardScore, scopeScore, freshnessScore, competitionScore, bestTargetScore);
|
|
@@ -6775,7 +7035,8 @@ function buildHuntTargetMatch(entry, targetsByProgram, recentByProgram, input) {
|
|
|
6775
7035
|
reward_score: roundScore(rewardScore),
|
|
6776
7036
|
scope_score: roundScore(scopeScore),
|
|
6777
7037
|
freshness_score: roundScore(freshnessScore),
|
|
6778
|
-
competition_score: roundScore(competitionScore),
|
|
7038
|
+
competition_score: publicReports === undefined ? undefined : roundScore(competitionScore),
|
|
7039
|
+
competition_signal: publicReports === undefined ? "unknown" : "public_report_count",
|
|
6779
7040
|
best_sample_target_score: bestTargetScore > 0 ? bestTargetScore : undefined,
|
|
6780
7041
|
recent_added_targets_in_window: recentTargets.length,
|
|
6781
7042
|
latest_added_target_at: latestRecentAt,
|
|
@@ -6880,11 +7141,52 @@ function compareHuntTargetMatches(left, right, mode) {
|
|
|
6880
7141
|
}
|
|
6881
7142
|
async function findRepositoryHuntTargets(client, config, input) {
|
|
6882
7143
|
const warnings = [];
|
|
7144
|
+
const repositoryPlatforms = input.platforms.filter((platform) => !isSelfHostedPlatform(platform));
|
|
7145
|
+
const selfHostedRequested = repositoryPlatforms.length !== input.platforms.length;
|
|
7146
|
+
if (input.platforms.length > 0 && repositoryPlatforms.length === 0) {
|
|
7147
|
+
return {
|
|
7148
|
+
request_id: randomUUID(),
|
|
7149
|
+
fetched_at: new Date().toISOString(),
|
|
7150
|
+
source_requests: [],
|
|
7151
|
+
warnings: [
|
|
7152
|
+
"Self-Hosted does not expose repository-change events or target intelligence, so repository-aware hunting is unavailable for this catalog. Use find_hunt_targets for current Self-Hosted program and target inventory."
|
|
7153
|
+
],
|
|
7154
|
+
filters: {
|
|
7155
|
+
platforms: input.platforms,
|
|
7156
|
+
surface_mode: input.surface_mode,
|
|
7157
|
+
ranking_mode: input.ranking_mode,
|
|
7158
|
+
output_mode: input.output_mode
|
|
7159
|
+
},
|
|
7160
|
+
upstream_budget: {
|
|
7161
|
+
initial: input.upstream_request_budget,
|
|
7162
|
+
used: 0,
|
|
7163
|
+
remaining: input.upstream_request_budget
|
|
7164
|
+
},
|
|
7165
|
+
ranking_scope: {
|
|
7166
|
+
mode: "unsupported_catalog",
|
|
7167
|
+
possibly_incomplete: false
|
|
7168
|
+
},
|
|
7169
|
+
programs: [],
|
|
7170
|
+
matches: [],
|
|
7171
|
+
meta: {
|
|
7172
|
+
returned: 0,
|
|
7173
|
+
matched_programs: 0,
|
|
7174
|
+
program_candidates: 0,
|
|
7175
|
+
repository_events_scanned: 0,
|
|
7176
|
+
qualifying_targets_before_result_limit: 0,
|
|
7177
|
+
repository_request_attempts: 0,
|
|
7178
|
+
parallel_source_fetch: false
|
|
7179
|
+
}
|
|
7180
|
+
};
|
|
7181
|
+
}
|
|
7182
|
+
if (selfHostedRequested) {
|
|
7183
|
+
warnings.push("Self-Hosted was excluded from repository-aware matching because that catalog has no repository-change stream; other requested platforms were processed normally.");
|
|
7184
|
+
}
|
|
6883
7185
|
const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
|
|
6884
7186
|
const programRequestBudget = Math.max(1, input.upstream_request_budget - repositoryRequestReserve);
|
|
6885
7187
|
const programInput = {
|
|
6886
|
-
platforms:
|
|
6887
|
-
exclude_platforms: input.exclude_platforms,
|
|
7188
|
+
platforms: repositoryPlatforms,
|
|
7189
|
+
exclude_platforms: uniqueStrings([...input.exclude_platforms, "Self-Hosted"]),
|
|
6888
7190
|
tags: input.tags,
|
|
6889
7191
|
scope_tags: input.scope_tags,
|
|
6890
7192
|
target_types: input.target_types,
|
|
@@ -6926,7 +7228,7 @@ async function findRepositoryHuntTargets(client, config, input) {
|
|
|
6926
7228
|
};
|
|
6927
7229
|
const [programPayload, repository] = await Promise.all([
|
|
6928
7230
|
findPrograms(client, config, programInput),
|
|
6929
|
-
fetchRepositoryHuntPage(client, input, warnings, repositoryRequestReserve)
|
|
7231
|
+
fetchRepositoryHuntPage(client, { ...input, platforms: repositoryPlatforms }, warnings, repositoryRequestReserve)
|
|
6930
7232
|
]);
|
|
6931
7233
|
const candidatePrograms = readArray(programPayload.programs)
|
|
6932
7234
|
.map(readObject)
|
|
@@ -7175,6 +7477,7 @@ async function findPrograms(client, config, input) {
|
|
|
7175
7477
|
const collected = await collectPrograms(client, {
|
|
7176
7478
|
platforms: input.platforms,
|
|
7177
7479
|
tags: upstreamTags,
|
|
7480
|
+
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
|
|
7178
7481
|
updated_since: input.updated_since,
|
|
7179
7482
|
opportunity_levels: input.opportunity_levels,
|
|
7180
7483
|
max_pages: input.max_pages,
|
|
@@ -7223,6 +7526,7 @@ async function findPrograms(client, config, input) {
|
|
|
7223
7526
|
exclude_non_web: input.exclude_non_web,
|
|
7224
7527
|
has_api_scope: input.has_api_scope,
|
|
7225
7528
|
contest_like: input.contest_like ? true : undefined,
|
|
7529
|
+
program_kind: input.program_kind,
|
|
7226
7530
|
vdp_mode: input.vdp_mode && input.vdp_mode !== "include" ? input.vdp_mode : undefined,
|
|
7227
7531
|
fresh_launch_days: input.fresh_launch_days,
|
|
7228
7532
|
min_wildcards: input.min_wildcards,
|
|
@@ -7652,6 +7956,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
|
|
|
7652
7956
|
platforms: options.platforms,
|
|
7653
7957
|
tags: options.tags,
|
|
7654
7958
|
updated_since: options.updated_since,
|
|
7959
|
+
program_kind: source.kind === "programs" ? options.program_kind : undefined,
|
|
7655
7960
|
page,
|
|
7656
7961
|
page_size: MAX_PROGRAMS_PER_SEARCH
|
|
7657
7962
|
});
|
|
@@ -7737,6 +8042,13 @@ function toProgramCandidate(rawProgram, webBaseUrl) {
|
|
|
7737
8042
|
return candidate;
|
|
7738
8043
|
}
|
|
7739
8044
|
function programCandidateMatches(candidate, filters) {
|
|
8045
|
+
if (filters.program_kind && stringField(candidate.program, "program_kind") !== filters.program_kind) {
|
|
8046
|
+
return false;
|
|
8047
|
+
}
|
|
8048
|
+
const programStatus = normalizeTag(stringField(candidate.program, "program_status") ?? "");
|
|
8049
|
+
if (readBoolean(candidate.program.is_active) === false || programStatus === "paused") {
|
|
8050
|
+
return false;
|
|
8051
|
+
}
|
|
7740
8052
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
7741
8053
|
return false;
|
|
7742
8054
|
}
|
|
@@ -8062,6 +8374,12 @@ function uniqueStrings(values) {
|
|
|
8062
8374
|
function normalizeTag(value) {
|
|
8063
8375
|
return value.trim().toLowerCase();
|
|
8064
8376
|
}
|
|
8377
|
+
function isSelfHostedPlatform(value) {
|
|
8378
|
+
return normalizeTag(value).replace(/[\s_]+/g, "-") === "self-hosted";
|
|
8379
|
+
}
|
|
8380
|
+
function isSelfHostedOnlyPlatformSelection(platforms) {
|
|
8381
|
+
return platforms.length > 0 && platforms.every(isSelfHostedPlatform);
|
|
8382
|
+
}
|
|
8065
8383
|
function tokenSet(value) {
|
|
8066
8384
|
return new Set(value.split(/[^a-z0-9]+/).filter((token) => token.length >= 2));
|
|
8067
8385
|
}
|
|
@@ -8089,7 +8407,7 @@ function registerPrompts(server, profile) {
|
|
|
8089
8407
|
tags: z.string().optional().describe("Optional comma-separated scope/language/target-type tags."),
|
|
8090
8408
|
max_programs: z.string().optional().describe("Optional maximum shortlist size.")
|
|
8091
8409
|
}
|
|
8092
|
-
}, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets.
|
|
8410
|
+
}, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Check each result's cache metadata before describing its freshness.
|
|
8093
8411
|
|
|
8094
8412
|
Find the best BBRadar program candidates.
|
|
8095
8413
|
${discoveryToolInstructions}
|
|
@@ -8140,8 +8458,12 @@ The plan must:
|
|
|
8140
8458
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
8141
8459
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
8142
8460
|
}
|
|
8143
|
-
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
8144
|
-
const normalizedFilters = stableStringify(
|
|
8461
|
+
function resolveFeedCursor(store, endpoint, filters, resume, config) {
|
|
8462
|
+
const normalizedFilters = stableStringify({
|
|
8463
|
+
api_base_url: config.apiBaseUrl,
|
|
8464
|
+
credential_scope: credentialCacheScope(config),
|
|
8465
|
+
filters: normalizeFeedCursorFilters(filters)
|
|
8466
|
+
});
|
|
8145
8467
|
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
8146
8468
|
const explicitAfterId = readNumber(filters.after_id);
|
|
8147
8469
|
if (explicitAfterId !== undefined) {
|
|
@@ -8172,7 +8494,7 @@ function advanceFeedCursor(store, cursor, rawMeta) {
|
|
|
8172
8494
|
stored_in_memory: true
|
|
8173
8495
|
};
|
|
8174
8496
|
}
|
|
8175
|
-
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
|
|
8497
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, initialPage, fetchPage, refreshInventory) {
|
|
8176
8498
|
const startedAt = Date.now();
|
|
8177
8499
|
const results = [];
|
|
8178
8500
|
const sourceRequests = [];
|
|
@@ -8184,9 +8506,114 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8184
8506
|
let hasMore = false;
|
|
8185
8507
|
let pageBudgetExhausted = false;
|
|
8186
8508
|
let timeBudgetExhausted = false;
|
|
8509
|
+
let gapRecovery;
|
|
8510
|
+
let activeSnapshot = initialSnapshot;
|
|
8187
8511
|
do {
|
|
8188
|
-
|
|
8512
|
+
let api;
|
|
8513
|
+
try {
|
|
8514
|
+
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot, activeSnapshot ? initialPage + pagesFetched : 1);
|
|
8515
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8516
|
+
}
|
|
8517
|
+
catch (error) {
|
|
8518
|
+
if (!(error instanceof BBRadarApiError) ||
|
|
8519
|
+
error.status !== 410 ||
|
|
8520
|
+
currentCursor.afterId === undefined ||
|
|
8521
|
+
activeSnapshot !== undefined ||
|
|
8522
|
+
!refreshInventory) {
|
|
8523
|
+
throw error;
|
|
8524
|
+
}
|
|
8525
|
+
const recoveryNonce = `${Date.now()}-${currentCursor.afterId}`;
|
|
8526
|
+
const markerApi = await fetchPage(undefined, `${recoveryNonce}-marker`);
|
|
8527
|
+
const markerData = readObject(markerApi.data);
|
|
8528
|
+
const markerAfterId = feedBaselineAfterId(markerData);
|
|
8529
|
+
let progress = feedRecoveryProgress.get(store)?.get(currentCursor.key);
|
|
8530
|
+
let restartedForDrift = false;
|
|
8531
|
+
if (progress && (progress.previousAfterId !== currentCursor.afterId ||
|
|
8532
|
+
progress.baselineAfterId !== markerAfterId)) {
|
|
8533
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8534
|
+
progress = undefined;
|
|
8535
|
+
restartedForDrift = true;
|
|
8536
|
+
}
|
|
8537
|
+
const beforeAfterId = progress?.baselineAfterId ?? markerAfterId;
|
|
8538
|
+
const startPage = progress?.nextPage ?? 1;
|
|
8539
|
+
const inventoryPart = await refreshInventory(startPage, `${recoveryNonce}-inventory`);
|
|
8540
|
+
const inventory = {
|
|
8541
|
+
...inventoryPart,
|
|
8542
|
+
rows_observed: (progress?.rowsObserved ?? 0) + (readNumber(inventoryPart.rows_observed) ?? 0),
|
|
8543
|
+
pages_fetched: (progress?.pagesFetched ?? 0) + (readNumber(inventoryPart.pages_fetched) ?? 0),
|
|
8544
|
+
resumed_from_page: startPage,
|
|
8545
|
+
restarted_for_feed_drift: restartedForDrift
|
|
8546
|
+
};
|
|
8547
|
+
const inventoryComplete = readBoolean(inventory.complete) === true;
|
|
8548
|
+
const endpointPageLimitReached = readBoolean(inventory.endpoint_page_limit_reached) === true;
|
|
8549
|
+
const afterApi = inventoryComplete ? await fetchPage(undefined, `${recoveryNonce}-after`) : markerApi;
|
|
8550
|
+
const afterData = readObject(afterApi.data);
|
|
8551
|
+
const afterAfterId = feedBaselineAfterId(afterData);
|
|
8552
|
+
const baselineStable = inventoryComplete && beforeAfterId !== undefined && beforeAfterId === afterAfterId;
|
|
8553
|
+
const nextAfterId = baselineStable ? beforeAfterId : currentCursor.afterId;
|
|
8554
|
+
if (baselineStable) {
|
|
8555
|
+
store.set(currentCursor.key, nextAfterId);
|
|
8556
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8557
|
+
}
|
|
8558
|
+
else if (!inventoryComplete && !endpointPageLimitReached && beforeAfterId !== undefined) {
|
|
8559
|
+
// Persist only the unchanged expired bookmark so resume_cursor can
|
|
8560
|
+
// continue recovery without treating partial inventory as a baseline.
|
|
8561
|
+
store.set(currentCursor.key, currentCursor.afterId);
|
|
8562
|
+
setRecoveryCheckpoint(store, currentCursor.key, {
|
|
8563
|
+
previousAfterId: currentCursor.afterId,
|
|
8564
|
+
baselineAfterId: beforeAfterId,
|
|
8565
|
+
nextPage: readNumber(inventoryPart.next_page) ?? startPage,
|
|
8566
|
+
rowsObserved: readNumber(inventory.rows_observed) ?? 0,
|
|
8567
|
+
pagesFetched: readNumber(inventory.pages_fetched) ?? 0
|
|
8568
|
+
});
|
|
8569
|
+
}
|
|
8570
|
+
else {
|
|
8571
|
+
// A failed first explicit recovery must remain resumable even when no
|
|
8572
|
+
// safe inventory checkpoint can be retained.
|
|
8573
|
+
store.set(currentCursor.key, currentCursor.afterId);
|
|
8574
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8575
|
+
}
|
|
8576
|
+
gapRecovery = {
|
|
8577
|
+
historical_gap: true,
|
|
8578
|
+
current_inventory_refreshed: inventoryComplete,
|
|
8579
|
+
recovery_complete: baselineStable,
|
|
8580
|
+
retained_previous_cursor: !baselineStable,
|
|
8581
|
+
incomplete_reason: !inventoryComplete
|
|
8582
|
+
? endpointPageLimitReached ? "inventory_endpoint_page_limit_reached" : "inventory_page_or_time_budget_exhausted"
|
|
8583
|
+
: beforeAfterId === undefined
|
|
8584
|
+
? "feed_baseline_unavailable"
|
|
8585
|
+
: beforeAfterId !== afterAfterId
|
|
8586
|
+
? "feed_changed_during_inventory_refresh"
|
|
8587
|
+
: undefined,
|
|
8588
|
+
inventory,
|
|
8589
|
+
next_inventory_page: baselineStable ? undefined : readNumber(inventoryPart.next_page),
|
|
8590
|
+
previous_after_id: currentCursor.afterId,
|
|
8591
|
+
baseline_after_id: baselineStable ? nextAfterId : undefined,
|
|
8592
|
+
baseline_events_emitted: 0,
|
|
8593
|
+
limitation: "Changes retired before recovery cannot be reconstructed."
|
|
8594
|
+
};
|
|
8595
|
+
lastApi = afterApi;
|
|
8596
|
+
lastData = afterData;
|
|
8597
|
+
cursorPayload = {
|
|
8598
|
+
endpoint: currentCursor.endpoint,
|
|
8599
|
+
mode: baselineStable ? "recovered_baseline" : "recovery_incomplete",
|
|
8600
|
+
used_after_id: currentCursor.afterId,
|
|
8601
|
+
next_after_id: nextAfterId,
|
|
8602
|
+
has_more: false,
|
|
8603
|
+
stored_in_memory: true
|
|
8604
|
+
};
|
|
8605
|
+
pagesFetched += 1;
|
|
8606
|
+
break;
|
|
8607
|
+
}
|
|
8189
8608
|
const data = readObject(api.data);
|
|
8609
|
+
const returnedSnapshot = readString(readObject(data?.meta)?.snapshot);
|
|
8610
|
+
if (activeSnapshot && !returnedSnapshot)
|
|
8611
|
+
throw new Error("Stable snapshot response omitted its token; discard this page and start a new snapshot.");
|
|
8612
|
+
if (activeSnapshot && activeSnapshot !== "start" && returnedSnapshot !== activeSnapshot)
|
|
8613
|
+
throw new Error("Stable snapshot changed between pages; discard these pages and start a new snapshot.");
|
|
8614
|
+
if (activeSnapshot === "start" && returnedSnapshot) {
|
|
8615
|
+
activeSnapshot = returnedSnapshot;
|
|
8616
|
+
}
|
|
8190
8617
|
const pageResults = readArray(data?.results);
|
|
8191
8618
|
lastApi = api;
|
|
8192
8619
|
lastData = data;
|
|
@@ -8229,12 +8656,61 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8229
8656
|
pagesFetched,
|
|
8230
8657
|
pageBudgetExhausted,
|
|
8231
8658
|
timeBudgetExhausted,
|
|
8232
|
-
sourceRequests
|
|
8659
|
+
sourceRequests,
|
|
8660
|
+
gapRecovery
|
|
8661
|
+
};
|
|
8662
|
+
}
|
|
8663
|
+
const feedRecoveryProgress = new WeakMap();
|
|
8664
|
+
function recoveryProgressFor(store) {
|
|
8665
|
+
let progress = feedRecoveryProgress.get(store);
|
|
8666
|
+
if (!progress) {
|
|
8667
|
+
progress = new Map();
|
|
8668
|
+
feedRecoveryProgress.set(store, progress);
|
|
8669
|
+
}
|
|
8670
|
+
return progress;
|
|
8671
|
+
}
|
|
8672
|
+
function setRecoveryCheckpoint(store, key, checkpoint) {
|
|
8673
|
+
const progress = recoveryProgressFor(store);
|
|
8674
|
+
if (!progress.has(key) && progress.size >= 500) {
|
|
8675
|
+
const oldest = progress.keys().next().value;
|
|
8676
|
+
if (oldest !== undefined)
|
|
8677
|
+
progress.delete(oldest);
|
|
8678
|
+
}
|
|
8679
|
+
progress.delete(key);
|
|
8680
|
+
progress.set(key, checkpoint);
|
|
8681
|
+
}
|
|
8682
|
+
function feedBaselineAfterId(data) {
|
|
8683
|
+
const metaAfterId = readNumber(readObject(data?.meta)?.next_after_id);
|
|
8684
|
+
const ids = readArray(data?.results)
|
|
8685
|
+
.map((entry) => readNumber(readObject(entry)?.id))
|
|
8686
|
+
.filter((value) => value !== undefined);
|
|
8687
|
+
return ids.length > 0 ? Math.max(metaAfterId ?? 0, ...ids) : metaAfterId;
|
|
8688
|
+
}
|
|
8689
|
+
async function refreshInventoryBaseline(fetchPage, collection, maxPages, timeBudgetMs, startPage = 1, recoveryProbe = "inventory") {
|
|
8690
|
+
const startedAt = Date.now();
|
|
8691
|
+
let page = startPage;
|
|
8692
|
+
let rows = 0;
|
|
8693
|
+
let hasMore = false;
|
|
8694
|
+
do {
|
|
8695
|
+
const api = await fetchPage(page, `${recoveryProbe}-${page}`);
|
|
8696
|
+
const data = readObject(api.data);
|
|
8697
|
+
rows += readArray(data?.[collection]).length;
|
|
8698
|
+
hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
|
|
8699
|
+
page += 1;
|
|
8700
|
+
} while (hasMore && page < startPage + maxPages && Date.now() - startedAt < timeBudgetMs);
|
|
8701
|
+
return {
|
|
8702
|
+
collection,
|
|
8703
|
+
rows_observed: rows,
|
|
8704
|
+
pages_fetched: page - startPage,
|
|
8705
|
+
next_page: hasMore && page <= MAX_UPSTREAM_PAGE ? page : undefined,
|
|
8706
|
+
endpoint_page_limit_reached: hasMore && page > MAX_UPSTREAM_PAGE,
|
|
8707
|
+
complete: !hasMore,
|
|
8708
|
+
bounded: true
|
|
8233
8709
|
};
|
|
8234
8710
|
}
|
|
8235
8711
|
function normalizeFeedCursorFilters(filters) {
|
|
8236
8712
|
return Object.fromEntries(Object.entries(filters)
|
|
8237
|
-
.filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
|
|
8713
|
+
.filter(([key, value]) => key !== "after_id" && key !== "page" && key !== "snapshot" && value !== undefined)
|
|
8238
8714
|
.map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
|
|
8239
8715
|
}
|
|
8240
8716
|
function normalizeFeedCursorFilterValue(value) {
|
|
@@ -8337,21 +8813,49 @@ function withApiMetadata(api, payload) {
|
|
|
8337
8813
|
hit: api.cached,
|
|
8338
8814
|
stale_while_revalidate: api.stale,
|
|
8339
8815
|
coalesced_live_request: api.coalesced,
|
|
8340
|
-
expires_at: api.cacheExpiresAt
|
|
8816
|
+
expires_at: api.cacheExpiresAt,
|
|
8817
|
+
no_store: api.noStore
|
|
8341
8818
|
}),
|
|
8342
8819
|
fetched_at: api.fetchedAt,
|
|
8343
8820
|
...payload
|
|
8344
8821
|
});
|
|
8345
8822
|
}
|
|
8823
|
+
function proPage(api, field) {
|
|
8824
|
+
const raw = readObject(api.data);
|
|
8825
|
+
if (!raw || !(field === "activity_summary" ? readObject(raw[field]) : Array.isArray(raw[field]))) {
|
|
8826
|
+
throw new Error(`BBRadar Pro API returned a malformed page: expected ${field}.`);
|
|
8827
|
+
}
|
|
8828
|
+
const data = sanitizeProPayload(api.data);
|
|
8829
|
+
return { ...data, ...withApiMetadata(api, {}) };
|
|
8830
|
+
}
|
|
8831
|
+
function assertProFeature(client, endpoint, flag) {
|
|
8832
|
+
const state = client.capabilitySnapshot();
|
|
8833
|
+
if (state.status !== "negotiated")
|
|
8834
|
+
return;
|
|
8835
|
+
const capability = client.endpointCapability(endpoint);
|
|
8836
|
+
if (!capability)
|
|
8837
|
+
throw new Error(`The negotiated BBRadar API does not advertise ${endpoint}. Upgrade the API deployment to use this tool.`);
|
|
8838
|
+
if (flag && capability[flag] === false) {
|
|
8839
|
+
throw new Error(`${endpoint} is advertised but ${flag}=false on this API deployment.`);
|
|
8840
|
+
}
|
|
8841
|
+
}
|
|
8842
|
+
function assertHistoryCapture(client) {
|
|
8843
|
+
if (client.capabilitySnapshot().status === "negotiated" &&
|
|
8844
|
+
client.endpointCapability("history_changes")?.history_capture_enabled === false) {
|
|
8845
|
+
throw new Error("History endpoints are advertised but history_capture_enabled=false on this API deployment.");
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8346
8848
|
function apiSourceMetadata(api) {
|
|
8347
8849
|
return stripUndefined({
|
|
8348
8850
|
cache_hit: api.cached,
|
|
8349
8851
|
cache_expires_at: api.cacheExpiresAt,
|
|
8350
|
-
coalesced_live_request: api.coalesced
|
|
8852
|
+
coalesced_live_request: api.coalesced,
|
|
8853
|
+
no_store: api.noStore
|
|
8351
8854
|
});
|
|
8352
8855
|
}
|
|
8353
8856
|
function errorPayload(error, decision, toolName, durationMs) {
|
|
8354
8857
|
if (error instanceof BBRadarApiError) {
|
|
8858
|
+
const recovery = error.status === 410 ? recoveryForGone(error, toolName) : undefined;
|
|
8355
8859
|
return stripUndefined({
|
|
8356
8860
|
request_id: error.requestId,
|
|
8357
8861
|
upstream_request_id: error.upstreamRequestId,
|
|
@@ -8361,6 +8865,7 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8361
8865
|
detail: sanitizeJson(error.detail),
|
|
8362
8866
|
errors: sanitizeJson(error.errors)
|
|
8363
8867
|
}),
|
|
8868
|
+
recovery,
|
|
8364
8869
|
mcp_rate_limit: toRateLimitPayload(decision),
|
|
8365
8870
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8366
8871
|
});
|
|
@@ -8374,6 +8879,19 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8374
8879
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8375
8880
|
};
|
|
8376
8881
|
}
|
|
8882
|
+
function recoveryForGone(error, toolName) {
|
|
8883
|
+
const detail = typeof error.detail === "string" ? error.detail.toLowerCase() : error.message.toLowerCase();
|
|
8884
|
+
if (detail.includes("retained analytics") || detail.includes("precede")) {
|
|
8885
|
+
return { action: "choose_supported_interval", message: "Choose dates inside the retained analytics window." };
|
|
8886
|
+
}
|
|
8887
|
+
if (detail.includes("snapshot")) {
|
|
8888
|
+
return { action: "start_new_snapshot", message: "Restart on page 1 with snapshot=start; do not combine pages from the old snapshot." };
|
|
8889
|
+
}
|
|
8890
|
+
if (toolName.startsWith("get_history") || toolName === "get_program_history" || toolName.startsWith("get_analytics")) {
|
|
8891
|
+
return { action: "restart_query", message: "Restart this History or analytics query without its expired opaque cursor, keeping the intended filters." };
|
|
8892
|
+
}
|
|
8893
|
+
return { action: "reconcile_inventory", message: "The retained numeric feed has a gap; reconcile current inventory before seeding a new after_id." };
|
|
8894
|
+
}
|
|
8377
8895
|
function recordToolMetric(toolName, durationMs, status) {
|
|
8378
8896
|
const existing = TOOL_METRICS.get(toolName) ??
|
|
8379
8897
|
{
|
|
@@ -8441,6 +8959,11 @@ function splitFilterList(value) {
|
|
|
8441
8959
|
function toQuery(values) {
|
|
8442
8960
|
return compactRecord(values);
|
|
8443
8961
|
}
|
|
8962
|
+
function assertStableSnapshotSupported(client, endpoint, snapshot) {
|
|
8963
|
+
if (snapshot !== undefined && !client.supportsStableSnapshot(endpoint)) {
|
|
8964
|
+
throw new Error(`The negotiated BBRadar API contract does not advertise stable snapshots for ${endpoint}. Omit snapshot to use live pagination.`);
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8444
8967
|
function compactRecord(values) {
|
|
8445
8968
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => {
|
|
8446
8969
|
if (value === undefined || value === null) {
|
|
@@ -8487,11 +9010,19 @@ function compactTarget(target) {
|
|
|
8487
9010
|
identifier: targetIdentifier(target),
|
|
8488
9011
|
target_type: stringField(target, "target_type"),
|
|
8489
9012
|
in_scope: readBoolean(target.in_scope),
|
|
8490
|
-
eligible_for_bounty:
|
|
9013
|
+
eligible_for_bounty: target.eligible_for_bounty,
|
|
8491
9014
|
wildcard: readBoolean(target.wildcard),
|
|
8492
|
-
|
|
9015
|
+
path_kind: stringField(target, "path_kind"),
|
|
9016
|
+
path_prefix: stringField(target, "path_prefix"),
|
|
9017
|
+
match_quality: stringField(target, "match_quality"),
|
|
9018
|
+
match_reason: stringField(target, "match_reason"),
|
|
9019
|
+
target_opportunity_score: target.target_opportunity_score,
|
|
8493
9020
|
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
9021
|
+
score_version: target.score_version,
|
|
9022
|
+
evidence_coverage: target.evidence_coverage,
|
|
9023
|
+
target_score_breakdown: target.target_score_breakdown,
|
|
8494
9024
|
competition_risk: stringField(target, "competition_risk"),
|
|
9025
|
+
competition_risk_reason: stringField(target, "competition_risk_reason"),
|
|
8495
9026
|
why_this_target: stringField(target, "why_this_target")
|
|
8496
9027
|
});
|
|
8497
9028
|
}
|