mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(providers): preserve reply bytes and structured JSON ownership
Share strict UTF-8 JSON admission across bounded worker reports. Preserve IDA/JEB structured dictionaries through negotiated SDK validation and adapter decoding, retaining inherited tool validation and protocol handling. Validated 240 tests, including real SDK/HTTP exchanges and firmware file reads; 83 host-specific cases skipped. Typecheck, lint, and discovery passed. Real-provider execution and modern MCP protocol behavior remain unverified.
This commit is contained in:
@@ -19,7 +19,7 @@ export const SOURCE_TEST_PROJECTS = [
|
||||
group: "source",
|
||||
include: [
|
||||
"src/*.test.ts",
|
||||
"src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
|
||||
"src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { safeParseUtf8Json } from "./safeJson.js";
|
||||
|
||||
it("distinguishes malformed UTF-8 from a literal Unicode replacement character", () => {
|
||||
const invalid = Buffer.concat([
|
||||
Buffer.from('{"message":"'),
|
||||
Buffer.from([0xff]),
|
||||
Buffer.from('"}'),
|
||||
]);
|
||||
expect(safeParseUtf8Json(invalid)).toMatchObject({
|
||||
ok: false,
|
||||
cause: { code: "ERR_ENCODING_INVALID_ENCODED_DATA" },
|
||||
});
|
||||
expect(safeParseUtf8Json(Buffer.from('{"message":"valid � 雪"}'))).toEqual({
|
||||
ok: true,
|
||||
value: { message: "valid � 雪" },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["\uFEFF{}", '{"message":', ""])(
|
||||
"retains strict JSON failure causes for %j",
|
||||
(text) => {
|
||||
expect(safeParseUtf8Json(Buffer.from(text))).toMatchObject({
|
||||
ok: false,
|
||||
cause: expect.any(SyntaxError),
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -3,6 +3,23 @@ export type SafeJsonParseResult =
|
||||
| { readonly ok: true; readonly value: unknown }
|
||||
| { readonly ok: false; readonly error: string; readonly cause: unknown };
|
||||
|
||||
/** Decode complete UTF-8 JSON without replacing malformed bytes or stripping a BOM. */
|
||||
export const safeParseUtf8Json = (bytes: Uint8Array): SafeJsonParseResult => {
|
||||
let text: string;
|
||||
try {
|
||||
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return {
|
||||
ok: false,
|
||||
error: cause instanceof Error ? cause.message : String(cause),
|
||||
cause,
|
||||
};
|
||||
}
|
||||
return safeParseJson(text);
|
||||
};
|
||||
|
||||
/** Parse JSON text, returning the failure reason instead of throwing. */
|
||||
export const safeParseJson = (
|
||||
value: string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { safeParseUtf8Json } from "../domain/safeJson.js";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { access, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
@@ -42,8 +43,8 @@ import { evmInterfaceEvidence } from "../domain/evm/evmInterfaceEvidence.js";
|
||||
import {
|
||||
DEFAULT_EVM_TIMEOUT_MS,
|
||||
type InspectEvmInterfaceInput,
|
||||
evmInterfaceSchema,
|
||||
} from "../domain/evm/evmInterface.js";
|
||||
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
|
||||
import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
@@ -569,7 +570,9 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
|
||||
const decoded = safeParseUtf8Json(file.bytes);
|
||||
if (!decoded.ok) throw decoded.cause;
|
||||
reply = replySchema.parse(decoded.value);
|
||||
} catch (cause: unknown) {
|
||||
const cleanup =
|
||||
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { safeParseUtf8Json } from "../domain/safeJson.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { chmod, lstat, opendir, readlink, realpath } from "node:fs/promises";
|
||||
@@ -264,12 +265,9 @@ export class FirmwareFileAccess {
|
||||
if (bytes.length === 0)
|
||||
throw firmwareReportFailure(operation, "empty", "Report is empty");
|
||||
try {
|
||||
const value: unknown = JSON.parse(
|
||||
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes,
|
||||
),
|
||||
);
|
||||
return value;
|
||||
const decoded = safeParseUtf8Json(bytes);
|
||||
if (!decoded.ok) throw decoded.cause;
|
||||
return decoded.value;
|
||||
} catch (cause: unknown) {
|
||||
throw firmwareReportFailure(
|
||||
operation,
|
||||
|
||||
@@ -645,3 +645,53 @@ it("keeps a real SDK physical failure when abort arrives during its outer unwind
|
||||
await connection.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves observed own structured members before deciding whether to unwrap a result", () => {
|
||||
const structured: unknown = JSON.parse(
|
||||
'{"__proto__":{"observed":true},"result":"field","nested":[{"__proto__":"nested fact"}]}',
|
||||
);
|
||||
const decoded = decodeIdaToolResult({
|
||||
content: [],
|
||||
structuredContent: structured,
|
||||
});
|
||||
expect(Object.hasOwn(decoded as object, "__proto__")).toBe(true);
|
||||
expect(JSON.stringify(decoded)).toBe(JSON.stringify(structured));
|
||||
const wrapped: unknown = JSON.parse(
|
||||
'{"result":{"__proto__":{"observed":true},"nested":{"__proto__":"nested fact"}}}',
|
||||
);
|
||||
const value = decodeIdaToolResult({
|
||||
content: [],
|
||||
structuredContent: wrapped,
|
||||
});
|
||||
expect(Object.hasOwn(value as object, "__proto__")).toBe(true);
|
||||
expect(JSON.stringify(value)).toBe(
|
||||
'{"__proto__":{"observed":true},"nested":{"__proto__":"nested fact"}}',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps error, truncation and native-text precedence ahead of structured JSON admission", () => {
|
||||
const structuredContent = { result: Infinity };
|
||||
expect(() =>
|
||||
decodeIdaToolResult({
|
||||
isError: true,
|
||||
structuredContent,
|
||||
content: [{ type: "text", text: "engine refused" }],
|
||||
}),
|
||||
).toThrow(/IDA MCP tool failed: \s*engine refused/u);
|
||||
expect(() =>
|
||||
decodeIdaToolResult({
|
||||
structuredContent,
|
||||
content: [],
|
||||
_meta: { ida_mcp: { output_truncated: true } },
|
||||
}),
|
||||
).toThrow("IDA MCP truncated its output");
|
||||
expect(
|
||||
decodeIdaToolResult(
|
||||
{
|
||||
structuredContent,
|
||||
content: [{ type: "text", text: '{"complete":true}' }],
|
||||
},
|
||||
"native",
|
||||
),
|
||||
).toEqual({ complete: true });
|
||||
});
|
||||
|
||||
+17
-10
@@ -1,3 +1,4 @@
|
||||
import { StructuredContentMcpClient } from "../providers/StructuredContentMcpClient.js";
|
||||
import {
|
||||
createIdaDiagnosticRedactor,
|
||||
IDA_DIAGNOSTIC_SOURCE_BYTES,
|
||||
@@ -8,7 +9,6 @@ import { ChildProcess } from "node:child_process";
|
||||
import { Socket } from "node:net";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import {
|
||||
Client,
|
||||
ProtocolError,
|
||||
SdkError,
|
||||
SdkErrorCode,
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
idaRegistrationTimeoutGuidance,
|
||||
type IdaConfiguration,
|
||||
} from "./IdaConfiguration.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
jsonObjectSchema,
|
||||
jsonValueSchema,
|
||||
type JsonValue,
|
||||
} from "../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
AnalysisTimeoutError,
|
||||
@@ -106,6 +110,8 @@ export const decodeIdaToolResult = (
|
||||
diagnostic: IdaDiagnosticRedactor = createIdaDiagnosticRedactor(),
|
||||
): JsonValue => {
|
||||
const result = toolResultSchema.parse(input);
|
||||
// Shape validation's record copy can omit own __proto__ members; JSON owns the original dictionary.
|
||||
const source = input as z.input<typeof toolResultSchema>;
|
||||
const blocks = result.content.flatMap((block) =>
|
||||
block.type === "text" && block.text !== undefined ? [block.text] : [],
|
||||
);
|
||||
@@ -138,13 +144,11 @@ export const decodeIdaToolResult = (
|
||||
);
|
||||
return parseIdaJsonText(text);
|
||||
}
|
||||
if (result.structuredContent !== undefined) {
|
||||
const value = result.structuredContent;
|
||||
return jsonValueSchema.parse(
|
||||
Object.keys(value).length === 1 && "result" in value
|
||||
? value.result
|
||||
: value,
|
||||
);
|
||||
if (source.structuredContent !== undefined) {
|
||||
const value = jsonObjectSchema.parse(source.structuredContent);
|
||||
return Object.keys(value).length === 1 && Object.hasOwn(value, "result")
|
||||
? value.result!
|
||||
: value;
|
||||
}
|
||||
if (text === undefined)
|
||||
throw new AnalysisProtocolError(
|
||||
@@ -360,7 +364,10 @@ export const createIdaMcpConnection = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
dependencies: IdaMcpConnectionDependencies = {},
|
||||
): IdaMcpConnection => {
|
||||
const client = new Client({ name: "rea-ida-provider", version: "1" });
|
||||
const client = new StructuredContentMcpClient({
|
||||
name: "rea-ida-provider",
|
||||
version: "1",
|
||||
});
|
||||
const redactor = (
|
||||
dependencies.createDiagnosticRedactor ?? createIdaDiagnosticRedactor
|
||||
)(config);
|
||||
|
||||
@@ -446,3 +446,19 @@ it("passes a discovery budget above the SDK default to connect and every catalog
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"retains structured own members and nested dictionaries in a JEB reply (isError=%s)",
|
||||
(isError) => {
|
||||
const structured: unknown = JSON.parse(
|
||||
'{"__proto__":{"observed":true},"success":false,"message":"missing","nested":[{"__proto__":"nested fact"}]}',
|
||||
);
|
||||
const decoded = decodeJebToolResult({
|
||||
isError,
|
||||
structuredContent: structured,
|
||||
content: [],
|
||||
});
|
||||
expect(Object.hasOwn(decoded as object, "__proto__")).toBe(true);
|
||||
expect(JSON.stringify(decoded)).toBe(JSON.stringify(structured));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,7 +3,11 @@ import {
|
||||
DEFAULT_REQUEST_TIMEOUT_MSEC,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import { z } from "zod";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
jsonObjectSchema,
|
||||
jsonValueSchema,
|
||||
type JsonValue,
|
||||
} from "../domain/jsonValue.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisProtocolError,
|
||||
@@ -57,12 +61,14 @@ const toolResultSchema = z.object({
|
||||
/** Decode a JEB MCP tool result without inventing an empty observation. */
|
||||
export const decodeJebToolResult = (input: unknown): JsonValue => {
|
||||
const result = toolResultSchema.parse(input);
|
||||
// Shape validation's record copy can omit own __proto__ members; JSON owns the original dictionary.
|
||||
const source = input as z.input<typeof toolResultSchema>;
|
||||
const blocks = result.content.flatMap((block) =>
|
||||
block.type === "text" && block.text !== undefined ? [block.text] : [],
|
||||
);
|
||||
let decoded: JsonValue;
|
||||
if (result.structuredContent !== undefined) {
|
||||
decoded = jsonValueSchema.parse(result.structuredContent);
|
||||
if (source.structuredContent !== undefined) {
|
||||
decoded = jsonObjectSchema.parse(source.structuredContent);
|
||||
} else {
|
||||
if (blocks.length !== 1)
|
||||
throw new AnalysisProtocolError(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StructuredContentMcpClient } from "../providers/StructuredContentMcpClient.js";
|
||||
import {
|
||||
Client,
|
||||
SdkError,
|
||||
SdkErrorCode,
|
||||
StreamableHTTPClientTransport,
|
||||
@@ -53,7 +53,10 @@ export class JebMcpAcquisitionCleanupFailure extends ProviderCleanupError {
|
||||
|
||||
/** One SDK client owns its attached transport; failed cleanup retains both identities. */
|
||||
export class JebMcpConnectionOwner {
|
||||
readonly client = new Client({ name: "rea-jeb", version: "0.0.0" });
|
||||
readonly client = new StructuredContentMcpClient({
|
||||
name: "rea-jeb",
|
||||
version: "0.0.0",
|
||||
});
|
||||
readonly transport: StreamableHTTPClientTransport;
|
||||
readonly signal = new AbortController();
|
||||
tools: readonly string[] | undefined;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { safeParseUtf8Json } from "../../domain/safeJson.js";
|
||||
import {
|
||||
AnalysisCleanupLease,
|
||||
AnalysisCleanupLeaseGroup,
|
||||
@@ -387,7 +388,9 @@ export class PwntoolsDecoder<
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
|
||||
const decoded = safeParseUtf8Json(file.bytes);
|
||||
if (!decoded.ok) throw decoded.cause;
|
||||
reply = replySchema.parse(decoded.value);
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { safeParseUtf8Json } from "../../domain/safeJson.js";
|
||||
import { join } from "node:path";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
|
||||
import {
|
||||
PWNTOOLS_MEMORY_FAILURE_EXIT,
|
||||
PWNTOOLS_FILE_SIZE_FAILURE_EXIT,
|
||||
PWNTOOLS_LIMITS,
|
||||
} from "./PwntoolsRelease.js";
|
||||
import { z } from "zod";
|
||||
import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
|
||||
/** Actual limits reported by the owned bridge after lowering inherited soft limits. */
|
||||
@@ -94,10 +95,10 @@ export const readPwntoolsFailureEvidence = async (
|
||||
undefined,
|
||||
resources,
|
||||
);
|
||||
const decoded = safeParseUtf8Json(reported.bytes);
|
||||
if (!decoded.ok) throw decoded.cause;
|
||||
limitReport = {
|
||||
limits: pwntoolsResourceLimitsSchema.parse(
|
||||
JSON.parse(reported.bytes.toString("utf8")),
|
||||
),
|
||||
limits: pwntoolsResourceLimitsSchema.parse(decoded.value),
|
||||
failure: null,
|
||||
};
|
||||
} catch (reportFailure: unknown) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { InMemoryTransport, type Tool } from "@modelcontextprotocol/client";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { StructuredContentMcpClient } from "./StructuredContentMcpClient.js";
|
||||
|
||||
/** Raw peer replies pass through the actual SDK protocol and era validator. */
|
||||
const exchange = async (
|
||||
reply: Record<string, unknown>,
|
||||
run: (client: StructuredContentMcpClient) => Promise<void>,
|
||||
) => {
|
||||
const client = new StructuredContentMcpClient({
|
||||
name: "structured-content-test",
|
||||
version: "1",
|
||||
});
|
||||
const [local, peer] = InMemoryTransport.createLinkedPair();
|
||||
peer.onmessage = async (message) => {
|
||||
if (
|
||||
!("method" in message) ||
|
||||
!("id" in message) ||
|
||||
message.id === undefined
|
||||
)
|
||||
return;
|
||||
const result =
|
||||
message.method === "initialize"
|
||||
? {
|
||||
protocolVersion: "2025-11-25",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "raw-peer", version: "1" },
|
||||
}
|
||||
: message.method === "tools/list"
|
||||
? { tools: [] }
|
||||
: message.method === "ping"
|
||||
? {}
|
||||
: reply;
|
||||
await peer.send({ jsonrpc: "2.0", id: message.id, result });
|
||||
};
|
||||
try {
|
||||
await peer.start();
|
||||
await client.connect(local);
|
||||
await run(client);
|
||||
} finally {
|
||||
await client.close();
|
||||
await peer.close();
|
||||
}
|
||||
};
|
||||
|
||||
it("preserves structured own properties while retaining legacy default content", async () => {
|
||||
const structuredContent: unknown = JSON.parse(
|
||||
'{"__proto__":{"observed":true},"result":"field"}',
|
||||
);
|
||||
await exchange({ structuredContent }, async (client) => {
|
||||
const result = await client.callTool({ name: "observed", arguments: {} });
|
||||
expect(result.content).toEqual([]);
|
||||
expect(Object.hasOwn(result.structuredContent as object, "__proto__")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(JSON.stringify(result.structuredContent)).toBe(
|
||||
JSON.stringify(structuredContent),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["task", "inputRequests", "requestState"])(
|
||||
"retains the SDK's legacy contentless foreign-family rejection for %s",
|
||||
async (key) => {
|
||||
await exchange({ [key]: {} }, async (client) => {
|
||||
await expect(
|
||||
client.callTool({ name: "foreign", arguments: {} }),
|
||||
).rejects.toThrow(/content is required/u);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("retains inherited callTool output-schema validation", async () => {
|
||||
const tool: Tool = {
|
||||
name: "typed",
|
||||
inputSchema: { type: "object" },
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { count: { type: "integer" } },
|
||||
required: ["count"],
|
||||
},
|
||||
};
|
||||
await exchange(
|
||||
{ content: [], structuredContent: { count: "wrong" } },
|
||||
async (client) => {
|
||||
await expect(
|
||||
client.callTool(
|
||||
{ name: tool.name, arguments: {} },
|
||||
{ toolDefinition: tool },
|
||||
),
|
||||
).rejects.toThrow(/does not match the tool's output schema/u);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("delegates explicitly supplied result schemas and other method requests unchanged", async () => {
|
||||
const reply = { observed: "custom result" };
|
||||
const schema = z
|
||||
.strictObject({ observed: z.string() })
|
||||
.transform(({ observed }) => observed);
|
||||
await exchange(reply, async (client) => {
|
||||
expect(
|
||||
await client.request(
|
||||
{ method: "tools/call", params: { name: "custom", arguments: {} } },
|
||||
schema,
|
||||
),
|
||||
).toBe("custom result");
|
||||
expect(await client.ping()).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Client,
|
||||
type CallToolResult,
|
||||
type Request,
|
||||
type RequestMethod,
|
||||
type RequestOptions,
|
||||
type ResultTypeMap,
|
||||
type StandardSchemaV1,
|
||||
} from "@modelcontextprotocol/client";
|
||||
|
||||
const isSchema = (
|
||||
value: RequestOptions | StandardSchemaV1 | undefined,
|
||||
): value is StandardSchemaV1 =>
|
||||
typeof value === "object" && value !== null && "~standard" in value;
|
||||
|
||||
/** Preserve observed structured dictionaries across the SDK's era-specific result validation. */
|
||||
export class StructuredContentMcpClient extends Client {
|
||||
readonly #toolResult: StandardSchemaV1<unknown, CallToolResult> = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "rea-mcp-structured-content",
|
||||
validate: (input) => {
|
||||
const parsed = this._wireCodec().validateResult("tools/call", input);
|
||||
if (!parsed.ok)
|
||||
return {
|
||||
issues: [
|
||||
{
|
||||
message:
|
||||
parsed.reason === "invalid"
|
||||
? parsed.message
|
||||
: "not-in-era: tools/call",
|
||||
},
|
||||
],
|
||||
};
|
||||
// Retain the SDK's envelope normalization, replacing only the dictionary
|
||||
// its legacy record parser may clone without own __proto__ members.
|
||||
if (
|
||||
typeof input === "object" &&
|
||||
input !== null &&
|
||||
Object.hasOwn(input, "structuredContent")
|
||||
)
|
||||
return {
|
||||
value: {
|
||||
...parsed.value,
|
||||
structuredContent: (input as { structuredContent: unknown })
|
||||
.structuredContent,
|
||||
},
|
||||
};
|
||||
return { value: parsed.value };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
override request<Method extends RequestMethod>(
|
||||
request: { method: Method; params?: Record<string, unknown> },
|
||||
options?: RequestOptions,
|
||||
): Promise<ResultTypeMap[Method]>;
|
||||
override request<Schema extends StandardSchemaV1>(
|
||||
request: Request,
|
||||
resultSchema: Schema,
|
||||
options?: RequestOptions,
|
||||
): Promise<StandardSchemaV1.InferOutput<Schema>>;
|
||||
override request(
|
||||
request: Request,
|
||||
schemaOrOptions?: RequestOptions | StandardSchemaV1,
|
||||
options?: RequestOptions,
|
||||
): Promise<unknown> {
|
||||
if (isSchema(schemaOrOptions))
|
||||
return super.request(request, schemaOrOptions, options);
|
||||
if (request.method === "tools/call")
|
||||
return super.request(request, this.#toolResult, schemaOrOptions);
|
||||
return super.request(
|
||||
request as { method: RequestMethod; params?: Record<string, unknown> },
|
||||
schemaOrOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -519,6 +519,9 @@ it
|
||||
"output-limit",
|
||||
"wrong-profile",
|
||||
"malformed-reply",
|
||||
"invalid-utf8",
|
||||
"literal-replacement",
|
||||
"bom-reply",
|
||||
])(
|
||||
"preserves process/reply boundaries and cleans private snapshots: %s",
|
||||
async (scenario) => {
|
||||
@@ -542,32 +545,50 @@ it
|
||||
diagnostics: _diagnostics,
|
||||
...value
|
||||
} = binaryLayoutFixture(path);
|
||||
await writeFile(
|
||||
request.reply_path,
|
||||
scenario === "malformed-reply"
|
||||
? "broken-json"
|
||||
: JSON.stringify(
|
||||
[
|
||||
"format",
|
||||
"unavailable",
|
||||
"unsupported",
|
||||
"output-limit",
|
||||
].includes(scenario)
|
||||
? {
|
||||
ok: false,
|
||||
reason: scenario,
|
||||
message: "ELF table is truncated.",
|
||||
}
|
||||
: {
|
||||
ok: true,
|
||||
profile:
|
||||
scenario === "wrong-profile"
|
||||
? "pwntools@other"
|
||||
: PWNTOOLS_PROVIDER_IDENTITY.version,
|
||||
value,
|
||||
},
|
||||
),
|
||||
);
|
||||
const encodedReplies: Partial<Record<string, string | Buffer>> = {
|
||||
"invalid-utf8": Buffer.concat([
|
||||
Buffer.from('{"ok":false,"reason":"format","message":"'),
|
||||
Buffer.from([0xff]),
|
||||
Buffer.from('"}'),
|
||||
]),
|
||||
"literal-replacement": JSON.stringify({
|
||||
ok: false,
|
||||
reason: "format",
|
||||
message: "valid �",
|
||||
}),
|
||||
"bom-reply": '\uFEFF{"ok":false,"reason":"format","message":"valid"}',
|
||||
};
|
||||
const encodedReply = encodedReplies[scenario];
|
||||
if (encodedReply !== undefined)
|
||||
await writeFile(request.reply_path, encodedReply);
|
||||
else {
|
||||
await writeFile(
|
||||
request.reply_path,
|
||||
scenario === "malformed-reply"
|
||||
? "broken-json"
|
||||
: JSON.stringify(
|
||||
[
|
||||
"format",
|
||||
"unavailable",
|
||||
"unsupported",
|
||||
"output-limit",
|
||||
].includes(scenario)
|
||||
? {
|
||||
ok: false,
|
||||
reason: scenario,
|
||||
message: "ELF table is truncated.",
|
||||
}
|
||||
: {
|
||||
ok: true,
|
||||
profile:
|
||||
scenario === "wrong-profile"
|
||||
? "pwntools@other"
|
||||
: PWNTOOLS_PROVIDER_IDENTITY.version,
|
||||
value,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return spawnOwnedProviderProcess({
|
||||
...spawn,
|
||||
command: process.execPath,
|
||||
@@ -576,6 +597,11 @@ it
|
||||
},
|
||||
);
|
||||
const result = await provider.inspect({ path });
|
||||
if (scenario === "literal-replacement")
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { issues: [{ message: "valid �" }] },
|
||||
});
|
||||
if (scenario === "success") {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.artifact).toMatchObject({
|
||||
@@ -589,7 +615,7 @@ it
|
||||
ok: false,
|
||||
error: {
|
||||
_tag:
|
||||
scenario === "format"
|
||||
scenario === "format" || scenario === "literal-replacement"
|
||||
? "AnalysisInputError"
|
||||
: scenario === "unavailable"
|
||||
? "ProviderSelectionError"
|
||||
|
||||
@@ -381,6 +381,9 @@ it
|
||||
"wrong-version",
|
||||
"wrong-decoded-carrier",
|
||||
"broken-reply",
|
||||
"invalid-utf8",
|
||||
"literal-replacement",
|
||||
"bom-reply",
|
||||
"out-of-range-body",
|
||||
])(
|
||||
"binds source carrier/worker representation and releases owned files: %s",
|
||||
@@ -405,45 +408,64 @@ it
|
||||
);
|
||||
const hex = scenario === "wrong-decoded-carrier" ? "00" : "60006000f3";
|
||||
const bytes = Buffer.from(hex, "hex");
|
||||
await writeFile(
|
||||
request.reply_path,
|
||||
scenario === "broken-reply"
|
||||
? "broken"
|
||||
: JSON.stringify(
|
||||
["format", "unsupported", "output-limit"].includes(scenario)
|
||||
? {
|
||||
ok: false,
|
||||
reason: scenario,
|
||||
message:
|
||||
scenario === "output-limit"
|
||||
? "Complete interface evidence exceeds the 16 MiB reply budget."
|
||||
: "Malformed hex carrier.",
|
||||
}
|
||||
: {
|
||||
ok: true,
|
||||
version: scenario === "wrong-version" ? "0.0.0" : "0.9.3",
|
||||
bytecode: {
|
||||
sha256: createHash("sha256")
|
||||
.update(bytes)
|
||||
.digest("hex"),
|
||||
bytes: bytes.length,
|
||||
hex,
|
||||
const encodedReplies: Partial<Record<string, string | Buffer>> = {
|
||||
"invalid-utf8": Buffer.concat([
|
||||
Buffer.from('{"ok":false,"reason":"format","message":"'),
|
||||
Buffer.from([0xff]),
|
||||
Buffer.from('"}'),
|
||||
]),
|
||||
"literal-replacement": JSON.stringify({
|
||||
ok: false,
|
||||
reason: "format",
|
||||
message: "valid �",
|
||||
}),
|
||||
"bom-reply": '\uFEFF{"ok":false,"reason":"format","message":"valid"}',
|
||||
};
|
||||
const encodedReply = encodedReplies[scenario];
|
||||
if (encodedReply !== undefined)
|
||||
await writeFile(request.reply_path, encodedReply);
|
||||
else {
|
||||
await writeFile(
|
||||
request.reply_path,
|
||||
scenario === "broken-reply"
|
||||
? "broken"
|
||||
: JSON.stringify(
|
||||
["format", "unsupported", "output-limit"].includes(scenario)
|
||||
? {
|
||||
ok: false,
|
||||
reason: scenario,
|
||||
message:
|
||||
scenario === "output-limit"
|
||||
? "Complete interface evidence exceeds the 16 MiB reply budget."
|
||||
: "Malformed hex carrier.",
|
||||
}
|
||||
: {
|
||||
ok: true,
|
||||
version:
|
||||
scenario === "wrong-version" ? "0.0.0" : "0.9.3",
|
||||
bytecode: {
|
||||
sha256: createHash("sha256")
|
||||
.update(bytes)
|
||||
.digest("hex"),
|
||||
bytes: bytes.length,
|
||||
hex,
|
||||
},
|
||||
raw: {
|
||||
functions:
|
||||
scenario === "out-of-range-body"
|
||||
? [
|
||||
{
|
||||
selector: "aabbccdd",
|
||||
bytecodeOffset: 5,
|
||||
dispatch: "abi",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
raw: {
|
||||
functions:
|
||||
scenario === "out-of-range-body"
|
||||
? [
|
||||
{
|
||||
selector: "aabbccdd",
|
||||
bytecodeOffset: 5,
|
||||
dispatch: "abi",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
return spawnOwnedProviderProcess({
|
||||
...spawn,
|
||||
command: process.execPath,
|
||||
@@ -452,6 +474,11 @@ it
|
||||
},
|
||||
);
|
||||
const result = await provider.inspect({ path, encoding: "hex" });
|
||||
if (scenario === "literal-replacement")
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { issues: [{ message: "valid �" }] },
|
||||
});
|
||||
if (scenario === "success") {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
@@ -465,7 +492,7 @@ it
|
||||
ok: false,
|
||||
error: {
|
||||
_tag:
|
||||
scenario === "format"
|
||||
scenario === "format" || scenario === "literal-replacement"
|
||||
? "AnalysisInputError"
|
||||
: scenario === "unsupported"
|
||||
? "AnalysisUnsupportedTargetError"
|
||||
|
||||
@@ -214,6 +214,20 @@ it("uses the pinned SDK for a local Streamable HTTP handshake, paged discovery,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("retains own structured fields through the SDK before scalar-wrapper selection", async () => {
|
||||
const structuredContent = {
|
||||
["__proto__"]: { observed: true },
|
||||
result: "ordinary field",
|
||||
nested: [{ ["__proto__"]: "nested fact" }],
|
||||
};
|
||||
const { connection } = await fixture({
|
||||
toolResult: { content: [], structuredContent },
|
||||
});
|
||||
await connection.connect();
|
||||
const result = await connection.call("first", {});
|
||||
expect(JSON.stringify(result)).toBe(JSON.stringify(structuredContent));
|
||||
});
|
||||
|
||||
it("omits an oversized HTTP error body before retaining or redacting it", async () => {
|
||||
const { connection } = await fixture({
|
||||
toolStatus: 500,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createStreamableHttpJebMcpConnection } from "../../../src/jeb/JebMcpConnection.js";
|
||||
import { startJebMcpFixture } from "../../fixtures/jebMcp.js";
|
||||
|
||||
it("retains own structured fields through the JEB SDK transport", async () => {
|
||||
const expected = {
|
||||
["__proto__"]: { observed: true },
|
||||
success: true,
|
||||
nested: [{ ["__proto__"]: "nested fact" }],
|
||||
};
|
||||
const fixture = await startJebMcpFixture(() => expected);
|
||||
const connection = createStreamableHttpJebMcpConnection(
|
||||
new URL(fixture.endpoint),
|
||||
);
|
||||
try {
|
||||
await connection.connect();
|
||||
const result = await connection.call("get_client_information", {});
|
||||
expect(JSON.stringify(result)).toBe(JSON.stringify(expected));
|
||||
} finally {
|
||||
try {
|
||||
await connection.close();
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user