codex-plugin-doctor 1.51.0 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,10 +78,21 @@ Runtime MCP validation with `--runtime`:
78
78
  - paginated list responses
79
79
  - runtime capability scorecard
80
80
  - redacted verbose transcript with `--verbose-runtime`
81
- - optional runtime approval gating with a precomputed `doctor runtime-plan` digest
82
- - optional Docker isolation for local Node.js stdio servers with `--sandbox docker`
83
-
84
- Output formats:
81
+ - optional runtime approval gating with a precomputed `doctor runtime-plan` digest
82
+ - optional Docker isolation for local Node.js stdio servers with `--sandbox docker`
83
+
84
+ ### Remote MCP Readiness
85
+
86
+ Remote MCP runtime probing is disabled until you explicitly pass `--allow-network`. Local endpoints also require `--allow-local-network`:
87
+
88
+ ```bash
89
+ codex-plugin-doctor check ./remote-mcp --runtime --allow-network
90
+ codex-plugin-doctor check ./remote-mcp --runtime --allow-network --allow-local-network
91
+ ```
92
+
93
+ The probe makes only bounded, read-only protocol and OAuth metadata-discovery requests, redacts report output, and applies SSRF controls. It does not authenticate or follow redirects. See [Remote MCP Readiness](./docs/architecture/remote-mcp-readiness.md).
94
+
95
+ Output formats:
85
96
 
86
97
  - human text output
87
98
  - JSON reports
@@ -427,9 +438,9 @@ jobs:
427
438
  runs-on: ubuntu-latest
428
439
  steps:
429
440
  - uses: actions/checkout@v5
430
- - uses: Esquetta/CodexPluginDoctor@v1.51.0
441
+ - uses: Esquetta/CodexPluginDoctor@v1.52.0
431
442
  with:
432
- version: "1.51.0"
443
+ version: "1.52.0"
433
444
  path: .
434
445
  runtime: "true"
435
446
  policy: codex-publish
