Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/host-selfhost/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@
# Allow sandboxed code to reach loopback/private network addresses. Off by
# default — adversarial generated code should not reach your internal network.
# EXECUTOR_ALLOW_LOCAL_NETWORK=false

# --- Local stdio MCP (trusted deployments only) -------------------------------
# Stdio MCP is disabled unless this is explicitly set to the exact string
# "true". Enabling it lets users configure MCP servers whose commands execute
# on this host.
# EXECUTOR_ALLOW_STDIO_MCP=true
12 changes: 7 additions & 5 deletions apps/host-selfhost/executor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import { resolveSecretKey } from "./src/config";
// Single source of truth for the self-hosted app's plugin list.
//
// Self-host runs the same protocol/provider plugins as cloud, minus the
// multi-tenant-only secret backends (WorkOS Vault). `dangerouslyAllowStdioMCP`
// is false: a server reachable by multiple users must not let one user spawn
// arbitrary stdio MCP processes on the host. The encrypted DB secret provider
// (slice 4) is added here as the first writable secret provider.
// multi-tenant-only secret backends (WorkOS Vault). Stdio MCP remains opt-in:
// a server reachable by multiple users must not let one user spawn arbitrary
// stdio MCP processes on the host. The encrypted DB secret provider (slice 4)
// is added here as the first writable secret provider.
// ---------------------------------------------------------------------------

interface SelfHostPluginDeps {
Expand All @@ -39,7 +39,9 @@ export default defineExecutorConfig({
presets: [...googleCatalog, ...microsoftCatalog],
specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter],
}),
mcpHttpPlugin({ dangerouslyAllowStdioMCP: false }),
mcpHttpPlugin({
dangerouslyAllowStdioMCP: process.env.EXECUTOR_ALLOW_STDIO_MCP === "true",
}),
Comment on lines +42 to +44
graphqlHttpPlugin(),
toolkitsPlugin({ activeToolkitSlug }),
// First writable secret provider -> the default for `secrets.set`.
Expand Down
59 changes: 59 additions & 0 deletions apps/host-selfhost/src/executor-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { afterEach, beforeEach, expect, test } from "@effect/vitest";

import executorConfig from "../executor.config";

const ENV_NAME = "EXECUTOR_ALLOW_STDIO_MCP";
const SECRET_ENV_NAME = "EXECUTOR_SECRET_KEY";
const originalValue = process.env[ENV_NAME];
const originalSecret = process.env[SECRET_ENV_NAME];

beforeEach(() => {
process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret";
});

afterEach(() => {
if (originalValue === undefined) {
delete process.env[ENV_NAME];
} else {
process.env[ENV_NAME] = originalValue;
}
if (originalSecret === undefined) {
delete process.env[SECRET_ENV_NAME];
} else {
process.env[SECRET_ENV_NAME] = originalSecret;
}
});

const allowStdio = (): boolean => {
const mcp = executorConfig.plugins().find((plugin) => plugin.id === "mcp");
expect(mcp).toBeDefined();
const clientConfig = mcp?.clientConfig;
if (
clientConfig &&
typeof clientConfig === "object" &&
"allowStdio" in clientConfig &&
typeof clientConfig.allowStdio === "boolean"
) {
return clientConfig.allowStdio;
}
expect.fail("MCP plugin did not expose its stdio setting");
return false;
};

test("stdio MCP stays disabled when the opt-in is absent", () => {
delete process.env[ENV_NAME];
expect(allowStdio()).toBe(false);
});

test("stdio MCP stays disabled unless the opt-in is exactly true", () => {
process.env[ENV_NAME] = "false";
expect(allowStdio()).toBe(false);

process.env[ENV_NAME] = "TRUE";
expect(allowStdio()).toBe(false);
});

test("stdio MCP is enabled when the opt-in is exactly true", () => {
process.env[ENV_NAME] = "true";
expect(allowStdio()).toBe(true);
});