-
Notifications
You must be signed in to change notification settings - Fork 601
fix(agent): harden MCP delivery — Daytona loopback guard, Pi user-MCP fail-loud, http SSRF guard #4853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(agent): harden MCP delivery — Daytona loopback guard, Pi user-MCP fail-loud, http SSRF guard #4853
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,70 @@ export interface McpServerHttp { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** One delivered MCP server: a (disabled) stdio entry or an enabled HTTP entry. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type McpServerEntry = McpServerStdio | McpServerHttp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * SSRF guard for a user HTTP MCP `url`. The runner emits the run's Agenta-resolved named secrets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * as request headers to this author-supplied URL, so an attacker-controlled config could point it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * at an internal/metadata endpoint and exfiltrate a credential (a classic server-side request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * forgery). The capability is flag-gated (`AGENTA_AGENT_ENABLE_MCP`, off by default) and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * config-trust, so a scheme + host guard is enough rather than full DNS-resolution pinning: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - require `https` (the secret rides in a header; `http` would send it in clear text). Opt out | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * for a known-safe non-https endpoint by listing its host in `AGENTA_AGENT_MCP_HOST_ALLOWLIST`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - reject loopback, link-local (incl. the `169.254.169.254` cloud metadata host), and private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * address literals unless the host is in `AGENTA_AGENT_MCP_HOST_ALLOWLIST` (comma-separated). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns an error message string when the URL is rejected, or `undefined` when it is allowed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function mcpHostAllowlist(): Set<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Set( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (process.env.AGENTA_AGENT_MCP_HOST_ALLOWLIST ?? "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(",") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((h) => h.trim().toLowerCase()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** True for a hostname/IP literal that must not receive a credentialed request (SSRF sinks). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isInternalHost(host: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const h = host.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (h === "localhost" || h.endsWith(".localhost")) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // IPv6 loopback / unspecified (URL host keeps the brackets, e.g. "[::1]"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (h === "[::1]" || h === "[::]" || h === "::1" || h === "::") return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Link-local IPv6 (fe80::/10) and unique-local IPv6 (fc00::/7) literals. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (/^\[?f[cde]/.test(h)) return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // IPv4 dotted-quad literals: loopback, link-local (incl. 169.254.169.254 metadata), private. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (m) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [a, b] = [Number(m[1]), Number(m[2])]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 127) return true; // loopback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 169 && b === 254) return true; // link-local + cloud metadata (169.254.169.254) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 10) return true; // private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 0) return true; // "this host" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 172 && b >= 16 && b <= 31) return true; // private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (a === 192 && b === 168) return true; // private | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function validateUserMcpUrl(rawUrl: string): string | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let parsed: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsed = new URL(rawUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `http MCP server url is not a valid URL: ${rawUrl}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allowlist = mcpHostAllowlist(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const host = parsed.hostname.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allowed = allowlist.has(host); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.protocol !== "https:" && !allowed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `http MCP server url must use https (got ${parsed.protocol.replace(":", "")}): ${rawUrl}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isInternalHost(host) && !allowed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `http MCP server url targets an internal/metadata host (${host}); not allowed: ${rawUrl}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Convert USER-declared MCP servers into ACP entries. (This is the USER MCP capability layer — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * distinct from the INTERNAL gateway-tool channel below; see `buildSessionMcpServers`.) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,6 +111,8 @@ export type McpServerEntry = McpServerStdio | McpServerHttp; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * than being delivered. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - A server that is neither a valid http (no `url`) nor a valid stdio (no `command`) is skipped | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * with a log — it was never deliverable. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - An http `url` that fails the SSRF guard (`validateUserMcpUrl`: non-https, or an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * internal/metadata host) throws, so a credentialed request is never sent to an internal sink. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function toAcpMcpServers( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| servers: McpServerConfig[] | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -61,6 +127,10 @@ export function toAcpMcpServers( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log(`skipping http MCP server '${s?.name ?? "?"}' (no url)`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SSRF guard: the resolved named secret rides as a header on this author-supplied URL, so | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // reject a non-https / internal / metadata target before any credential is attached. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const urlError = validateUserMcpUrl(s.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (urlError) throw new Error(urlError); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "http", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: s.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -88,6 +158,16 @@ export interface BuildSessionMcpServersInput { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPi: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| capabilities: HarnessCapabilities; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| harness: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * True when the run executes in a REMOTE Daytona sandbox (the harness runs IN the sandbox, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * not on the runner host). Gates the internal gateway-tool channel: the channel's loopback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (`127.0.0.1`) HTTP MCP URL resolves to the SANDBOX's loopback there, not the runner's, so | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * advertising it would hand the in-sandbox harness an unreachable URL. On Daytona the channel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * is skipped and gateway tools are delivered through the file relay instead (the relay loop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * already polls the sandbox filesystem on Daytona — see `engines/sandbox_agent.ts`). See the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Daytona guard in `buildSessionMcpServers`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDaytona: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+161
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Propagate
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toolSpecs: ResolvedToolSpec[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userMcpServers?: McpServerConfig[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relayDir: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,17 +188,24 @@ export interface SessionMcpServers { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * them into one switch; project gateway-tool-mcp): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. INTERNAL gateway-tool channel (`buildToolMcpServers`): the runner-synthesized loopback HTTP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * MCP server that delivers the run's resolved gateway/callback tools to the harness. Carries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * only public metadata; execution relays server-side. RESTORED. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * only public metadata; execution relays server-side. RESTORED — but advertised over a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * loopback (`127.0.0.1`) URL, so it is LOCAL-ONLY. On Daytona the harness runs IN the sandbox, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * where `127.0.0.1` is the sandbox's loopback, not the runner's, so the channel is SKIPPED and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the tools reach the harness through the file relay instead (the relay loop already works on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Daytona; gateway-tool-mcp open question 3). This honors the #4844 decision: HTTP advertisement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * for local, file relay for Daytona. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. USER MCP capability (`toAcpMcpServers`): the user's own declared servers — stdio DISABLED, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * http delivered (#4834). UNCHANGED. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * http delivered (#4834). UNCHANGED on every sandbox: a user http MCP is a REMOTE url the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * harness dials directly (not a runner loopback), so it stays reachable from a Daytona sandbox. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns a `close()` the caller MUST run when the session ends, to release the internal server's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * loopback port. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * loopback port (a no-op on Daytona, where no internal server starts). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function buildSessionMcpServers({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPi, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| capabilities, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| harness, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDaytona, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toolSpecs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userMcpServers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relayDir, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -135,9 +222,22 @@ export async function buildSessionMcpServers({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { servers: [], close: async () => {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Layer 1: INTERNAL gateway-tool channel (do not merge with the user gate below). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const internal = await buildToolMcpServers(toolSpecs, relayDir, log); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Layer 1: INTERNAL gateway-tool channel (do not merge with the user gate below). LOCAL ONLY: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // its advertised URL is a runner loopback (`127.0.0.1`), unreachable from a remote Daytona | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // sandbox where the harness runs. On Daytona, skip the loopback HTTP advertisement and let the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // file relay deliver the tools (the relay loop polls the sandbox filesystem; see the Daytona | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // tool relay in `engines/sandbox_agent.ts`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const internal = isDaytona | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { servers: [], close: async () => {} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : await buildToolMcpServers(toolSpecs, relayDir, log); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isDaytona && toolSpecs.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `daytona: ${toolSpecs.length} gateway tool(s) delivered via the file relay, not a ` + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `loopback MCP URL (unreachable from the sandbox)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Layer 2: USER MCP capability (stdio disabled, http delivered; do not merge with Layer 1). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A user http MCP is a remote url the harness dials directly, so it is delivered on Daytona too. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = toAcpMcpServers(userMcpServers, log); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+225
to
241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Validate user MCP URLs before opening the internal MCP port. On non-Daytona runs, Suggested reordering- const internal = isDaytona
- ? { servers: [], close: async () => {} }
- : await buildToolMcpServers(toolSpecs, relayDir, log);
+ const user = toAcpMcpServers(userMcpServers, log);
+ const internal = isDaytona
+ ? { servers: [], close: async () => {} }
+ : await buildToolMcpServers(toolSpecs, relayDir, log);
if (isDaytona && toolSpecs.length > 0) {
log(
`daytona: ${toolSpecs.length} gateway tool(s) delivered via the file relay, not a ` +
`loopback MCP URL (unreachable from the sandbox)`,
);
}
- const user = toAcpMcpServers(userMcpServers, log);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
Repository: Agenta-AI/agenta
Length of output: 300
🏁 Script executed:
Repository: Agenta-AI/agenta
Length of output: 14073
🏁 Script executed:
Repository: Agenta-AI/agenta
Length of output: 14073
Normalize trailing-dot hostnames before classification.
new URL(...).hostnamepreserves the trailing dot, solocalhost.andfoo.localhost.bypass the internal-host check, and dotted allowlist entries won’t match either. Strip a trailing.from both the parsed hostname and allowlist entries, then add regression coverage forlocalhost.andfoo.localhost..