diff --git a/src/ida/IdaConfiguration.ts b/src/ida/IdaConfiguration.ts index 33aa88675c..6d8ffeb47f 100644 --- a/src/ida/IdaConfiguration.ts +++ b/src/ida/IdaConfiguration.ts @@ -5,6 +5,7 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js"; import type { RetainedResourceScope } from "../application/AnalysisProvider.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import type { AnalysisInputIssue } from "../domain/analysisErrorCore.js"; import { projectInputIssues } from "../domain/inputIssueProjection.js"; import { err, ok, type Result } from "../domain/result.js"; import { @@ -20,6 +21,8 @@ import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; /** Omitted registration `timeoutMs` for each upstream IDA MCP request. */ export const DEFAULT_IDA_REQUEST_TIMEOUT_MS = 300_000; +/** Shortest valid registration request timeout. */ +const MIN_IDA_REQUEST_TIMEOUT_MS = 1; /** Node timer ceiling. Larger registration values would be clamped instead of honored. */ export const MAX_IDA_REQUEST_TIMEOUT_MS = 2_147_483_647; @@ -34,7 +37,7 @@ const common = { protocol: z.literal("native").optional(), timeoutMs: z .int() - .positive() + .min(MIN_IDA_REQUEST_TIMEOUT_MS) .max(MAX_IDA_REQUEST_TIMEOUT_MS) .default(DEFAULT_IDA_REQUEST_TIMEOUT_MS), workspaceRoot: z @@ -78,6 +81,17 @@ const registrationSchema = z /** An upstream MCP registration plus REA's database lifecycle selection. */ export type IdaConfiguration = z.infer; +/** Render only schema-owned constraints, never rejected registration values. */ +const registrationConstraint = ( + issue: AnalysisInputIssue, +): string | undefined => { + if (issue.path.length === 1 && issue.path[0] === "timeoutMs") + return `timeoutMs must be an integer from ${String(MIN_IDA_REQUEST_TIMEOUT_MS)} to ${String(MAX_IDA_REQUEST_TIMEOUT_MS)} milliseconds`; + return issue.message === undefined + ? undefined + : `${issue.path.join(".") || "registration"}: ${issue.message}`; +}; + /** Resolved compatibility facts; persisted identities and diagnostics intentionally differ. */ export const IDA_COMPATIBILITY_PROFILES = { legacy: { @@ -171,11 +185,10 @@ export const readIdaConfiguration = ( const constraints = projectInputIssues( parsed.error.issues, registration, - ).flatMap((issue) => - issue.message === undefined - ? [] - : [`${issue.path.join(".") || "registration"}: ${issue.message}`], - ); + ).flatMap((issue) => { + const constraint = registrationConstraint(issue); + return constraint === undefined ? [] : [constraint]; + }); if (constraints.length === 0) return err( idaConfigurationError( diff --git a/tests/boundary/cli/idaDoctorRegistration.test.ts b/tests/boundary/cli/idaDoctorRegistration.test.ts index 1424087761..592827b707 100644 --- a/tests/boundary/cli/idaDoctorRegistration.test.ts +++ b/tests/boundary/cli/idaDoctorRegistration.test.ts @@ -119,6 +119,17 @@ cliTest.for([ }, constraint: "workspaceRoot must be absolute", }, + ...[0, 2_147_483_648, 1.5, "registration-secret"].map((timeoutMs) => ({ + name: `invalid timeout ${String(timeoutMs)}`, + registration: { command: "provider-must-not-launch", timeoutMs }, + constraint: + "timeoutMs must be an integer from 1 to 2147483647 milliseconds", + })), + ...[1, 2_147_483_647].map((timeoutMs) => ({ + name: `valid timeout ${String(timeoutMs)}`, + registration: { command: "provider-must-not-launch", timeoutMs }, + constraint: undefined, + })), { name: "valid registration", registration: { command: "provider-must-not-launch", args: [] }, @@ -156,5 +167,36 @@ cliTest.for([ ]), }); expect(result.stdout).not.toContain("registration-secret"); + if (constraint?.startsWith("timeoutMs") === true) { + const providers = await cli.run({ + arguments: ["providers", "--json"], + environment: { HOME: root, REA_IDA_MCP_CONFIG: path }, + timeoutMs: 10_000, + }); + expect(providers.exitCode, providers.stderr).toBe(0); + expect(providers.json).toMatchObject({ + analysis_provider_candidates: expect.arrayContaining([ + expect.objectContaining({ + provider: expect.objectContaining({ id: "ida" }), + availability: expect.objectContaining({ + status: "unavailable", + code: "open_options_invalid", + reason: expect.stringContaining(constraint), + diagnostics: expect.objectContaining({ + configuration_details: expect.objectContaining({ + settings: expect.arrayContaining([ + expect.objectContaining({ + setting: "REA_IDA_MCP_CONFIG", + constraint: expect.stringContaining(constraint), + }), + ]), + }), + }), + }), + }), + ]), + }); + expect(providers.stdout).not.toContain("registration-secret"); + } }, );