@bbradar/mcp 0.3.0 → 0.3.2
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 +19 -0
- package/README.md +89 -9
- package/benchmark/live-api-check.mjs +126 -0
- package/benchmark/live-hunt.mjs +105 -0
- package/benchmark/live-stdio.mjs +80 -0
- package/dist/activityContract.d.ts +2 -0
- package/dist/activityContract.js +32 -0
- package/dist/activityContract.js.map +1 -1
- package/dist/bbradarClient.d.ts +5 -1
- package/dist/bbradarClient.js +24 -3
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.js +37 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +271 -122
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { requiresActivityQuery } from "./activityContract.js";
|
|
2
|
+
import { requiresActivityQuery, validateActivityCatalogInput } from "./activityContract.js";
|
|
3
3
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
@@ -7,7 +7,7 @@ import { NumericCursorStore } from "./cursorStore.js";
|
|
|
7
7
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
8
8
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
9
9
|
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
10
|
-
const SERVER_VERSION = "0.3.
|
|
10
|
+
const SERVER_VERSION = "0.3.2";
|
|
11
11
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
12
12
|
const MAX_TARGET_EXPORT = 500;
|
|
13
13
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -46,7 +46,7 @@ const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
|
46
46
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
47
47
|
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
48
48
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
49
|
-
const SDK_VERSION = "1.
|
|
49
|
+
const SDK_VERSION = "1.32.2";
|
|
50
50
|
const CORE_TOOL_NAMES = new Set([
|
|
51
51
|
"search_programs",
|
|
52
52
|
"get_program",
|
|
@@ -257,11 +257,11 @@ const stableSnapshotSchema = z
|
|
|
257
257
|
.string()
|
|
258
258
|
.trim()
|
|
259
259
|
.min(1)
|
|
260
|
-
.max(
|
|
260
|
+
.max(32, "Catalog snapshot tokens have at most 32 characters. Use 'start', then meta.snapshot; Activity snapshot IDs belong only to Activity and payout tools.")
|
|
261
261
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
262
262
|
message: "Snapshot tokens cannot contain control characters."
|
|
263
263
|
})
|
|
264
|
-
.describe("Optional
|
|
264
|
+
.describe("Optional catalog/feed snapshot token, at most 32 characters. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Do not pass get_activity_snapshot's snapshot_id here. Snapshot creation can be slower; omit it for the fastest live result.");
|
|
265
265
|
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.");
|
|
266
266
|
const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
|
|
267
267
|
const proLimitSchema = z.number().int().min(1).max(100);
|
|
@@ -294,7 +294,7 @@ const ACTIVITY_SHARED_PARAMS = ["tab", "sort", "direction", "platforms", "tags",
|
|
|
294
294
|
// a request so it cannot be silently ignored or reinterpreted.
|
|
295
295
|
const ACTIVITY_TAB_PARAMS = {
|
|
296
296
|
payouts: ["period", "basis", "precision"],
|
|
297
|
-
value: ["include_map"],
|
|
297
|
+
value: ["period", "include_map"],
|
|
298
298
|
scope: ["period"],
|
|
299
299
|
rewards: ["period"],
|
|
300
300
|
updates: ["period", "category"],
|
|
@@ -309,7 +309,7 @@ const activityEventPeriodSchema = () => z.enum(["7d", "30d", "90d"]);
|
|
|
309
309
|
const activityCatalogNumberSchema = () => z.number().finite().min(0).optional();
|
|
310
310
|
const activityCatalogInput = {
|
|
311
311
|
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
312
|
-
payout_metric: z.enum(["payout_total", "payout_increase", "payout_native_90d"]).optional(),
|
|
312
|
+
payout_metric: z.enum(["payout_total", "payout_increase", "payout_native_90d"]).optional().describe("payout_total requires all_time; payout_increase is a tracked counter increase in 24h/7d/30d/90d; payout_native_90d requires 90d and is provider-reported. Omit to select the period's default metric."),
|
|
313
313
|
payout_period: activityCountPeriodSchema().optional(),
|
|
314
314
|
payout_precision: z.enum(["exact", "proven_bounds"]).optional(),
|
|
315
315
|
payout_min: activityCatalogNumberSchema(), payout_max: activityCatalogNumberSchema(),
|
|
@@ -322,7 +322,7 @@ const activityCatalogInput = {
|
|
|
322
322
|
reward_change: z.enum(["increased", "decreased"]).optional(), reward_change_min_pct: activityCatalogNumberSchema(),
|
|
323
323
|
updates_period: activityEventPeriodSchema().optional(),
|
|
324
324
|
active_days_min: activityCatalogNumberSchema(), changes_min: activityCatalogNumberSchema(),
|
|
325
|
-
change_category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
325
|
+
change_category: z.enum(["scope", "rewards", "rules", "program"]).optional().describe("Update-event category; requires its own updates_period. scope_period with scope_added_min already filters scope additions and does not need this field."),
|
|
326
326
|
reports_period: activityCountPeriodSchema().optional(),
|
|
327
327
|
reports_min: activityCatalogNumberSchema(), reports_max: activityCatalogNumberSchema(),
|
|
328
328
|
reports_change: z.enum(["rising", "falling"]).optional(), reports_change_min_pct: activityCatalogNumberSchema(),
|
|
@@ -336,7 +336,7 @@ const activityCatalogInput = {
|
|
|
336
336
|
activity_labels: z.array(z.string().min(1).max(64)).max(10).optional(),
|
|
337
337
|
sort_by: activityCatalogSortSchema.optional(), sort_direction: z.enum(["asc", "desc"]).optional()
|
|
338
338
|
};
|
|
339
|
-
const activityCatalogGuidance = "Give each Activity signal its period
|
|
339
|
+
const activityCatalogGuidance = "Give each Activity signal its own period. payout_total requires all_time; payout_increase uses 24h/7d/30d/90d; payout_native_90d requires 90d. change_category requires updates_period even when scope_period is present; scope_period with scope_added_min needs no change_category. Use enabled sort_by/sort_direction combinations; default is updated descending, and an omitted direction for first_response or time_to_bounty means ascending (fastest first). Tracked no_increase/lower require 30d. Catalog snapshot is 'start' or meta.snapshot, never an Activity snapshot_id. An empty result does not authorize relaxing the user's thresholds or restricting platforms to a recent-feed sample.";
|
|
340
340
|
const freeSubmissionOnlySchema = z.boolean().default(false).describe("Only programs with no detected submission fee or deposit.");
|
|
341
341
|
const programIdSchema = z
|
|
342
342
|
.string()
|
|
@@ -348,6 +348,7 @@ const programIdSchema = z
|
|
|
348
348
|
message: "program_id cannot contain control characters."
|
|
349
349
|
})
|
|
350
350
|
.describe("platform:handle id.");
|
|
351
|
+
const PROGRAM_IDENTIFIER_DESCRIPTION = "BBRadar platform:handle program_id or a human program name/handle. Names are resolved automatically.";
|
|
351
352
|
const programIdentifierSchema = z
|
|
352
353
|
.string()
|
|
353
354
|
.trim()
|
|
@@ -356,9 +357,9 @@ const programIdentifierSchema = z
|
|
|
356
357
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
357
358
|
message: "Program identifiers cannot contain control characters."
|
|
358
359
|
})
|
|
359
|
-
.describe(
|
|
360
|
+
.describe(PROGRAM_IDENTIFIER_DESCRIPTION);
|
|
360
361
|
function jsonRecordSchema() {
|
|
361
|
-
return z.record(z.unknown());
|
|
362
|
+
return z.record(z.string(), z.unknown());
|
|
362
363
|
}
|
|
363
364
|
function programOutputSchema() {
|
|
364
365
|
return jsonRecordSchema();
|
|
@@ -380,6 +381,13 @@ function apiEnvelopeOutputShape() {
|
|
|
380
381
|
error: jsonRecordSchema().optional()
|
|
381
382
|
};
|
|
382
383
|
}
|
|
384
|
+
// Tools that return one Pro API payload as it is. The API adds response fields
|
|
385
|
+
// between MCP releases, and clients validate each structured result against the
|
|
386
|
+
// discovered schema: listed fields document the contract, while a new top-level
|
|
387
|
+
// field validates instead of failing every call (the API added Status facets on 2026-10-13).
|
|
388
|
+
function apiMirrorOutput(shape) {
|
|
389
|
+
return z.object(shape).passthrough();
|
|
390
|
+
}
|
|
383
391
|
function programListOutputShape() {
|
|
384
392
|
return {
|
|
385
393
|
...apiEnvelopeOutputShape(),
|
|
@@ -444,7 +452,13 @@ function createProfiledToolRegistrar(server, profile, client) {
|
|
|
444
452
|
const extra = args[args.length - 1];
|
|
445
453
|
return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
|
|
446
454
|
});
|
|
447
|
-
|
|
455
|
+
// Raw SDK shapes otherwise strip unknown arguments. Every advertised
|
|
456
|
+
// filter must be applied or rejected, including on the older shortcuts.
|
|
457
|
+
const inputSchema = toolConfig.inputSchema;
|
|
458
|
+
const strictConfig = inputSchema && !(inputSchema instanceof z.ZodType)
|
|
459
|
+
? { ...toolConfig, inputSchema: z.object(inputSchema).strict() }
|
|
460
|
+
: toolConfig;
|
|
461
|
+
const registeredTool = server.registerTool(name, strictConfig, scopedCallback);
|
|
448
462
|
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
449
463
|
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
450
464
|
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
@@ -462,7 +476,8 @@ export function createBbradarServer(client, config) {
|
|
|
462
476
|
"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.",
|
|
463
477
|
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
464
478
|
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities, then get_activity_rankings with the matching tab for one ranking. For several conditions at once (e.g. recent payouts plus new scope, Web2-only, low competition, no submission fee) use one find_hunt_targets or find_programs call with Activity inputs and local filters together. Keep periods explicit; never intersect separately paginated rankings.",
|
|
465
|
-
"Activity data is published in versions. get_activity_snapshot
|
|
479
|
+
"Activity data is published in versions. get_activity_snapshot's snapshot_id binds only Activity capabilities, Activity rankings and program payouts. Catalog/feed tools use snapshot=start then meta.snapshot instead; do not send an Activity snapshot_id to find_programs. Never mix rows from different versions.",
|
|
480
|
+
"Preserve the user's thresholds, periods and platform scope when discovery times out or returns no matches. A sampled recent-change feed does not enumerate all supported platforms. Inspect partial_success, errors, ranking_scope and scan before claiming complete coverage.",
|
|
466
481
|
"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.",
|
|
467
482
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
468
483
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -474,7 +489,8 @@ export function createBbradarServer(client, config) {
|
|
|
474
489
|
"Do not repeat program discovery with alternate sorts or fan out get_program_targets across a shortlist; find_hunt_targets already includes exact bounded samples and recent target additions.",
|
|
475
490
|
"Use advanced tools only after the user's goal is clear or the guide points to them.",
|
|
476
491
|
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities. Use get_activity_rankings (or its per-view alias) for one ranking, or one find_hunt_targets/find_programs call combining Activity inputs with Web2, scope, competition and fee filters. Keep server order and explicit periods; never re-rank a sampled catalog or intersect separate pages.",
|
|
477
|
-
"Activity data is published in versions. get_activity_snapshot
|
|
492
|
+
"Activity data is published in versions. get_activity_snapshot's snapshot_id binds only Activity capabilities, Activity rankings and program payouts. Catalog/feed tools use snapshot=start then meta.snapshot instead; do not send an Activity snapshot_id to find_programs. Never mix rows from different versions.",
|
|
493
|
+
"Preserve the user's thresholds, periods and platform scope when discovery times out or returns no matches. A sampled recent-change feed does not enumerate all supported platforms. Inspect partial_success, errors, ranking_scope and scan before claiming complete coverage.",
|
|
478
494
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
479
495
|
"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.",
|
|
480
496
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
@@ -509,8 +525,8 @@ export function createBbradarServer(client, config) {
|
|
|
509
525
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
510
526
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
511
527
|
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
512
|
-
const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
|
|
513
|
-
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() };
|
|
528
|
+
const historyOutput = apiMirrorOutput({ ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() });
|
|
529
|
+
const analyticsOutput = apiMirrorOutput({ ...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() });
|
|
514
530
|
// Published Activity versions are 64 lowercase hex characters. "start" and
|
|
515
531
|
// catalog snapshot tokens are different contracts and are rejected here.
|
|
516
532
|
const activitySnapshotSchema = z.string().regex(/^[0-9a-f]{64}$/, "Use a snapshot_id returned by Activity.")
|
|
@@ -529,7 +545,7 @@ export function createBbradarServer(client, config) {
|
|
|
529
545
|
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
530
546
|
search: z.string().min(1).max(200).optional(),
|
|
531
547
|
current_status: z.enum(["active", "paused", "suspended", "disabled", "closed", "unavailable", "unknown"]).optional(),
|
|
532
|
-
labels: z.array(z.string().min(1).max(64)).max(10).optional().describe("Only labels listed by get_activity_capabilities; others return 422."),
|
|
548
|
+
labels: z.array(z.string().min(1).max(64)).max(10).optional().describe("Only labels listed by get_activity_capabilities (for tab=value, the chosen period view's labels); others return 422."),
|
|
533
549
|
opportunity_tiers: z.array(opportunityLevelSchema).min(1).max(4).optional().describe("Any of these catalog opportunity tiers, as frozen into the Activity version. get_activity_capabilities lists the tiers present in facets.opportunity_tiers; an API without the filter is rejected before any request."),
|
|
534
550
|
launched_within: z.enum(["7d", "30d", "90d"]).optional(),
|
|
535
551
|
category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
@@ -539,19 +555,23 @@ export function createBbradarServer(client, config) {
|
|
|
539
555
|
cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(25),
|
|
540
556
|
snapshot: activitySnapshotSchema.optional()
|
|
541
557
|
};
|
|
542
|
-
const activityOutput = {
|
|
558
|
+
const activityOutput = apiMirrorOutput({
|
|
543
559
|
...apiEnvelopeOutputShape(), rows: z.array(jsonRecordSchema()).optional(), points: z.array(jsonRecordSchema()).optional(),
|
|
544
560
|
tab: z.string().optional(), period: z.string().nullable().optional(),
|
|
545
561
|
basis: z.string().nullable().optional(), precision: z.string().nullable().optional(),
|
|
546
562
|
sort: z.string().nullable().optional(), direction: z.string().optional(), meta: jsonRecordSchema().optional(),
|
|
547
563
|
cohort_median: jsonRecordSchema().nullable().optional(), available_views: z.array(jsonRecordSchema()).optional(),
|
|
548
|
-
map_points_omitted: z.number().int().optional()
|
|
549
|
-
|
|
564
|
+
map_points_omitted: z.number().int().optional(),
|
|
565
|
+
// tab=value: the label shortlist applied, including a default landing list (empty = the map's programs).
|
|
566
|
+
labels: z.array(z.string()).optional(),
|
|
567
|
+
// tab=status: event counts per change_type and current_status, each under the other filters.
|
|
568
|
+
facets: jsonRecordSchema().optional()
|
|
569
|
+
});
|
|
550
570
|
registerTool("get_activity_capabilities", {
|
|
551
571
|
title: "Get Available Activity Views",
|
|
552
572
|
description: "Read the exact enabled period, sort, direction, basis and precision combinations before choosing an Activity ranking. Availability is evaluated before user filters; do not infer combinations from independent enums. Honor each view's sort_directions and default_direction and the enabled label list. The snapshot block identifies the published version these capabilities belong to.",
|
|
553
573
|
inputSchema: z.object({ snapshot: activitySnapshotSchema.optional() }).strict(),
|
|
554
|
-
outputSchema: { ...apiEnvelopeOutputShape(), tabs: z.array(jsonRecordSchema()).optional(), version: z.union([z.string(), z.number()]).optional(), facets: jsonRecordSchema().optional(), labels: z.array(z.string()).optional(), listing: jsonRecordSchema().optional(), listing_payouts_enabled: z.boolean().optional(), derived_signals_enabled: z.boolean().optional(), snapshot: jsonRecordSchema().optional() },
|
|
574
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), tabs: z.array(jsonRecordSchema()).optional(), version: z.union([z.string(), z.number()]).optional(), facets: jsonRecordSchema().optional(), labels: z.array(z.string()).optional(), listing: jsonRecordSchema().optional(), listing_payouts_enabled: z.boolean().optional(), derived_signals_enabled: z.boolean().optional(), snapshot: jsonRecordSchema().optional() }),
|
|
555
575
|
annotations: readOnlyAnnotations
|
|
556
576
|
}, (args) => runTool("get_activity_capabilities", config, rateLimiter, async () => {
|
|
557
577
|
assertProFeature(client, "activity_capabilities", "enabled");
|
|
@@ -560,9 +580,9 @@ export function createBbradarServer(client, config) {
|
|
|
560
580
|
}));
|
|
561
581
|
registerTool("get_activity_snapshot", {
|
|
562
582
|
title: "Get Current Activity Version",
|
|
563
|
-
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds.
|
|
583
|
+
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds. Pass snapshot_id as snapshot only to get_activity_capabilities, get_activity_rankings and get_program_payouts. Catalog/feed tools such as find_programs use snapshot=start then meta.snapshot instead. as_of is when BBRadar froze its calculation, not a provider payment or update time.",
|
|
564
584
|
inputSchema: z.object({}).strict(),
|
|
565
|
-
outputSchema: { ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() },
|
|
585
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() }),
|
|
566
586
|
annotations: readOnlyAnnotations
|
|
567
587
|
}, () => runTool("get_activity_snapshot", config, rateLimiter, async () => {
|
|
568
588
|
assertProFeature(client, "activity_snapshot", "enabled");
|
|
@@ -575,11 +595,11 @@ export function createBbradarServer(client, config) {
|
|
|
575
595
|
}));
|
|
576
596
|
registerTool("get_program_payouts", {
|
|
577
597
|
title: "Get Program Payouts and Response Facts",
|
|
578
|
-
description: "Read reported totals, measured counter increases, source dates, precision, report and response facts, derived signals and label explanations for one exact platform:handle. Keep null separate from zero, bounds separate from exact amounts and unknown payment dates null. Payout timestamps are when increases were seen, never payment dates. No provider requests are made.",
|
|
598
|
+
description: "Read reported totals, measured counter increases, source dates, precision, report and response facts, derived signals and label explanations for one exact platform:handle. facts also carry each platform's own published figures (paid_average, paid_maximum, last_paid_on, HackerOne per-severity averages and report counts, triage/resolution times, response rate, researcher counts), each with its measure, unit, period and source_label; keep each definition and never compare them across platforms as one measure. Keep null separate from zero, bounds separate from exact amounts and unknown payment dates null. Payout timestamps are when increases were seen, never payment dates. value_points (include_map) are same-platform peers in the Value period named by value_period. No provider requests are made.",
|
|
579
599
|
inputSchema: z.object({ program_id: programIdSchema, snapshot: activitySnapshotSchema.optional(), include_map: includeMapSchema }).strict(),
|
|
580
|
-
//
|
|
600
|
+
// Lists the API response fields, including its version identity: a client that
|
|
581
601
|
// discovered this schema validates every structured result against it.
|
|
582
|
-
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), program: jsonRecordSchema().optional(), summary: jsonRecordSchema().optional(), as_of: z.string().optional(), snapshot_id: z.string().optional(), expires_at: z.string().nullable().optional(), facts: z.array(jsonRecordSchema()).optional(), windows: jsonRecordSchema().optional(), series: z.array(jsonRecordSchema()).optional(), payout_summary: jsonRecordSchema().nullable().optional(), views: z.array(jsonRecordSchema()).optional(), reports: jsonRecordSchema().optional(), response: jsonRecordSchema().optional(), last_increase_at: z.string().nullable().optional(), signals: jsonRecordSchema().optional(), label_details: z.array(jsonRecordSchema()).optional(), labels: z.array(z.string()).optional(), value_points: z.array(jsonRecordSchema()).optional(), map_points_omitted: z.number().int().optional(), meta: jsonRecordSchema().optional() },
|
|
602
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), program_id: z.string().optional(), program: jsonRecordSchema().optional(), summary: jsonRecordSchema().optional(), as_of: z.string().optional(), snapshot_id: z.string().optional(), expires_at: z.string().nullable().optional(), facts: z.array(jsonRecordSchema()).optional(), windows: jsonRecordSchema().optional(), series: z.array(jsonRecordSchema()).optional(), payout_summary: jsonRecordSchema().nullable().optional(), views: z.array(jsonRecordSchema()).optional(), reports: jsonRecordSchema().optional(), response: jsonRecordSchema().optional(), last_increase_at: z.string().nullable().optional(), signals: jsonRecordSchema().optional(), label_details: z.array(jsonRecordSchema()).optional(), labels: z.array(z.string()).optional(), value_points: z.array(jsonRecordSchema()).optional(), value_period: z.string().nullable().optional(), map_points_omitted: z.number().int().optional(), meta: jsonRecordSchema().optional() }),
|
|
583
603
|
annotations: readOnlyAnnotations
|
|
584
604
|
}, (args) => runTool("get_program_payouts", config, rateLimiter, async () => {
|
|
585
605
|
assertProFeature(client, "program_payouts", "enabled");
|
|
@@ -598,7 +618,7 @@ export function createBbradarServer(client, config) {
|
|
|
598
618
|
};
|
|
599
619
|
registerTool("get_activity_rankings", {
|
|
600
620
|
title: "Get Activity Ranking",
|
|
601
|
-
description: "One globally ranked server page of an Activity view, frozen at meta.as_of. Read get_activity_capabilities first: explicit period/basis/precision/sort/direction never fall back, and each tab only accepts its own inputs. tab: payouts (reported and tracked bases stay separate; proven_bounds desc = highest reported minimum, asc = lowest reported maximum, never an exact ranking; source totals can lag); value (
|
|
621
|
+
description: "One globally ranked server page of an Activity view, frozen at meta.as_of. Read get_activity_capabilities first: explicit period/basis/precision/sort/direction never fall back, and each tab only accepts its own inputs. tab: payouts (reported and tracked bases stay separate; proven_bounds desc = highest reported minimum, asc = lowest reported maximum, never an exact ranking; source totals can lag; average_paid is the platform's own current average payment, a range for HackerOne); value (advertised period 7d/30d/90d of report volume against lifetime payout per processed report; each row's value holds that period's count and percentile; Pays near top, No payouts seen and processed_received are 90d only; without period, labels or sort mean 90d and a bare read opens the advertised default, whose shortlist the page's labels name; one label gives its shortlist); scope (recorded additions/removals/net, not inventory differences); rewards (net reward-range change comparing native maxima, unknown % stays null; minimum_usd/maximum_usd are display conversions); updates (active UTC days, change groups); reports (received counts; volume is not competition; all_time is the lifetime counter); response (resolved counts, last resolution, response time with each provider's own label and window; time to bounty is award timing; durations default to fastest first); status (first_seen/paused/resumed/left_listing/returned events; facets count events per change_type and current_status); all_changes (chronological events; get_activity_change_detail for before/after). Event tabs use rolling 7d/30d/90d, not calendar months. launched_within applies to reports/response. Payout increases are observed counters, not payment dates. Reuse cursors only with identical inputs. For several signals at once, or with Web2/scope/competition/fee filters, use find_programs or find_hunt_targets.",
|
|
602
622
|
inputSchema: z.object(activityParams).strict(),
|
|
603
623
|
outputSchema: activityOutput,
|
|
604
624
|
annotations: readOnlyAnnotations
|
|
@@ -609,14 +629,14 @@ export function createBbradarServer(client, config) {
|
|
|
609
629
|
// Single-view aliases for the full profile; each schema exposes only the
|
|
610
630
|
// inputs its view accepts and shares the ranking tool's request path.
|
|
611
631
|
const activityDescriptions = {
|
|
612
|
-
value: "Discover programs with frozen within-platform value and report-volume percentiles. Generosity is lifetime; volume
|
|
613
|
-
payouts: "Rank payouts using the same server calculation as BBRadar. Last 7 days and last 30 days are rolling windows. Explicit periods never fall back. For high or low payouts lately, use the same enabled tracked-30-day cohort, or separately label a native-90-day cohort. Reported and tracked bases stay separate. With proven_bounds, descending is Highest reported minimum using lower bounds; ascending is Lowest reported maximum using upper bounds. Neither claims an exact ranking. Source totals can be delayed, including Immunefi.",
|
|
632
|
+
value: "Discover programs with frozen within-platform value and report-volume percentiles. Generosity is lifetime payout per processed report; volume covers the selected period: 7d, 30d or 90d, each only when get_activity_capabilities advertises it. Each row's value holds that period's received count, basis and percentile; signals keep the lifetime and 90-day figures. Without period, naming labels or sort means 90d, and a bare read opens the advertised default, which may be a shortlist named in the page's labels. No launch-cohort input applies. High value, Popular and Low per report exist per period; Pays near top (orders by realization), No payouts seen (orders by received_90d, raw count with its basis; small cohorts remain eligible) and processed_received are 90d only. Comparative ratio sorts use server percentiles; never recompute after filtering. Preserve label_details explanations and destinations. A counter increase is observed activity, not an individual payment. Map points are returned only with include_map.",
|
|
633
|
+
payouts: "Rank payouts using the same server calculation as BBRadar. Last 7 days and last 30 days are rolling windows. Explicit periods never fall back. For high or low payouts lately, use the same enabled tracked-30-day cohort, or separately label a native-90-day cohort. Reported and tracked bases stay separate. With proven_bounds, descending is Highest reported minimum using lower bounds; ascending is Lowest reported maximum using upper bounds. Neither claims an exact ranking. Source totals can be delayed, including Immunefi. Rows also carry average_paid, the platform's own current average payment with its source_label, period, native currency and USD conversion (HackerOne publishes a 45th-55th percentile range); it is the same in every period, and sort=average_paid ranks it where advertised.",
|
|
614
634
|
scope: "Rank recorded scope additions, removals, net movement or largest update; not differences between inventory totals. Periods are rolling 7, 30 or 90 days, not calendar months.",
|
|
615
|
-
rewards: "Rank comparable net reward-range changes over rolling 7, 30 or 90 days.
|
|
635
|
+
rewards: "Rank comparable net reward-range changes over rolling 7, 30 or 90 days. The change percentage compares native maxima; unknown percentages remain null. before/after keep native amounts and add minimum_usd/maximum_usd at the version's current quotes for display (null without a quote).",
|
|
616
636
|
updates: "Rank active UTC days and recorded change groups over rolling periods. Baselines and internal corrections are excluded.",
|
|
617
637
|
reports: "Rank received report counts. Each program uses measured lifetime-counter growth where available, otherwise its supported native period. All-time is the current lifetime counter. Previous and change compare like-for-like. Report volume is not competition. Launch cohorts use the canonical launch date; amounts are not reports since launch.",
|
|
618
|
-
response: "Rank resolved counter growth or current lifetime counters, last resolution, response duration or time to bounty.
|
|
619
|
-
status: "Read distinct First seen, Paused, Resumed, Left listing and Returned events. Event type and current status are separate filters; never merge events from one program.",
|
|
638
|
+
response: "Rank resolved counter growth or current lifetime counters, last resolution, response duration or time to bounty. Each cell keeps its provider's definition, label and window (for example HackerOne first response over 90 days, YesWeHack's 6-month band, Bugcrowd's 75% validated time); first_response_precision says whether a time is exact, a band bound or approximate. Time to bounty is award timing, not payment settlement. Durations sort fastest first unless a direction is given. Period applies only to resolved counts; use only advertised periods.",
|
|
639
|
+
status: "Read distinct First seen, Paused, Resumed, Left listing and Returned events. Event type and current status are separate filters; never merge events from one program. facets count events per change_type and current_status for the period and filters, each counted without its own selection; offer only options with counts.",
|
|
620
640
|
all_changes: "Read chronological semantic program changes with event IDs. Fetch get_activity_change_detail lazily for before/after items, including eligible events for programs that left the catalog. Separate events stay separate."
|
|
621
641
|
};
|
|
622
642
|
for (const tab of ACTIVITY_TABS) {
|
|
@@ -635,7 +655,7 @@ export function createBbradarServer(client, config) {
|
|
|
635
655
|
title: "Get Activity Change Detail",
|
|
636
656
|
description: "Read one bounded page of recorded before/after items for an exact Activity event ID in its source snapshot. Pass meta.snapshot_id from the Activity page. Retained eligible events may describe programs that have left the catalog. Reuse an opaque cursor only with the same event, snapshot and limit.",
|
|
637
657
|
inputSchema: z.object({ event_id: z.string().min(1).max(128), snapshot: activitySnapshotSchema.describe("meta.snapshot_id from the Activity page that supplied this event."), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) }).strict(),
|
|
638
|
-
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
658
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
639
659
|
}, (args) => runTool("get_activity_change_detail", config, rateLimiter, async () => {
|
|
640
660
|
assertProFeature(client, "activity_event_detail", "enabled");
|
|
641
661
|
return proPage(await client.getActivityChangeDetail(args.event_id, toQuery({ snapshot: args.snapshot, cursor: args.cursor, limit: args.limit })), "items");
|
|
@@ -660,7 +680,7 @@ export function createBbradarServer(client, config) {
|
|
|
660
680
|
title: "Get History Change Detail",
|
|
661
681
|
description: "Fetch one semantic History change's paginated before/after items only when needed.",
|
|
662
682
|
inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
663
|
-
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
683
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
664
684
|
}, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
|
|
665
685
|
assertProFeature(client, "history_change_detail");
|
|
666
686
|
assertHistoryCapture(client);
|
|
@@ -680,7 +700,7 @@ export function createBbradarServer(client, config) {
|
|
|
680
700
|
title: "Get Exact Program Activity",
|
|
681
701
|
description: "Read the authoritative activity_summary for one exact program ID.",
|
|
682
702
|
inputSchema: { program_id: programIdSchema },
|
|
683
|
-
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
703
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
684
704
|
}, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
|
|
685
705
|
assertProFeature(client, "program_activity");
|
|
686
706
|
return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
|
|
@@ -787,7 +807,7 @@ export function createBbradarServer(client, config) {
|
|
|
787
807
|
}));
|
|
788
808
|
registerTool("get_program", {
|
|
789
809
|
title: "Get BBRadar Program",
|
|
790
|
-
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
|
|
810
|
+
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous. A contest or Huntr challenge adds contest_awards: its final published award allocations (awarded_total and pool_amount as decimal strings in the contest's own currency token; currency null when the platform names none, as Huntr; never USD or a payment record), or null before a final result. A finished contest with a final result stays readable here by exact platform:handle, without an opportunity tag, although discovery and name resolution exclude ended contests.",
|
|
791
811
|
inputSchema: {
|
|
792
812
|
program_id: programIdentifierSchema
|
|
793
813
|
},
|
|
@@ -918,6 +938,7 @@ export function createBbradarServer(client, config) {
|
|
|
918
938
|
program_id: programIdentifierSchema,
|
|
919
939
|
search: searchTextSchema.optional(),
|
|
920
940
|
target_type: filterValueSchema().optional(),
|
|
941
|
+
language_tags: languageTagListSchema().describe("Keep targets with any requested language tag. Applied while paging; repeat with the returned next_offset."),
|
|
921
942
|
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
922
943
|
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
923
944
|
competition_risk: competitionRiskSchema.optional(),
|
|
@@ -1195,7 +1216,7 @@ export function createBbradarServer(client, config) {
|
|
|
1195
1216
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
1196
1217
|
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
1197
1218
|
max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
|
|
1198
|
-
include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program
|
|
1219
|
+
include_full_target_list: z.boolean().default(true).describe("Fetch up to full_target_limit targets for the newest matching program. Check full_targets_has_more and continue with get_program_targets at full_targets_next_offset. Set false for the fastest cross-program discovery."),
|
|
1199
1220
|
full_target_list_mode: targetListModeSchema.default("identifiers"),
|
|
1200
1221
|
full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
|
|
1201
1222
|
full_list_include_out_of_scope: z.boolean().default(false),
|
|
@@ -1431,7 +1452,8 @@ export function createBbradarServer(client, config) {
|
|
|
1431
1452
|
min_added_7d: z.number().int().min(0).optional(),
|
|
1432
1453
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1433
1454
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
1434
|
-
|
|
1455
|
+
// One flat enum, as clients have always seen it; a union would advertise anyOf.
|
|
1456
|
+
sort_by: z.enum([...programSortSchema.removeDefault().options, ...activityCatalogSortSchema.options]).optional(),
|
|
1435
1457
|
include_target_samples: z.boolean().default(false),
|
|
1436
1458
|
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1437
1459
|
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
@@ -2389,6 +2411,7 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2389
2411
|
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Choose an enabled period, sort and direction, then call get_activity_rankings with that tab for one server-ranked page. Do not approximate by paging or re-ranking the catalog." },
|
|
2390
2412
|
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" }, then: "Use find_programs with the same Activity inputs and competition_risks for program-only results; continue with scan.next_cursor." },
|
|
2391
2413
|
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2414
|
+
{ user_goal: "final award results of a finished contest or Huntr challenge", first_tool: "get_program", arguments: { program_id: "Platform:handle" }, then: "Read contest_awards: awarded_total and pool_amount are decimal strings in the contest's own token (currency null when the platform names none); null means no final result yet." },
|
|
2392
2415
|
{ 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." },
|
|
2393
2416
|
{ 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." },
|
|
2394
2417
|
{ user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
@@ -2502,6 +2525,7 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2502
2525
|
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2503
2526
|
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2504
2527
|
filters,
|
|
2528
|
+
activity_catalog: activityCatalogGuidance,
|
|
2505
2529
|
advanced_tools: advancedTools,
|
|
2506
2530
|
examples
|
|
2507
2531
|
};
|
|
@@ -2537,6 +2561,7 @@ function coreBbradarGuide(topic, client) {
|
|
|
2537
2561
|
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Read supported view combinations and call get_activity_rankings with that tab. Use get_activity_change_detail lazily for a selected Activity event." },
|
|
2538
2562
|
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" } },
|
|
2539
2563
|
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2564
|
+
{ user_goal: "final award results of a finished contest or Huntr challenge", first_tool: "get_program", arguments: { program_id: "Platform:handle" }, then: "Read contest_awards in the contest's own token; null means no final result yet." },
|
|
2540
2565
|
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
|
|
2541
2566
|
{ user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
|
|
2542
2567
|
{ user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
@@ -2580,7 +2605,8 @@ function coreBbradarGuide(topic, client) {
|
|
|
2580
2605
|
],
|
|
2581
2606
|
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2582
2607
|
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2583
|
-
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
|
|
2608
|
+
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name)),
|
|
2609
|
+
activity_catalog: activityCatalogGuidance
|
|
2584
2610
|
};
|
|
2585
2611
|
}
|
|
2586
2612
|
function toolAvailableForContract(client, toolName) {
|
|
@@ -3590,29 +3616,14 @@ async function runResolvedProgramAction(client, config, programId, input) {
|
|
|
3590
3616
|
});
|
|
3591
3617
|
}
|
|
3592
3618
|
if (input.action === "targets") {
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const sanitizedTargets = rawTargets
|
|
3597
|
-
.map(sanitizeTarget)
|
|
3598
|
-
.filter((target) => targetHasAllowedScope(target, {
|
|
3619
|
+
return getProgramTargetsPayload(client, {
|
|
3620
|
+
target_type: input.target_type,
|
|
3621
|
+
language_tags: input.language_tags,
|
|
3599
3622
|
include_out_of_scope: input.include_out_of_scope,
|
|
3600
3623
|
include_ineligible: input.include_ineligible,
|
|
3601
|
-
strict_scope_filter: true
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
return withApiMetadata(api, {
|
|
3605
|
-
program_id: programId,
|
|
3606
|
-
targets: formatTargetList(targets, input.target_list_mode),
|
|
3607
|
-
meta: {
|
|
3608
|
-
limit: input.max_targets,
|
|
3609
|
-
output_mode: input.target_list_mode,
|
|
3610
|
-
total_active_targets: rawTargets.length,
|
|
3611
|
-
total_after_filters: sanitizedTargets.length,
|
|
3612
|
-
returned: targets.length,
|
|
3613
|
-
has_more: sanitizedTargets.length > targets.length
|
|
3614
|
-
}
|
|
3615
|
-
});
|
|
3624
|
+
strict_scope_filter: true,
|
|
3625
|
+
offset: 0, limit: input.max_targets, output_mode: input.target_list_mode
|
|
3626
|
+
}, programId);
|
|
3616
3627
|
}
|
|
3617
3628
|
return checkProgramNewTargets(client, config, {
|
|
3618
3629
|
program_id: programId,
|
|
@@ -4738,30 +4749,18 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
4738
4749
|
.map((change) => readObject(change.target))
|
|
4739
4750
|
.filter((target) => target !== undefined);
|
|
4740
4751
|
let fullTargets = [];
|
|
4741
|
-
let
|
|
4742
|
-
let totalAfterFilters;
|
|
4752
|
+
let fullTargetsMeta;
|
|
4743
4753
|
if (input.include_full_target_list && programId) {
|
|
4744
|
-
const
|
|
4745
|
-
const targetsData = readObject(targetsApi.data);
|
|
4746
|
-
const rawTargets = readArray(targetsData?.targets);
|
|
4747
|
-
fullTargets = rawTargets
|
|
4748
|
-
.map(sanitizeTarget)
|
|
4749
|
-
.filter((target) => targetHasAllowedScope(target, {
|
|
4754
|
+
const payload = await getProgramTargetsPayload(client, {
|
|
4750
4755
|
include_out_of_scope: input.full_list_include_out_of_scope,
|
|
4751
4756
|
include_ineligible: input.full_list_include_ineligible,
|
|
4752
|
-
strict_scope_filter: false
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
program_id: programId,
|
|
4759
|
-
request_id: targetsApi.requestId,
|
|
4760
|
-
upstream_request_id: targetsApi.upstreamRequestId,
|
|
4761
|
-
...apiSourceMetadata(targetsApi)
|
|
4762
|
-
});
|
|
4757
|
+
strict_scope_filter: false,
|
|
4758
|
+
offset: 0, limit: input.full_target_limit, output_mode: input.full_target_list_mode
|
|
4759
|
+
}, programId);
|
|
4760
|
+
fullTargets = readArray(payload.targets);
|
|
4761
|
+
fullTargetsMeta = readObject(payload.meta);
|
|
4762
|
+
sourceRequests.push(...readArray(payload.source_requests).map(readObject).filter((request) => request !== undefined));
|
|
4763
4763
|
}
|
|
4764
|
-
const limitedFullTargets = fullTargets.slice(0, input.full_target_limit);
|
|
4765
4764
|
return stripUndefined({
|
|
4766
4765
|
request_id: randomUUID(),
|
|
4767
4766
|
source_requests: sourceRequests,
|
|
@@ -4770,17 +4769,19 @@ async function getLatestAddedTargets(client, config, input) {
|
|
|
4770
4769
|
program,
|
|
4771
4770
|
matches,
|
|
4772
4771
|
new_targets: formatTargetList(latestNewTargets, input.full_target_list_mode),
|
|
4773
|
-
full_targets: input.include_full_target_list ?
|
|
4772
|
+
full_targets: input.include_full_target_list ? fullTargets : undefined,
|
|
4774
4773
|
meta: stripUndefined({
|
|
4775
4774
|
recent_changes_scanned: changes.length,
|
|
4776
4775
|
matching_changes_scanned: matchingChanges.length,
|
|
4777
4776
|
matches_returned: matches.length,
|
|
4778
4777
|
matches_has_more: matches.length < matchingChanges.length,
|
|
4779
4778
|
new_targets_returned: formatTargetList(latestNewTargets, input.full_target_list_mode).length,
|
|
4780
|
-
full_targets_total_active:
|
|
4781
|
-
full_targets_total_after_filters:
|
|
4782
|
-
full_targets_returned: input.include_full_target_list ?
|
|
4783
|
-
full_targets_has_more:
|
|
4779
|
+
full_targets_total_active: fullTargetsMeta?.total_active_targets,
|
|
4780
|
+
full_targets_total_after_filters: fullTargetsMeta?.total_after_filters,
|
|
4781
|
+
full_targets_returned: input.include_full_target_list ? fullTargets.length : undefined,
|
|
4782
|
+
full_targets_has_more: fullTargetsMeta?.has_more,
|
|
4783
|
+
full_targets_next_offset: fullTargetsMeta?.next_offset,
|
|
4784
|
+
full_targets_possibly_incomplete: fullTargetsMeta?.possibly_incomplete
|
|
4784
4785
|
})
|
|
4785
4786
|
});
|
|
4786
4787
|
}
|
|
@@ -4818,15 +4819,21 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4818
4819
|
return true;
|
|
4819
4820
|
}
|
|
4820
4821
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4821
|
-
const
|
|
4822
|
+
const maxPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
4822
4823
|
// `eligibility=eligible` is an API-level in-scope bounty-eligibility filter,
|
|
4823
4824
|
// so the common eligible-only path needs no second local scope pass.
|
|
4824
|
-
const
|
|
4825
|
+
const languageTags = new Set((input.language_tags ?? []).map(normalizeTag));
|
|
4826
|
+
const directOffsetPaging = languageTags.size === 0 && (!input.include_ineligible || (!input.strict_scope_filter && input.include_out_of_scope));
|
|
4827
|
+
// Bound direct reads to the requested prefix. Local post-filters still need
|
|
4828
|
+
// full pages to find sparse matches. Offsets remain filtered row offsets,
|
|
4829
|
+
// independent of the page size used by an earlier call.
|
|
4830
|
+
const pageSize = directOffsetPaging ? Math.min(maxPageSize, input.offset + input.limit) : maxPageSize;
|
|
4825
4831
|
const collected = [];
|
|
4826
4832
|
let matchedSeen = directOffsetPaging ? Math.floor(input.offset / pageSize) * pageSize : 0;
|
|
4827
4833
|
let page = directOffsetPaging ? Math.floor(input.offset / pageSize) + 1 : 1;
|
|
4828
4834
|
const firstPage = page;
|
|
4829
4835
|
let api;
|
|
4836
|
+
const sourceRequests = [];
|
|
4830
4837
|
let upstreamMeta;
|
|
4831
4838
|
let upstreamHasMore = false;
|
|
4832
4839
|
do {
|
|
@@ -4840,11 +4847,15 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4840
4847
|
page,
|
|
4841
4848
|
page_size: pageSize
|
|
4842
4849
|
});
|
|
4850
|
+
sourceRequests.push({ source: "program_targets", program_id: programId, page,
|
|
4851
|
+
request_id: api.requestId, upstream_request_id: api.upstreamRequestId, ...apiSourceMetadata(api) });
|
|
4843
4852
|
const data = readObject(api.data);
|
|
4844
4853
|
upstreamMeta = readObject(data?.meta);
|
|
4845
4854
|
for (const target of readArray(data?.targets).map(sanitizeTarget)) {
|
|
4846
4855
|
if (!targetHasAllowedScope(target, input))
|
|
4847
4856
|
continue;
|
|
4857
|
+
if (languageTags.size > 0 && !arraysIntersectNormalized(languageTags, readStringArrayField(target, "language_tags")))
|
|
4858
|
+
continue;
|
|
4848
4859
|
if (matchedSeen >= input.offset && collected.length < input.limit)
|
|
4849
4860
|
collected.push(target);
|
|
4850
4861
|
matchedSeen += 1;
|
|
@@ -4862,8 +4873,10 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4862
4873
|
throw new Error("No program target response was collected.");
|
|
4863
4874
|
}
|
|
4864
4875
|
const targets = collected;
|
|
4876
|
+
const hasMore = upstreamHasMore || matchedSeen > input.offset + targets.length;
|
|
4865
4877
|
return withApiMetadata(api, {
|
|
4866
4878
|
program_id: programId,
|
|
4879
|
+
source_requests: sourceRequests,
|
|
4867
4880
|
targets: formatTargetList(targets, input.output_mode),
|
|
4868
4881
|
meta: {
|
|
4869
4882
|
offset: input.offset,
|
|
@@ -4872,7 +4885,8 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4872
4885
|
total_active_targets: readNumber(upstreamMeta?.total) ?? matchedSeen,
|
|
4873
4886
|
total_after_filters: upstreamHasMore ? undefined : matchedSeen,
|
|
4874
4887
|
returned: targets.length,
|
|
4875
|
-
has_more:
|
|
4888
|
+
has_more: hasMore,
|
|
4889
|
+
next_offset: hasMore ? input.offset + targets.length : undefined,
|
|
4876
4890
|
upstream_has_more: upstreamHasMore,
|
|
4877
4891
|
possibly_incomplete: upstreamHasMore,
|
|
4878
4892
|
upstream_pages_fetched: page - firstPage,
|
|
@@ -4885,9 +4899,16 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4885
4899
|
});
|
|
4886
4900
|
}
|
|
4887
4901
|
async function getProgramTargetsExportFallbackPayload(client, input, programId) {
|
|
4902
|
+
if (input.search || input.target_type || input.min_target_score !== undefined
|
|
4903
|
+
|| input.target_opportunity_label || input.competition_risk) {
|
|
4904
|
+
throw new Error("The per-program targets endpoint is unavailable. Its search, type and intelligence filters cannot be preserved by the export fallback; the requested filters were not discarded.");
|
|
4905
|
+
}
|
|
4888
4906
|
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, programId);
|
|
4889
|
-
const
|
|
4907
|
+
const languageTags = new Set((input.language_tags ?? []).map(normalizeTag));
|
|
4908
|
+
const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input)
|
|
4909
|
+
&& (languageTags.size === 0 || arraysIntersectNormalized(languageTags, readStringArrayField(target, "language_tags"))));
|
|
4890
4910
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
4911
|
+
const hasMore = input.offset + targets.length < sanitizedTargets.length;
|
|
4891
4912
|
return stripUndefined({
|
|
4892
4913
|
request_id: exportLookup.requestId,
|
|
4893
4914
|
upstream_request_id: exportLookup.upstreamRequestId,
|
|
@@ -4901,10 +4922,10 @@ async function getProgramTargetsExportFallbackPayload(client, input, programId)
|
|
|
4901
4922
|
offset: input.offset,
|
|
4902
4923
|
limit: input.limit,
|
|
4903
4924
|
output_mode: input.output_mode,
|
|
4904
|
-
|
|
4905
|
-
total_after_filters: sanitizedTargets.length,
|
|
4925
|
+
...fallbackTargetCoverage(exportLookup, sanitizedTargets.length),
|
|
4906
4926
|
returned: targets.length,
|
|
4907
|
-
has_more:
|
|
4927
|
+
has_more: hasMore || exportLookup.coverage?.possibly_incomplete === true,
|
|
4928
|
+
next_offset: hasMore ? input.offset + targets.length : undefined,
|
|
4908
4929
|
target_source: exportLookup.source,
|
|
4909
4930
|
target_list_unavailable: exportLookup.unavailable,
|
|
4910
4931
|
fallback_error: exportLookup.error
|
|
@@ -4933,6 +4954,7 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
|
4933
4954
|
})),
|
|
4934
4955
|
source: "program_targets",
|
|
4935
4956
|
rawTargets,
|
|
4957
|
+
coverage: targetPageCoverage(data, rawTargets.length, rawTargets.length),
|
|
4936
4958
|
sourceRequests: [
|
|
4937
4959
|
{
|
|
4938
4960
|
source: "program_targets",
|
|
@@ -5032,14 +5054,21 @@ function cacheTtlMsFromDiagnostics(cache) {
|
|
|
5032
5054
|
}
|
|
5033
5055
|
async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
5034
5056
|
try {
|
|
5057
|
+
const limit = Math.min(MAX_TARGET_EXPORT, client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT));
|
|
5035
5058
|
const api = await client.exportTargets({
|
|
5036
5059
|
program_ids: [programId],
|
|
5037
5060
|
include_out_of_scope: true,
|
|
5038
5061
|
include_ineligible: true,
|
|
5039
5062
|
format: "json",
|
|
5040
|
-
limit
|
|
5063
|
+
limit
|
|
5041
5064
|
});
|
|
5042
5065
|
const rawTargets = readExportTargetRows(api.data).map((row) => sanitizeTarget(readObject(row)?.target ?? row));
|
|
5066
|
+
const data = readObject(api.data);
|
|
5067
|
+
const coverage = targetPageCoverage({ meta: {
|
|
5068
|
+
...readObject(data?.meta),
|
|
5069
|
+
// A full capped export is not proof that the inventory ends here.
|
|
5070
|
+
has_more: readObject(data?.meta)?.has_more === true || rawTargets.length >= limit
|
|
5071
|
+
} }, rawTargets.length, rawTargets.length);
|
|
5043
5072
|
return {
|
|
5044
5073
|
requestId: api.requestId,
|
|
5045
5074
|
upstreamRequestId: api.upstreamRequestId,
|
|
@@ -5053,6 +5082,7 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
5053
5082
|
})),
|
|
5054
5083
|
source: "target_export",
|
|
5055
5084
|
rawTargets,
|
|
5085
|
+
coverage,
|
|
5056
5086
|
sourceRequests: [
|
|
5057
5087
|
{
|
|
5058
5088
|
source: "target_export",
|
|
@@ -5313,8 +5343,7 @@ async function getProgramScopeSummary(client, config, input) {
|
|
|
5313
5343
|
program: formatProgram(lookup.program, "compact"),
|
|
5314
5344
|
scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
|
|
5315
5345
|
meta: {
|
|
5316
|
-
|
|
5317
|
-
total_after_filters: targets.length,
|
|
5346
|
+
...targetPageCoverage(targetsData, rawTargets.length, targets.length),
|
|
5318
5347
|
target_list_mode: input.target_list_mode,
|
|
5319
5348
|
group_limit: input.group_limit,
|
|
5320
5349
|
target_source: "program_targets",
|
|
@@ -5339,8 +5368,7 @@ async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, in
|
|
|
5339
5368
|
program: formatProgram(indexFallback.program, "compact"),
|
|
5340
5369
|
scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
|
|
5341
5370
|
meta: stripUndefined({
|
|
5342
|
-
|
|
5343
|
-
total_after_filters: targets.length,
|
|
5371
|
+
...fallbackTargetCoverage(exportLookup, targets.length),
|
|
5344
5372
|
target_list_mode: input.target_list_mode,
|
|
5345
5373
|
group_limit: input.group_limit,
|
|
5346
5374
|
target_source: exportLookup.source,
|
|
@@ -5376,8 +5404,7 @@ async function getProgramTargetBreakdown(client, config, input) {
|
|
|
5376
5404
|
groups: readObject(scope.groups)
|
|
5377
5405
|
},
|
|
5378
5406
|
meta: {
|
|
5379
|
-
|
|
5380
|
-
total_after_filters: targets.length,
|
|
5407
|
+
...targetPageCoverage(targetsData, rawTargets.length, targets.length),
|
|
5381
5408
|
target_list_mode: input.target_list_mode,
|
|
5382
5409
|
group_limit: input.group_limit,
|
|
5383
5410
|
target_source: "program_targets",
|
|
@@ -5385,6 +5412,32 @@ async function getProgramTargetBreakdown(client, config, input) {
|
|
|
5385
5412
|
}
|
|
5386
5413
|
});
|
|
5387
5414
|
}
|
|
5415
|
+
function targetPageCoverage(data, rows, filteredRows) {
|
|
5416
|
+
const meta = readObject(data?.meta);
|
|
5417
|
+
const total = numberField(meta, "total");
|
|
5418
|
+
const incomplete = meta?.has_more === true || (numberField(meta, "total_pages") ?? 1) > 1
|
|
5419
|
+
|| (total !== undefined && total > rows);
|
|
5420
|
+
return stripUndefined({
|
|
5421
|
+
total_active_targets: total ?? (incomplete ? undefined : rows),
|
|
5422
|
+
total_after_filters: incomplete ? undefined : filteredRows,
|
|
5423
|
+
targets_scanned: rows,
|
|
5424
|
+
matching_targets_scanned: filteredRows,
|
|
5425
|
+
possibly_incomplete: incomplete,
|
|
5426
|
+
count_scope: incomplete ? "sampled_target_page" : "complete_target_inventory"
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
5429
|
+
function fallbackTargetCoverage(lookup, filteredRows) {
|
|
5430
|
+
const incomplete = lookup.unavailable || lookup.coverage?.possibly_incomplete === true;
|
|
5431
|
+
return stripUndefined({
|
|
5432
|
+
...lookup.coverage,
|
|
5433
|
+
total_active_targets: lookup.unavailable ? undefined : lookup.coverage?.total_active_targets ?? (incomplete ? undefined : lookup.rawTargets.length),
|
|
5434
|
+
total_after_filters: incomplete ? undefined : filteredRows,
|
|
5435
|
+
targets_scanned: lookup.rawTargets.length,
|
|
5436
|
+
matching_targets_scanned: filteredRows,
|
|
5437
|
+
possibly_incomplete: incomplete,
|
|
5438
|
+
count_scope: lookup.unavailable ? "unavailable" : incomplete ? "sampled_target_page" : "complete_target_inventory"
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5388
5441
|
async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
|
|
5389
5442
|
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
|
|
5390
5443
|
const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
|
|
@@ -5411,8 +5464,7 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
|
|
|
5411
5464
|
groups: readObject(scope.groups)
|
|
5412
5465
|
},
|
|
5413
5466
|
meta: stripUndefined({
|
|
5414
|
-
|
|
5415
|
-
total_after_filters: targets.length,
|
|
5467
|
+
...fallbackTargetCoverage(exportLookup, targets.length),
|
|
5416
5468
|
target_list_mode: input.target_list_mode,
|
|
5417
5469
|
group_limit: input.group_limit,
|
|
5418
5470
|
target_source: exportLookup.source,
|
|
@@ -6255,6 +6307,7 @@ function compactProgram(program) {
|
|
|
6255
6307
|
competition_risk: stringField(program, "competition_risk"),
|
|
6256
6308
|
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
6257
6309
|
submission_cost: program.submission_cost,
|
|
6310
|
+
contest_awards: program.contest_awards,
|
|
6258
6311
|
payout_summary: program.payout_summary,
|
|
6259
6312
|
activity: program.activity,
|
|
6260
6313
|
activity_summary: program.activity_summary,
|
|
@@ -6820,6 +6873,18 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6820
6873
|
// server order, so a larger pool lets ranking_mode choose among more of them.
|
|
6821
6874
|
const HUNT_SERVER_POOL = 100;
|
|
6822
6875
|
async function findHuntTargets(client, config, input, activity = {}) {
|
|
6876
|
+
const huntStartedAt = Date.now();
|
|
6877
|
+
const phaseTiming = {};
|
|
6878
|
+
const runStage = async (phase, timeoutMs, callback) => {
|
|
6879
|
+
const startedAt = Date.now();
|
|
6880
|
+
try {
|
|
6881
|
+
return await client.withRequestTimeout(timeoutMs, callback);
|
|
6882
|
+
}
|
|
6883
|
+
finally {
|
|
6884
|
+
phaseTiming[phase] = Date.now() - startedAt;
|
|
6885
|
+
}
|
|
6886
|
+
};
|
|
6887
|
+
validateActivityCatalogInput(activity);
|
|
6823
6888
|
const warnings = [];
|
|
6824
6889
|
const errors = [];
|
|
6825
6890
|
const selfHostedOnly = isSelfHostedOnlyPlatformSelection(input.platforms);
|
|
@@ -6885,22 +6950,28 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6885
6950
|
warnings.push(`Recent target-change queries were reduced from ${desiredRecentChangeQueries.length} to ${recentChangeQueries.length} to preserve the upstream request budget.`);
|
|
6886
6951
|
}
|
|
6887
6952
|
const serverFiltered = hasActivityCatalogInput(activity);
|
|
6953
|
+
// Catalog discovery and optional changes share at most 60% of the tool
|
|
6954
|
+
// deadline. Keep at least 30% for exact targets and 10% for the response.
|
|
6888
6955
|
const [programPayload, recentResult] = await Promise.all([
|
|
6889
|
-
serverFiltered
|
|
6956
|
+
runStage("program_discovery_ms", Math.floor(config.requestTimeoutMs * 0.6), () => serverFiltered
|
|
6890
6957
|
? findFilteredServerPrograms(client, config, {
|
|
6891
6958
|
...programInput, ...activity, opportunity_levels: [], sort_by: activity.sort_by, sort_direction: activity.sort_direction,
|
|
6892
6959
|
platforms: input.platforms, tags: input.tags, updated_since: input.updated_since, vdp_mode: "exclude", include_target_samples: false
|
|
6893
6960
|
}, {
|
|
6894
6961
|
...programInput, platforms: [], tags: [], vdp_mode: "include", updated_since: undefined,
|
|
6895
6962
|
max_pages: programRequestBudget, upstream_request_budget: programRequestBudget
|
|
6896
|
-
}, HUNT_SERVER_POOL)
|
|
6897
|
-
: findPrograms(client, config, programInput
|
|
6963
|
+
}, HUNT_SERVER_POOL, true)
|
|
6964
|
+
: findPrograms(client, config, programInput, {
|
|
6965
|
+
allowPartialTimeout: true, excludeVdpUpstream: true
|
|
6966
|
+
})),
|
|
6898
6967
|
recentChangesEnabled
|
|
6899
|
-
? fetchHuntRecentChanges(client, config, input, recentChangeQueries)
|
|
6968
|
+
? runStage("recent_changes_ms", Math.floor(config.requestTimeoutMs * 0.3), () => fetchHuntRecentChanges(client, config, input, recentChangeQueries))
|
|
6900
6969
|
: Promise.resolve({ changes: [], sourceRequests: [], requestsUsed: 0, errors: [] })
|
|
6901
6970
|
]);
|
|
6971
|
+
client.throwIfRequestAborted();
|
|
6902
6972
|
const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
|
|
6903
6973
|
warnings.push(...programWarnings);
|
|
6974
|
+
errors.push(...readArray(programPayload.errors).map(readObject).filter((error) => error !== undefined));
|
|
6904
6975
|
errors.push(...recentResult.errors);
|
|
6905
6976
|
const programBudget = readObject(programPayload.upstream_budget);
|
|
6906
6977
|
const programRequestsUsed = readNumber(programBudget?.used) ?? 0;
|
|
@@ -6919,8 +6990,12 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6919
6990
|
const finalistEntries = preliminaryRankedEntries
|
|
6920
6991
|
.slice(0, sampledProgramCount)
|
|
6921
6992
|
.map((entry) => entry.entry);
|
|
6922
|
-
const targetResult = await collectHuntTargetSamples(client, finalistEntries, input);
|
|
6993
|
+
const targetResult = await runStage("target_sampling_ms", Math.floor(config.requestTimeoutMs * 0.9) - (Date.now() - huntStartedAt), () => collectHuntTargetSamples(client, finalistEntries, input));
|
|
6994
|
+
client.throwIfRequestAborted();
|
|
6923
6995
|
errors.push(...targetResult.errors);
|
|
6996
|
+
if (errors.length > 0) {
|
|
6997
|
+
warnings.push("Some discovery or enrichment reads failed or reached their time budget. Returned matches preserve the requested filters; missing target samples or change evidence are unavailable, not confirmed absent. Report the incomplete coverage without narrowing platforms or relaxing thresholds.");
|
|
6998
|
+
}
|
|
6924
6999
|
if (sampledProgramCount < Math.min(input.max_results, programEntries.length)) {
|
|
6925
7000
|
warnings.push(`The upstream request budget limited the final exact target shortlist to ${sampledProgramCount} of ${Math.min(input.max_results, programEntries.length)} requested results.`);
|
|
6926
7001
|
}
|
|
@@ -6934,6 +7009,10 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6934
7009
|
}));
|
|
6935
7010
|
const targetRequestsUsed = targetResult.requestsUsed;
|
|
6936
7011
|
const totalRequestsUsed = programRequestsUsed + recentResult.requestsUsed + targetRequestsUsed;
|
|
7012
|
+
const coverageIncomplete = errors.length > 0
|
|
7013
|
+
|| readObject(programPayload.ranking_scope)?.possibly_incomplete === true
|
|
7014
|
+
|| readObject(programPayload.scan)?.complete === false
|
|
7015
|
+
|| sampledProgramCount < Math.min(input.max_results, programEntries.length);
|
|
6937
7016
|
return stripUndefined({
|
|
6938
7017
|
request_id: randomUUID(),
|
|
6939
7018
|
fetched_at: new Date().toISOString(),
|
|
@@ -6985,7 +7064,10 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6985
7064
|
result_guidance: {
|
|
6986
7065
|
final_shortlist: true,
|
|
6987
7066
|
follow_up_calls_required: false,
|
|
6988
|
-
|
|
7067
|
+
coverage_incomplete: coverageIncomplete,
|
|
7068
|
+
instruction: coverageIncomplete
|
|
7069
|
+
? "Use the bounded matches and report warnings and incomplete coverage. Preserve the user's periods, thresholds and platform scope; do not infer supported platforms from a sampled recent-change feed."
|
|
7070
|
+
: "Use these matches as the final shortlist. Exact bounded target samples and recent additions are already included; call get_program_targets only if the user explicitly requests an exhaustive scope dump for one selected program."
|
|
6989
7071
|
},
|
|
6990
7072
|
matches,
|
|
6991
7073
|
meta: {
|
|
@@ -6998,7 +7080,8 @@ async function findHuntTargets(client, config, input, activity = {}) {
|
|
|
6998
7080
|
model_round_trips_expected: 1,
|
|
6999
7081
|
parallel_recent_change_fetch: recentChangeQueries.length > 0,
|
|
7000
7082
|
bulk_resource_links_suppressed: true,
|
|
7001
|
-
partial_success: errors.length > 0
|
|
7083
|
+
partial_success: errors.length > 0,
|
|
7084
|
+
phase_timing: phaseTiming
|
|
7002
7085
|
}
|
|
7003
7086
|
});
|
|
7004
7087
|
}
|
|
@@ -7738,6 +7821,7 @@ function hasActivityCatalogInput(input) {
|
|
|
7738
7821
|
}
|
|
7739
7822
|
/** Validates and builds one server-ordered catalog query; nothing is sent. */
|
|
7740
7823
|
function serverCatalogQuery(client, input) {
|
|
7824
|
+
validateActivityCatalogInput(input);
|
|
7741
7825
|
const levels = Array.isArray(input.opportunity_levels) ? input.opportunity_levels : [];
|
|
7742
7826
|
if (levels.length > 1)
|
|
7743
7827
|
throw new Error("Combined Activity queries accept at most one opportunity tier; separate tier pages cannot form a global ranking.");
|
|
@@ -7757,6 +7841,17 @@ function serverCatalogQuery(client, input) {
|
|
|
7757
7841
|
vdp_mode: input.vdp_mode, snapshot: input.snapshot }
|
|
7758
7842
|
};
|
|
7759
7843
|
}
|
|
7844
|
+
// Durations rank fastest first when no direction is given, as the API does. The
|
|
7845
|
+
// direction the server reports it applied wins, so an older API is labelled
|
|
7846
|
+
// correctly too.
|
|
7847
|
+
const ASCENDING_FIRST_SORTS = new Set(["first_response", "time_to_bounty"]);
|
|
7848
|
+
function appliedSortDirection(input, served) {
|
|
7849
|
+
if (served === "asc" || served === "desc")
|
|
7850
|
+
return served;
|
|
7851
|
+
if (input.sort_direction === "asc" || input.sort_direction === "desc")
|
|
7852
|
+
return input.sort_direction;
|
|
7853
|
+
return ASCENDING_FIRST_SORTS.has(String(input.sort_by ?? "updated")) ? "asc" : "desc";
|
|
7854
|
+
}
|
|
7760
7855
|
function readServerCatalogPage(client, catalog, page, pageSize, snapshot) {
|
|
7761
7856
|
const query = toQuery({ ...catalog.query, snapshot, page, page_size: pageSize });
|
|
7762
7857
|
return catalog.level ? client.getOpportunities(catalog.level, query) : client.listPrograms(query);
|
|
@@ -7773,7 +7868,7 @@ async function findServerCatalogPrograms(client, config, input) {
|
|
|
7773
7868
|
const mode = input.output_mode === "full" ? "full" : "compact";
|
|
7774
7869
|
return withApiMetadata(api, {
|
|
7775
7870
|
programs: readArray(data?.programs).map((program) => formatProgram(sanitizeProgram(program, config.webBaseUrl), mode)),
|
|
7776
|
-
ranking_scope: { mode: "server", sort_by: input.sort_by ?? "updated", sort_direction: input
|
|
7871
|
+
ranking_scope: { mode: "server", sort_by: input.sort_by ?? "updated", sort_direction: appliedSortDirection(input, readObject(data?.meta)?.sort_direction) },
|
|
7777
7872
|
meta: sanitizeJson(data?.meta)
|
|
7778
7873
|
});
|
|
7779
7874
|
}
|
|
@@ -7817,6 +7912,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7817
7912
|
const start = cursorValue ? decodeScanCursor(cursorValue, fingerprint, pageSize)
|
|
7818
7913
|
: { v: 1, p: Number(input.page ?? 1), k: 0, s: pageSize, f: fingerprint, a: undefined, t: undefined };
|
|
7819
7914
|
const sourceRequests = [];
|
|
7915
|
+
const errors = [];
|
|
7820
7916
|
let matches = [];
|
|
7821
7917
|
let seen = new Set();
|
|
7822
7918
|
let nextPage = start.p;
|
|
@@ -7825,6 +7921,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7825
7921
|
let token = start.t ?? input.snapshot;
|
|
7826
7922
|
let totalPages;
|
|
7827
7923
|
let serverTotal;
|
|
7924
|
+
let servedDirection;
|
|
7828
7925
|
let rowsScanned = 0;
|
|
7829
7926
|
let pagesRead = 0;
|
|
7830
7927
|
let restarted = false;
|
|
@@ -7840,11 +7937,25 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7840
7937
|
const needed = pagesRead === 0 ? 1 : Math.ceil((limits.maxResults - matches.length) / (rate * pageSize));
|
|
7841
7938
|
const batch = Math.max(1, Math.min(allowance, needed, CATALOG_SCAN_PREFETCH));
|
|
7842
7939
|
const pageToken = token;
|
|
7843
|
-
const
|
|
7940
|
+
const batchStart = nextPage;
|
|
7941
|
+
const pages = await Promise.allSettled(Array.from({ length: batch }, (_, index) => batchStart + index)
|
|
7844
7942
|
.map(async (page) => ({ page, api: await readServerCatalogPage(client, catalog, page, pageSize, pageToken) })));
|
|
7845
|
-
|
|
7943
|
+
// Count all admitted reads, including a failed page or a prefetched page
|
|
7944
|
+
// beyond a gap; only the contiguous successful prefix enters the ranking.
|
|
7945
|
+
for (const result of pages) {
|
|
7946
|
+
const noRequest = result.status === "fulfilled" ? result.value.api.cached || result.value.api.coalesced
|
|
7947
|
+
: result.reason instanceof BBRadarApiError && (result.reason.cached || result.reason.coalesced);
|
|
7948
|
+
limits.budget.remaining -= noRequest ? 0 : 1;
|
|
7949
|
+
}
|
|
7950
|
+
for (const [index, result] of pages.entries()) {
|
|
7951
|
+
if (result.status === "rejected") {
|
|
7952
|
+
if (!limits.allowPartialTimeout || pagesRead === 0 || !(result.reason instanceof BBRadarApiError) || result.reason.status !== 408)
|
|
7953
|
+
throw result.reason;
|
|
7954
|
+
errors.push(apiOperationError(`program catalog page ${batchStart + index}`, result.reason));
|
|
7955
|
+
break scan;
|
|
7956
|
+
}
|
|
7957
|
+
const { page, api } = result.value;
|
|
7846
7958
|
pagesRead += 1;
|
|
7847
|
-
limits.budget.remaining -= api.cached || api.coalesced ? 0 : 1;
|
|
7848
7959
|
sourceRequests.push({ source: catalog.level ? `opportunities:${catalog.level}` : "programs", page, request_id: api.requestId,
|
|
7849
7960
|
upstream_request_id: api.upstreamRequestId, ...apiSourceMetadata(api) });
|
|
7850
7961
|
const data = readObject(api.data);
|
|
@@ -7869,6 +7980,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7869
7980
|
version ??= pageVersion;
|
|
7870
7981
|
totalPages = readNumber(meta?.total_pages) ?? totalPages;
|
|
7871
7982
|
serverTotal = readNumber(meta?.total) ?? serverTotal;
|
|
7983
|
+
servedDirection = meta?.sort_direction ?? servedDirection;
|
|
7872
7984
|
if (token !== undefined && typeof meta?.snapshot === "string")
|
|
7873
7985
|
token = meta.snapshot;
|
|
7874
7986
|
const rows = readArray(data?.programs);
|
|
@@ -7900,10 +8012,11 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7900
8012
|
return {
|
|
7901
8013
|
candidates: matches,
|
|
7902
8014
|
sourceRequests,
|
|
8015
|
+
errors,
|
|
7903
8016
|
scan: stripUndefined({
|
|
7904
8017
|
order: "Server order; local filters only remove programs.",
|
|
7905
8018
|
sort_by: input.sort_by ?? "updated",
|
|
7906
|
-
sort_direction: input
|
|
8019
|
+
sort_direction: appliedSortDirection(input, servedDirection),
|
|
7907
8020
|
local_filters: localFilters.length > 0 ? localFilters : undefined,
|
|
7908
8021
|
server_matches: serverTotal,
|
|
7909
8022
|
rows_scanned: rowsScanned,
|
|
@@ -7913,17 +8026,20 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7913
8026
|
? encodeScanCursor({ v: 1, p: resume.p, k: resume.k, s: pageSize, f: fingerprint, a: version, t: typeof token === "string" ? token : undefined })
|
|
7914
8027
|
: undefined,
|
|
7915
8028
|
activity_snapshot_id: version,
|
|
7916
|
-
restarted_on_new_activity_version: restarted || undefined
|
|
8029
|
+
restarted_on_new_activity_version: restarted || undefined,
|
|
8030
|
+
deadline_reached: errors.length > 0 || undefined
|
|
7917
8031
|
})
|
|
7918
8032
|
};
|
|
7919
8033
|
}
|
|
7920
8034
|
/** find_programs with server Activity filters plus local filters or target samples. */
|
|
7921
|
-
async function findFilteredServerPrograms(client, config, input, local, maxResults = local.max_results) {
|
|
8035
|
+
async function findFilteredServerPrograms(client, config, input, local, maxResults = local.max_results, allowPartialTimeout = false) {
|
|
7922
8036
|
const normalized = normalizeFindProgramsInput(local);
|
|
7923
8037
|
const warnings = normalized.warnings;
|
|
7924
8038
|
local = normalized.input;
|
|
7925
8039
|
const budget = { initial: local.upstream_request_budget, remaining: local.upstream_request_budget };
|
|
7926
|
-
const result = await scanServerCatalog(client, config, input, local, { maxResults, maxPages: local.max_pages, budget });
|
|
8040
|
+
const result = await scanServerCatalog(client, config, input, local, { maxResults, maxPages: local.max_pages, budget, allowPartialTimeout });
|
|
8041
|
+
if (result.errors.length > 0)
|
|
8042
|
+
warnings.push("Program discovery reached its time budget; ranking covers only the completed server-ordered prefix.");
|
|
7927
8043
|
if (result.scan.complete !== true) {
|
|
7928
8044
|
warnings.push("More server pages remain; scan.next_cursor continues in the same order.");
|
|
7929
8045
|
}
|
|
@@ -7936,6 +8052,7 @@ async function findFilteredServerPrograms(client, config, input, local, maxResul
|
|
|
7936
8052
|
request_id: randomUUID(),
|
|
7937
8053
|
fetched_at: new Date().toISOString(),
|
|
7938
8054
|
source_requests: result.sourceRequests,
|
|
8055
|
+
errors: result.errors.length > 0 ? result.errors : undefined,
|
|
7939
8056
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
7940
8057
|
upstream_budget: { used: budget.initial - budget.remaining, remaining: budget.remaining },
|
|
7941
8058
|
ranking_scope: { mode: "server_filtered", sort_by: result.scan.sort_by, sort_direction: result.scan.sort_direction, complete: result.scan.complete },
|
|
@@ -7950,10 +8067,11 @@ function programHasNoSubmissionFee(program) {
|
|
|
7950
8067
|
const amount = numberField(cost, "amount");
|
|
7951
8068
|
return amount === undefined || (amount === 0 && (numberField(cost, "maximum_amount") ?? 0) === 0);
|
|
7952
8069
|
}
|
|
7953
|
-
async function findPrograms(client, config, input) {
|
|
8070
|
+
async function findPrograms(client, config, input, execution = {}) {
|
|
7954
8071
|
const normalized = normalizeFindProgramsInput(input);
|
|
7955
8072
|
const warnings = normalized.warnings;
|
|
7956
8073
|
input = normalized.input;
|
|
8074
|
+
const errors = [];
|
|
7957
8075
|
const budget = {
|
|
7958
8076
|
initial: input.upstream_request_budget,
|
|
7959
8077
|
remaining: input.upstream_request_budget
|
|
@@ -7970,11 +8088,13 @@ async function findPrograms(client, config, input) {
|
|
|
7970
8088
|
platforms: input.platforms,
|
|
7971
8089
|
tags: upstreamTags,
|
|
7972
8090
|
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
|
|
8091
|
+
vdp_mode: execution.excludeVdpUpstream && input.vdp_mode === "exclude" ? "exclude" : undefined,
|
|
7973
8092
|
updated_since: input.updated_since,
|
|
7974
8093
|
opportunity_levels: input.opportunity_levels,
|
|
7975
8094
|
max_pages: input.max_pages,
|
|
7976
8095
|
budget,
|
|
7977
|
-
warnings
|
|
8096
|
+
warnings,
|
|
8097
|
+
partialTimeoutErrors: execution.allowPartialTimeout ? errors : undefined
|
|
7978
8098
|
});
|
|
7979
8099
|
const filteredCandidates = collected.programs
|
|
7980
8100
|
.map((program) => toProgramCandidate(program, config.webBaseUrl))
|
|
@@ -7998,6 +8118,7 @@ async function findPrograms(client, config, input) {
|
|
|
7998
8118
|
source_requests: collected.sourceRequests,
|
|
7999
8119
|
fetched_at: new Date().toISOString(),
|
|
8000
8120
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
8121
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
8001
8122
|
filters: stripUndefined({
|
|
8002
8123
|
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
8003
8124
|
exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
@@ -8040,7 +8161,7 @@ async function findPrograms(client, config, input) {
|
|
|
8040
8161
|
platform_filter: input.platforms.length > 0 ? input.platforms : "all_platforms",
|
|
8041
8162
|
max_pages_scanned_per_source: input.max_pages,
|
|
8042
8163
|
upstream_total_pages: collected.totalPagesBySource,
|
|
8043
|
-
possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, input.max_pages)
|
|
8164
|
+
possibly_incomplete: errors.length > 0 || isRankingPossiblyIncomplete(collected.totalPagesBySource, input.max_pages)
|
|
8044
8165
|
},
|
|
8045
8166
|
programs,
|
|
8046
8167
|
meta: {
|
|
@@ -8445,20 +8566,39 @@ async function collectProgramSource(client, source, sourceIndex, options) {
|
|
|
8445
8566
|
async function fetchProgramPage(client, source, sourceIndex, page, options) {
|
|
8446
8567
|
const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
|
|
8447
8568
|
return runWithSlot(async () => {
|
|
8569
|
+
const endpoint = source.kind === "programs" ? "programs" : "opportunities";
|
|
8570
|
+
const vdpModes = readArray(readObject(client.endpointCapability(endpoint)?.catalog_filters)?.vdp_mode);
|
|
8448
8571
|
const query = toQuery({
|
|
8449
8572
|
search: options.search,
|
|
8450
8573
|
platforms: options.platforms,
|
|
8451
8574
|
tags: options.tags,
|
|
8452
8575
|
updated_since: options.updated_since,
|
|
8453
8576
|
program_kind: source.kind === "programs" ? options.program_kind : undefined,
|
|
8577
|
+
// This removes only known VDPs; unknown classifications with positive
|
|
8578
|
+
// rewards still reach the local filters. Older APIs keep local exclusion.
|
|
8579
|
+
vdp_mode: supportsCatalogFilters(client, endpoint) && vdpModes.includes("exclude") ? options.vdp_mode : undefined,
|
|
8454
8580
|
page,
|
|
8455
|
-
page_size: MAX_PROGRAMS_PER_SEARCH
|
|
8581
|
+
page_size: Math.min(MAX_PROGRAMS_PER_SEARCH, client.endpointNumber(endpoint, "max_page_size", MAX_PROGRAMS_PER_SEARCH))
|
|
8456
8582
|
});
|
|
8457
8583
|
if (!tryConsumeBudget(options.budget)) {
|
|
8458
8584
|
addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all requested program pages could be fetched.");
|
|
8459
8585
|
return undefined;
|
|
8460
8586
|
}
|
|
8461
|
-
|
|
8587
|
+
let api;
|
|
8588
|
+
try {
|
|
8589
|
+
api = source.kind === "opportunity" ? await client.getOpportunities(source.level, query) : await client.listPrograms(query);
|
|
8590
|
+
}
|
|
8591
|
+
catch (error) {
|
|
8592
|
+
// Hunts can retain completed catalog pages. Ordinary discovery keeps its
|
|
8593
|
+
// existing failure contract, and a failed first page is never an empty hit.
|
|
8594
|
+
if (page === 1 || !options.partialTimeoutErrors || !(error instanceof BBRadarApiError) || error.status !== 408)
|
|
8595
|
+
throw error;
|
|
8596
|
+
if (error.cached || error.coalesced)
|
|
8597
|
+
options.budget.remaining += 1;
|
|
8598
|
+
options.partialTimeoutErrors.push(apiOperationError(`program catalog page ${page}`, error));
|
|
8599
|
+
addUniqueWarning(options.warnings, "Program discovery reached its time budget; ranking covers only completed catalog pages.");
|
|
8600
|
+
return undefined;
|
|
8601
|
+
}
|
|
8462
8602
|
refundBudgetIfNoUpstreamRequest(options.budget, api);
|
|
8463
8603
|
const data = readObject(api.data);
|
|
8464
8604
|
const meta = readObject(data?.meta);
|
|
@@ -8944,7 +9084,9 @@ Summarize the program scope:
|
|
|
8944
9084
|
title: "Prepare Passive Recon Plan",
|
|
8945
9085
|
description: "Create a passive-only plan from BBRadar program and target data. Use only when planning is explicitly requested.",
|
|
8946
9086
|
argsSchema: {
|
|
8947
|
-
|
|
9087
|
+
// Zod 4 keeps a description on the schema it was set on, and prompt
|
|
9088
|
+
// arguments read the outer optional, so describe it again here.
|
|
9089
|
+
program_id: programIdentifierSchema.optional().describe(PROGRAM_IDENTIFIER_DESCRIPTION),
|
|
8948
9090
|
focus: z.string().optional().describe("Optional focus area such as APIs, domains, mobile, or cloud.")
|
|
8949
9091
|
}
|
|
8950
9092
|
}, (args) => promptResult(`Prepare a passive-only plan from BBRadar data. Program id JSON string, if present: ${promptJson(args.program_id ?? "")}.
|
|
@@ -9280,6 +9422,7 @@ function compactToolText(payload, isError) {
|
|
|
9280
9422
|
}));
|
|
9281
9423
|
}
|
|
9282
9424
|
const meta = readObject(payload.meta);
|
|
9425
|
+
const coverageIncomplete = readObject(payload.result_guidance)?.coverage_incomplete === true;
|
|
9283
9426
|
const returned = numberField(meta, "returned") ??
|
|
9284
9427
|
[
|
|
9285
9428
|
readArray(payload.programs).length,
|
|
@@ -9291,6 +9434,12 @@ function compactToolText(payload, isError) {
|
|
|
9291
9434
|
request_id: payload.request_id,
|
|
9292
9435
|
summary: "Full tool result is available in structuredContent.",
|
|
9293
9436
|
returned,
|
|
9437
|
+
partial_success: meta?.partial_success === true ? true : undefined,
|
|
9438
|
+
coverage_incomplete: coverageIncomplete ? true : undefined,
|
|
9439
|
+
errors: meta?.partial_success === true ? readArray(payload.errors).map((error) => {
|
|
9440
|
+
const value = readObject(error);
|
|
9441
|
+
return stripUndefined({ operation: value?.operation, status: value?.status });
|
|
9442
|
+
}) : undefined,
|
|
9294
9443
|
mcp_timing: payload.mcp_timing
|
|
9295
9444
|
}));
|
|
9296
9445
|
}
|