fix(mcp): advertise reserved process environment key (#949)
This commit is contained in:
1 parent
f14b6a8430
commit
e0d76ae80d
8 files changed
+122
-26
No files matched your search
@@ -669,9 +669,9 @@
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "d0b9e5c99d6a2c9bdf065429db901231326a7de5910f2d878cee124891dba57e",
|
||||
"tools_sha256": "d04bfd74f6bafcaa1f74c68971adf2a3304dd241e2f0803448f052e350d04055",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "188d726867808cd63764625b93d89e2936af29d9943dbc0e5d22044294521e1d",
|
||||
"combined_sha256": "dc6c3d015d861c8d560139abbb37988eb429165378510f599e1ca57a4f084cfe",
|
||||
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_4bb7d6acd148fffbd7e696cd56b22ed7a031fcb4b51473170058b4a9e902ee44",
|
||||
"manifest_id": "ecm_bc201add488cd7ff585d7f6d6d0100b3ef88221fd1a36e41c73d93c8df8105eb",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "1738df3db4b39d4967e40cc147d24393fd8e1e21fc4f7b7b45da30ce8880c2a9"
|
||||
"sha256": "9c694b312964402d9d2975a5350fbbb4fcabe512f0543c6f536eaa53ba7801ee"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
||||
metadata:
|
||||
version: "27"
|
||||
tool_count: 131
|
||||
catalog_digest: "188d726867808cd63764625b93d89e2936af29d9943dbc0e5d22044294521e1d"
|
||||
catalog_digest: "dc6c3d015d861c8d560139abbb37988eb429165378510f599e1ca57a4f084cfe"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -80,7 +80,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"capture_process_scenario",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
processScenarioSchema,
|
||||
),
|
||||
session(
|
||||
|
||||
@@ -28,13 +28,37 @@ describe("direct process scenarios", () => {
|
||||
});
|
||||
|
||||
it("rejects malformed environment names and adapter-reserved identity", () => {
|
||||
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"])
|
||||
expect(
|
||||
processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { [name]: "value" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"]) {
|
||||
const result = processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { [name]: "value" },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (name === "" && !result.success) {
|
||||
const invalidKey = result.error.issues[0];
|
||||
expect(invalidKey?.code).toBe("invalid_key");
|
||||
if (invalidKey?.code === "invalid_key") {
|
||||
expect(invalidKey.issues[0]?.message).toBe(
|
||||
"Environment names must not be empty",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (name === "REA_PROCESS_RUN_ID" && !result.success) {
|
||||
const invalidKey = result.error.issues[0];
|
||||
expect(invalidKey?.code).toBe("invalid_key");
|
||||
if (invalidKey?.code === "invalid_key") {
|
||||
expect(invalidKey.issues[0]?.message).toBe(
|
||||
"REA_PROCESS_RUN_ID is reserved by the process adapter",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(
|
||||
processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { "REA_PROCESS_RUN_ID\n": "caller-value" },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps bounded capture controls and ordered scheduled interaction", () => {
|
||||
|
||||
@@ -4,10 +4,16 @@ import { z } from "zod";
|
||||
|
||||
const positiveBudget = z.number().int().safe().positive();
|
||||
const timedEventBase = { at_ms: z.number().int().safe().nonnegative() };
|
||||
const reservedRunIdEnvironmentName = "REA_PROCESS_RUN_ID";
|
||||
const environmentName = z
|
||||
.string()
|
||||
.regex(/^[^=\0]+$/u, "Environment names cannot contain '=' or NUL");
|
||||
const reservedEnvironment = new Set(["REA_PROCESS_RUN_ID"]);
|
||||
.min(1, "Environment names must not be empty")
|
||||
.regex(/^(?!REA_PROCESS_RUN_ID(?![\s\S]))[^=\0]+$/u, {
|
||||
error: (issue) =>
|
||||
issue.input === reservedRunIdEnvironmentName
|
||||
? `${reservedRunIdEnvironmentName} is reserved by the process adapter`
|
||||
: "Environment names cannot contain '=' or NUL",
|
||||
});
|
||||
export const normalizationSchema = z.object({
|
||||
paths: z.boolean(),
|
||||
pids: z.boolean(),
|
||||
@@ -170,16 +176,6 @@ export const processScenarioSchema = z
|
||||
});
|
||||
}
|
||||
}
|
||||
const explicit = new Set(Object.keys(scenario.environment));
|
||||
for (const name of explicit) {
|
||||
if (reservedEnvironment.has(name)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: `${name} is reserved by the process adapter`,
|
||||
path: ["environment", name],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const event of scenario.events) {
|
||||
if (event.at_ms > scenario.timeout_ms) {
|
||||
context.addIssue({
|
||||
|
||||
@@ -30,7 +30,7 @@ interface GeneratedPayload {
|
||||
}
|
||||
|
||||
const GENERATED_PAYLOAD_JSON =
|
||||
'{\n "auxiliaryProviders": [\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inventory_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "extract_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "decode_interface_builder",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_keyed_archive",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DLine truncated
|
||||
'{\n "auxiliaryProviders": [\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inventory_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "extract_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "decode_interface_builder",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_keyed_archive",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DLine truncated
|
||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
||||
if (!isGeneratedPayload(generatedPayload))
|
||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
|
||||
import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js";
|
||||
import { processScenarioSchema } from "../../../src/domain/processScenario.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js";
|
||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
||||
|
||||
@@ -27,6 +28,76 @@ function schemaErrors(tools: readonly ToolSchemas[]): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
const advertiseAndEnforceProcessEnvironmentKeyConstraint =
|
||||
async (): Promise<void> => {
|
||||
const contract = TOOL_CONTRACTS.find(
|
||||
({ name }) => name === "capture_process_scenario",
|
||||
);
|
||||
if (contract === undefined)
|
||||
throw new Error("Process capture contract was not registered");
|
||||
|
||||
let handlerCalled = false;
|
||||
const server = new McpServer({ name: "process-schema", version: "0" });
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
{
|
||||
title: contract.title,
|
||||
description: contract.description,
|
||||
inputSchema: processScenarioSchema.shape,
|
||||
},
|
||||
async () => {
|
||||
handlerCalled = true;
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "handler ran" }],
|
||||
isError: true,
|
||||
};
|
||||
},
|
||||
);
|
||||
const client = new Client({ name: "process-schema", version: "0" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
({ name }) => name === contract.name,
|
||||
);
|
||||
if (advertised === undefined)
|
||||
throw new Error("Process capture tool was not advertised");
|
||||
|
||||
const validate = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(advertised.inputSchema);
|
||||
const valid = { executable: "node", environment: { APP_MODE: "test" } };
|
||||
const reserved = {
|
||||
executable: "node",
|
||||
environment: { REA_PROCESS_RUN_ID: "caller-value" },
|
||||
};
|
||||
const reservedWithTrailingNewline = {
|
||||
executable: "node",
|
||||
environment: { "REA_PROCESS_RUN_ID\n": "caller-value" },
|
||||
};
|
||||
expect(validate(valid)).toBe(true);
|
||||
expect(contract.inputSchema.safeParse(valid).success).toBe(true);
|
||||
expect(validate(reserved)).toBe(false);
|
||||
expect(contract.inputSchema.safeParse(reserved).success).toBe(false);
|
||||
expect(validate(reservedWithTrailingNewline)).toBe(true);
|
||||
expect(
|
||||
contract.inputSchema.safeParse(reservedWithTrailingNewline).success,
|
||||
).toBe(true);
|
||||
|
||||
const result = await client.callTool({
|
||||
name: contract.name,
|
||||
arguments: reserved,
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(handlerCalled).toBe(false);
|
||||
} finally {
|
||||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
};
|
||||
|
||||
describe("MCP JSON Schema validity", () => {
|
||||
it("preserves empty-array validation in the advertised representation", () => {
|
||||
const schema = z.toJSONSchema(emptyArraySchema);
|
||||
@@ -154,3 +225,8 @@ describe("MCP JSON Schema validity", () => {
|
||||
expect(schemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"advertises and enforces the process-owned environment key constraint",
|
||||
advertiseAndEnforceProcessEnvironmentKeyConstraint,
|
||||
);
|
||||
Reference in new issue
Block a user