@@ -0,0 +1,23 @@
1
+ import { type RemoteLookup } from "./remote-network-policy.js";
2
+ export interface BoundedHttpRequestOptions {
3
+ allowLocalNetwork?: boolean;
4
+ lookup?: RemoteLookup;
5
+ timeoutMs?: number;
6
+ maxResponseBytes?: number;
7
+ method?: string;
8
+ body?: string | Buffer;
9
+ headers?: Record<string, string | undefined>;
10
+ stopAfter?: (body: Buffer) => boolean;
11
+ }
12
+ export interface BoundedHttpResponse {
13
+ statusCode: number;
14
+ headers: Record<string, string | string[]>;
15
+ body: Buffer;
16
+ }
17
+ export declare class BoundedHttpError extends Error {
18
+ readonly code: "REMOTE_HTTP_ENCODING_UNSUPPORTED" | "REMOTE_HTTP_HEADER_FORBIDDEN" | "REMOTE_HTTP_OPTIONS_INVALID" | "REMOTE_HTTP_PEER_MISMATCH" | "REMOTE_HTTP_REDIRECT" | "REMOTE_HTTP_REQUEST_FAILED" | "REMOTE_HTTP_RESPONSE_TOO_LARGE" | "REMOTE_HTTP_STOP_CONDITION_FAILED" | "REMOTE_HTTP_TIMEOUT" | "REMOTE_HTTP_URL_CREDENTIALS" | "REMOTE_HTTP_URL_UNSUPPORTED";
19
+ readonly statusCode?: number | undefined;
20
+ readonly headers?: Record<string, string | string[]> | undefined;
21
+ constructor(code: "REMOTE_HTTP_ENCODING_UNSUPPORTED" | "REMOTE_HTTP_HEADER_FORBIDDEN" | "REMOTE_HTTP_OPTIONS_INVALID" | "REMOTE_HTTP_PEER_MISMATCH" | "REMOTE_HTTP_REDIRECT" | "REMOTE_HTTP_REQUEST_FAILED" | "REMOTE_HTTP_RESPONSE_TOO_LARGE" | "REMOTE_HTTP_STOP_CONDITION_FAILED" | "REMOTE_HTTP_TIMEOUT" | "REMOTE_HTTP_URL_CREDENTIALS" | "REMOTE_HTTP_URL_UNSUPPORTED", message: string, statusCode?: number | undefined, headers?: Record<string, string | string[]> | undefined);
22
+ }
23
+ export declare function requestBoundedHttp(rawUrl: string, options?: BoundedHttpRequestOptions): Promise<BoundedHttpResponse>;
@@ -0,0 +1,240 @@
1
+ import * as http from "node:http";
2
+ import * as https from "node:https";
3
+ import { BlockList, isIP } from "node:net";
4
+ import { resolveRemoteTarget } from "./remote-network-policy.js";
5
+ const DEFAULT_TIMEOUT_MS = 3_000;
6
+ const DEFAULT_MAX_RESPONSE_BYTES = 1_024 * 1_024;
7
+ const allowedRequestHeaders = new Set([
8
+ "accept",
9
+ "content-type",
10
+ "mcp-protocol-version",
11
+ "mcp-session-id",
12
+ "user-agent"
13
+ ]);
14
+ const safeResponseHeaders = new Set([
15
+ "content-type",
16
+ "www-authenticate",
17
+ "mcp-session-id",
18
+ "mcp-protocol-version",
19
+ "location"
20
+ ]);
21
+ export class BoundedHttpError extends Error {
22
+ code;
23
+ statusCode;
24
+ headers;
25
+ constructor(code, message, statusCode, headers) {
26
+ super(message);
27
+ this.code = code;
28
+ this.statusCode = statusCode;
29
+ this.headers = headers;
30
+ this.name = "BoundedHttpError";
31
+ }
32
+ }
33
+ function validateHeaders(headers) {
34
+ const validated = {};
35
+ for (const [name, value] of Object.entries(headers ?? {})) {
36
+ if (value === undefined) {
37
+ continue;
38
+ }
39
+ const normalizedName = name.toLowerCase();
40
+ if (!allowedRequestHeaders.has(normalizedName)) {
41
+ throw new BoundedHttpError("REMOTE_HTTP_HEADER_FORBIDDEN", `Remote HTTP request header is not allowed: ${normalizedName}.`);
42
+ }
43
+ if (normalizedName in validated || typeof value !== "string") {
44
+ throw new BoundedHttpError("REMOTE_HTTP_HEADER_FORBIDDEN", `Remote HTTP request header must be a single valid string: ${normalizedName}.`);
45
+ }
46
+ try {
47
+ http.validateHeaderValue(normalizedName, value);
48
+ }
49
+ catch {
50
+ throw new BoundedHttpError("REMOTE_HTTP_HEADER_FORBIDDEN", `Remote HTTP request header must be a single valid string: ${normalizedName}.`);
51
+ }
52
+ validated[normalizedName] = value;
53
+ }
54
+ return validated;
55
+ }
56
+ function validateOptions(options) {
57
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
+ const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
59
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > DEFAULT_TIMEOUT_MS
60
+ || !Number.isInteger(maxResponseBytes) || maxResponseBytes < 1 || maxResponseBytes > DEFAULT_MAX_RESPONSE_BYTES) {
61
+ throw new BoundedHttpError("REMOTE_HTTP_OPTIONS_INVALID", "Remote HTTP request options are invalid.");
62
+ }
63
+ return { timeoutMs, maxResponseBytes };
64
+ }
65
+ async function resolveWithinDeadline(url, options, timeoutMs) {
66
+ let timer;
67
+ try {
68
+ return await Promise.race([
69
+ resolveRemoteTarget(url, {
70
+ allowLocalNetwork: options.allowLocalNetwork,
71
+ lookup: options.lookup
72
+ }),
73
+ new Promise((_resolve, reject) => {
74
+ timer = setTimeout(() => {
75
+ reject(new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote HTTP request timed out."));
76
+ }, timeoutMs);
77
+ })
78
+ ]);
79
+ }
80
+ finally {
81
+ if (timer !== undefined) {
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+ }
86
+ function selectSafeHeaders(headers) {
87
+ const selected = {};
88
+ for (const name of safeResponseHeaders) {
89
+ const value = headers[name];
90
+ if (value !== undefined) {
91
+ selected[name] = value;
92
+ }
93
+ }
94
+ return selected;
95
+ }
96
+ function hasIdentityContentEncoding(headers) {
97
+ const value = headers["content-encoding"];
98
+ const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
99
+ return values.every((entry) => entry.toLowerCase() === "identity");
100
+ }
101
+ function matchesTargetPeer(target, remoteAddress) {
102
+ if (remoteAddress === undefined || isIP(remoteAddress) === 0) {
103
+ return false;
104
+ }
105
+ const peers = new BlockList();
106
+ peers.addAddress(target.address, target.family === 4 ? "ipv4" : "ipv6");
107
+ if (target.family === 4) {
108
+ peers.addAddress(`::ffff:${target.address}`, "ipv6");
109
+ }
110
+ return peers.check(remoteAddress, isIP(remoteAddress) === 4 ? "ipv4" : "ipv6");
111
+ }
112
+ export async function requestBoundedHttp(rawUrl, options = {}) {
113
+ let url;
114
+ try {
115
+ url = new URL(rawUrl);
116
+ }
117
+ catch {
118
+ throw new BoundedHttpError("REMOTE_HTTP_URL_UNSUPPORTED", "Remote HTTP URL must be absolute HTTP or HTTPS.");
119
+ }
120
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
121
+ throw new BoundedHttpError("REMOTE_HTTP_URL_UNSUPPORTED", "Remote HTTP URL must be absolute HTTP or HTTPS.");
122
+ }
123
+ if (url.username || url.password) {
124
+ throw new BoundedHttpError("REMOTE_HTTP_URL_CREDENTIALS", "Remote HTTP URL must not include credentials.");
125
+ }
126
+ const { timeoutMs, maxResponseBytes } = validateOptions(options);
127
+ const headers = validateHeaders(options.headers);
128
+ const deadline = Date.now() + timeoutMs;
129
+ const target = await resolveWithinDeadline(url, options, timeoutMs);
130
+ const remainingMs = deadline - Date.now();
131
+ if (remainingMs <= 0) {
132
+ throw new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote HTTP request timed out.");
133
+ }
134
+ const transport = url.protocol === "https:" ? https : http;
135
+ return new Promise((resolve, reject) => {
136
+ let request;
137
+ let response;
138
+ let settled = false;
139
+ const timeout = setTimeout(() => {
140
+ fail(new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote HTTP request timed out."));
141
+ }, remainingMs);
142
+ const fail = (error) => {
143
+ if (settled) {
144
+ return;
145
+ }
146
+ settled = true;
147
+ clearTimeout(timeout);
148
+ response?.destroy();
149
+ request?.destroy();
150
+ reject(error);
151
+ };
152
+ const complete = (body) => {
153
+ if (settled) {
154
+ return;
155
+ }
156
+ settled = true;
157
+ clearTimeout(timeout);
158
+ response?.destroy();
159
+ request?.destroy();
160
+ resolve({
161
+ statusCode: response?.statusCode ?? 0,
162
+ headers: response ? selectSafeHeaders(response.headers) : {},
163
+ body
164
+ });
165
+ };
166
+ try {
167
+ request = transport.request({
168
+ protocol: url.protocol,
169
+ hostname: url.hostname,
170
+ port: url.port || undefined,
171
+ path: `${url.pathname}${url.search}`,
172
+ method: options.method ?? "GET",
173
+ headers,
174
+ agent: false,
175
+ lookup: (_hostname, lookupOptions, callback) => {
176
+ if (lookupOptions.all) {
177
+ callback(null, [{ address: target.address, family: target.family }]);
178
+ return;
179
+ }
180
+ callback(null, target.address, target.family);
181
+ }
182
+ });
183
+ }
184
+ catch {
185
+ fail(new BoundedHttpError("REMOTE_HTTP_REQUEST_FAILED", "Remote HTTP request failed."));
186
+ return;
187
+ }
188
+ request.once("error", () => {
189
+ fail(new BoundedHttpError("REMOTE_HTTP_REQUEST_FAILED", "Remote HTTP request failed."));
190
+ });
191
+ request.once("response", (incoming) => {
192
+ response = incoming;
193
+ const safeHeaders = selectSafeHeaders(incoming.headers);
194
+ if (!matchesTargetPeer(target, incoming.socket.remoteAddress)) {
195
+ fail(new BoundedHttpError("REMOTE_HTTP_PEER_MISMATCH", "Remote HTTP peer did not match the resolved target."));
196
+ return;
197
+ }
198
+ if ((incoming.statusCode ?? 0) >= 300 && (incoming.statusCode ?? 0) < 400) {
199
+ fail(new BoundedHttpError("REMOTE_HTTP_REDIRECT", "Remote HTTP redirects are not allowed.", incoming.statusCode, safeHeaders));
200
+ return;
201
+ }
202
+ if (!hasIdentityContentEncoding(incoming.headers)) {
203
+ fail(new BoundedHttpError("REMOTE_HTTP_ENCODING_UNSUPPORTED", "Remote HTTP response content encoding must be identity."));
204
+ return;
205
+ }
206
+ const chunks = [];
207
+ let receivedBytes = 0;
208
+ incoming.on("data", (chunk) => {
209
+ receivedBytes += chunk.length;
210
+ if (receivedBytes > maxResponseBytes) {
211
+ fail(new BoundedHttpError("REMOTE_HTTP_RESPONSE_TOO_LARGE", "Remote HTTP response exceeded the configured size limit."));
212
+ return;
213
+ }
214
+ chunks.push(chunk);
215
+ const body = Buffer.concat(chunks);
216
+ let shouldStop;
217
+ try {
218
+ shouldStop = options.stopAfter?.(body) ?? false;
219
+ }
220
+ catch {
221
+ fail(new BoundedHttpError("REMOTE_HTTP_STOP_CONDITION_FAILED", "Remote HTTP stop condition failed."));
222
+ return;
223
+ }
224
+ if (shouldStop) {
225
+ complete(body);
226
+ return;
227
+ }
228
+ });
229
+ incoming.once("error", () => {
230
+ fail(new BoundedHttpError("REMOTE_HTTP_REQUEST_FAILED", "Remote HTTP request failed."));
231
+ });
232
+ incoming.once("end", () => {
233
+ if (!settled) {
234
+ complete(Buffer.concat(chunks));
235
+ }
236
+ });
237
+ });
238
+ request.end(options.body);
239
+ });
240
+ }
@@ -35,6 +35,27 @@ const runtimeConformanceSchema = {
35
35
  ],
36
36
  additionalProperties: false
37
37
  };
38
+ const remoteRuntimeScorecardSchema = {
39
+ type: "object",
40
+ properties: {
41
+ transport: runtimeCapabilityStatusSchema,
42
+ networkSafety: runtimeCapabilityStatusSchema,
43
+ initialize: runtimeCapabilityStatusSchema,
44
+ contentType: runtimeCapabilityStatusSchema,
45
+ session: {
46
+ type: "string",
47
+ enum: ["absent", "present-valid", "present-invalid"]
48
+ },
49
+ protocolHeaders: runtimeCapabilityStatusSchema,
50
+ authorization: runtimeCapabilityStatusSchema,
51
+ overall: {
52
+ type: "string",
53
+ enum: ["pass", "warn", "fail", "skipped"]
54
+ }
55
+ },
56
+ required: ["transport", "networkSafety", "initialize", "contentType", "session", "protocolHeaders", "authorization", "overall"],
57
+ additionalProperties: false
58
+ };
38
59
  const runtimeScorecardSchema = {
39
60
  type: "object",
40
61
  properties: {
@@ -46,7 +67,8 @@ const runtimeScorecardSchema = {
46
67
  resourceTemplatesList: runtimeCapabilityStatusSchema,
47
68
  promptsList: runtimeCapabilityStatusSchema,
48
69
  promptGet: runtimeCapabilityStatusSchema,
49
- conformance: runtimeConformanceSchema
70
+ conformance: runtimeConformanceSchema,
71
+ remote: remoteRuntimeScorecardSchema
50
72
  },
51
73
  required: [
52
74
  "initialize",
@@ -41,6 +41,8 @@ export interface BuildReleaseCheckOptions {
41
41
  env?: Record<string, string | undefined>;
42
42
  platform?: NodeJS.Platform;
43
43
  runtime?: boolean;
44
+ allowNetwork?: boolean;
45
+ allowLocalNetwork?: boolean;
44
46
  runtimeSandbox?: RuntimeSandboxMode;
45
47
  runCheck?: (targetPath: string, options: CheckOptions) => Promise<CheckResult>;
46
48
  }
@@ -106,6 +106,8 @@ export async function buildReleaseCheck(targetPath, options = {}) {
106
106
  const runtimeProbeEnabled = options.runtime ?? false;
107
107
  const validationResult = await (options.runCheck ?? validatePlugin)(resolvedPath, {
108
108
  runtime: runtimeProbeEnabled,
109
+ ...(options.allowNetwork ? { allowNetwork: true } : {}),
110
+ ...(options.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
109
111
  ...(options.runtimeSandbox ? { runtimeSandbox: options.runtimeSandbox } : {})
110
112
  });
111
113
  const securityResult = await buildSecurityAudit(resolvedPath);
@@ -111,6 +111,8 @@ export interface BuildDoctorReleaseEvidenceOptions {
111
111
  requireRuntimeApproval?: boolean;
112
112
  runtimeApprovalDigest?: string | null;
113
113
  runtime?: boolean;
114
+ allowNetwork?: boolean;
115
+ allowLocalNetwork?: boolean;
114
116
  sandbox?: RuntimeSandboxMode;
115
117
  environment?: CompatibilityEnvironment;
116
118
  runCheck?: (targetPath: string, options?: CheckOptions) => Promise<CheckResult>;
@@ -192,17 +192,126 @@ function signReleaseEvidence(report, signingKey, keyHint) {
192
192
  keyHint
193
193
  };
194
194
  }
195
+ function buildRuntimeApprovalFailureReport(rootPath, runtimePlan, runtimeApproval, options) {
196
+ const generatedAt = new Date().toISOString();
197
+ const emptyDigest = sha256("");
198
+ const failureMessage = "Release evidence checks were not run because runtime approval did not pass.";
199
+ const report = {
200
+ schemaVersion: "1.0.0",
201
+ kind: "doctor.release.evidence",
202
+ generatedAt,
203
+ version: packageVersion,
204
+ targetPath: rootPath,
205
+ status: "fail",
206
+ exitCode: 1,
207
+ releaseReady: false,
208
+ summary: {
209
+ attestation: "fail",
210
+ attestationVerification: "fail",
211
+ corpus: "fail",
212
+ performance: "fail",
213
+ releaseGates: "fail",
214
+ runtimeApproval: "fail",
215
+ security: "fail",
216
+ trust: "fail"
217
+ },
218
+ releaseGates: {
219
+ status: "fail",
220
+ checks: [{ id: "runtime.approval", status: "fail", message: runtimeApproval.message }]
221
+ },
222
+ runtimeApproval,
223
+ ...(options.runtime ? { execution: runtimePlan.execution } : {}),
224
+ package: { name: null, version: null, private: null },
225
+ git: { commit: null, tag: null, dirty: null },
226
+ attestation: {
227
+ schemaVersion: "1.0.0",
228
+ kind: "doctor.attestation",
229
+ generatedAt,
230
+ version: packageVersion,
231
+ targetPath: rootPath,
232
+ subject: { name: "unavailable", version: null, description: null },
233
+ packageFingerprint: { algorithm: "sha256", digest: emptyDigest, files: { total: 0, bytes: 0 } },
234
+ reportDigest: { algorithm: "sha256", digest: emptyDigest },
235
+ summary: {
236
+ status: "fail",
237
+ validation: { status: "fail", findingCount: 0 },
238
+ security: { status: "fail", score: 0, findingCount: 0 },
239
+ compatibility: { failedClients: [] },
240
+ trust: { status: "fail", score: 0, findingCount: 0 },
241
+ recommendations: { actionCount: 0 }
242
+ },
243
+ verification: { recomputeCommand: "", notes: [failureMessage] },
244
+ signature: { status: "unsigned", reason: failureMessage }
245
+ },
246
+ attestationVerification: {
247
+ schemaVersion: "1.0.0",
248
+ kind: "doctor.attestation.verification",
249
+ generatedAt,
250
+ artifactPath: "inline:doctor.release-evidence.attestation",
251
+ targetPath: rootPath,
252
+ status: "fail",
253
+ exitCode: 1,
254
+ summary: { packageFingerprint: "fail", reportDigest: "fail", signature: "fail" },
255
+ unsignedFields: [],
256
+ checks: [{ id: "runtime.approval", status: "fail", message: runtimeApproval.message }]
257
+ },
258
+ corpus: {
259
+ schemaVersion: "1.0.0",
260
+ kind: "doctor.validation.corpus",
261
+ generatedAt,
262
+ version: packageVersion,
263
+ summary: { status: "fail", caseCount: 0, passedExpectations: 0, failedExpectations: 0, runtimeCases: 0 },
264
+ cases: []
265
+ },
266
+ performance: {
267
+ schemaVersion: "1.0.0",
268
+ kind: "doctor.perf",
269
+ generatedAt,
270
+ targetPath: rootPath,
271
+ status: "fail",
272
+ exitCode: 1,
273
+ summary: {
274
+ stageCount: 0,
275
+ slowestStage: "total",
276
+ totalDurationMs: 0,
277
+ validationStatus: "fail",
278
+ securityStatus: "fail",
279
+ trustScore: 0,
280
+ compatibilityFailures: 0,
281
+ thresholdFailures: 0
282
+ },
283
+ stages: [],
284
+ thresholds: []
285
+ },
286
+ security: { status: "fail", score: 0, findingCounts: { fail: 0, warn: 0, total: 0 } },
287
+ trust: { status: "fail", score: 0, findingCounts: { fail: 0, warn: 0, total: 0 } }
288
+ };
289
+ return {
290
+ ...report,
291
+ evidenceSignature: signReleaseEvidence(report, options.signingKey, `env:${options.signingKeyEnv}`)
292
+ };
293
+ }
195
294
  export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
196
295
  const rootPath = path.resolve(targetPath);
296
+ const runtimePlan = await buildDoctorRuntimePlan(rootPath, new Date().toISOString(), options.sandbox ? { sandbox: options.sandbox } : {});
297
+ const runtimeApproval = evaluateRuntimeApproval(runtimePlan, {
298
+ required: options.requireRuntimeApproval ?? false,
299
+ approvedDigest: options.runtimeApprovalDigest
300
+ });
301
+ if (!runtimeApprovalPassed(runtimeApproval)) {
302
+ return buildRuntimeApprovalFailureReport(rootPath, runtimePlan, runtimeApproval, options);
303
+ }
197
304
  const security = await buildSecurityAudit(rootPath);
198
305
  const runCheck = options.runCheck ?? validatePlugin;
199
306
  const checkOptions = options.runtime
200
307
  ? {
201
308
  runtime: true,
309
+ ...(options.allowNetwork ? { allowNetwork: true } : {}),
310
+ ...(options.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
202
311
  ...(options.sandbox ? { runtimeSandbox: options.sandbox } : {})
203
312
  }
204
313
  : {};
205
- const [attestation, corpus, performance, trust, packageMetadata, git, runtimePlan] = await Promise.all([
314
+ const [attestation, corpus, performance, trust, packageMetadata, git] = await Promise.all([
206
315
  buildDoctorAttestation(rootPath, {
207
316
  signingKey: options.signingKey,
208
317
  signingKeyHint: `env:${options.signingKeyEnv}`,
@@ -216,8 +325,7 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
216
325
  }),
217
326
  buildTrustScore(rootPath, { securityAudit: security }),
218
327
  readPackageMetadata(rootPath),
219
- readGitMetadata(rootPath),
220
- buildDoctorRuntimePlan(rootPath, new Date().toISOString(), options.sandbox ? { sandbox: options.sandbox } : {})
328
+ readGitMetadata(rootPath)
221
329
  ]);
222
330
  const normalizedPackageMetadata = {
223
331
  name: packageMetadata.name ?? attestation.subject.name,
@@ -229,10 +337,6 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
229
337
  artifactPath: "inline:doctor.release-evidence.attestation"
230
338
  });
231
339
  const releaseGates = buildReleaseGateReport(git, options);
232
- const runtimeApproval = evaluateRuntimeApproval(runtimePlan, {
233
- required: options.requireRuntimeApproval ?? false,
234
- approvedDigest: options.runtimeApprovalDigest
235
- });
236
340
  const partialReport = {
237
341
  schemaVersion: "1.0.0",
238
342
  kind: "doctor.release.evidence",
@@ -0,0 +1,16 @@
1
+ import { type BoundedHttpRequestOptions, type BoundedHttpResponse } from "./bounded-http-client.js";
2
+ import { type RemoteLookup } from "./remote-network-policy.js";
3
+ import type { Finding, RemoteRuntimeScorecard } from "../domain/types.js";
4
+ export type RemoteMcpRequest = (rawUrl: string, options?: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
5
+ export interface RemoteMcpProbeOptions {
6
+ allowNetwork?: boolean;
7
+ allowLocalNetwork?: boolean;
8
+ requestTimeoutMs?: number;
9
+ lookup?: RemoteLookup;
10
+ request?: RemoteMcpRequest;
11
+ }
12
+ export interface RemoteMcpProbeResult {
13
+ findings: Finding[];
14
+ scorecard: RemoteRuntimeScorecard;
15
+ }
16
+ export declare function probeRemoteMcpServer(serverName: string, rawUrl: string, options?: RemoteMcpProbeOptions): Promise<RemoteMcpProbeResult>;