From bb72c205be3699a6046153dd8f99a3fb6b0924d3 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 12:12:25 -0700
Subject: [PATCH 01/49] fix(mcp): reject unsafe Deep Agents projection types
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 9 +++
...idge-adapter-deepagents-projection.test.ts | 22 ++++++-
.../sandbox/mcp-bridge-adapter-status.ts | 19 ++++++
.../actions/sandbox/mcp-bridge-adapters.ts | 1 +
.../mcp-bridge-status-resolution.test.ts | 58 +++++++++++++++++++
src/lib/actions/sandbox/mcp-bridge-status.ts | 19 ++++--
.../mcp-bridge-adapter-deepagents-fixture.ts | 13 +++--
7 files changed, 129 insertions(+), 12 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 4cc214d9d3c..ab0d6e96b4b 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -37,6 +37,15 @@ An existing valid provider can remain ready when a host variable is unset becaus
For a trusted private server, status resolves the endpoint without changing managed state. Text output reports `private address pins: match`, `drift`, or `unresolved`. JSON output reports the same value in `trustedPrivateTarget.state` and includes the recorded pins. Status never adds a new address to the policy.
+
+
+Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code `mcp status` checks the path type without following a symbolic link or opening a FIFO.
+If the path is a symbolic link or FIFO, the command prints `Unsafe managed Deep Agents MCP projection path: symbolic link` or `Unsafe managed Deep Agents MCP projection path: FIFO`, writes no status output, and exits with status `2`.
+This exit status applies only to the unsafe path-type diagnostic.
+Other adapter inspection failures retain their ordinary status behavior.
+
+
+
## Discover Advertised Tools
Pass `--tools` with one server name to request the server's advertised tool names:
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index da848e4ff0f..b59687ea82b 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -74,7 +74,7 @@ describe("Deep Agents managed MCP projection safety", () => {
);
});
- it("fails status inspection closed without following hostile projection paths", () => {
+ it("names unsafe projection types without following hostile paths (#10754)", () => {
const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
expect(statusCommand).toContain("os.O_NONBLOCK | os.O_NOFOLLOW");
expect(statusCommand).not.toContain("config_path.read_text");
@@ -88,7 +88,9 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(symlink.status).toBe(2);
expect(symlink.stdout.trim()).toBe("");
- expect(symlink.stderr).toContain("Could not inspect managed Deep Agents MCP state");
+ expect(symlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
@@ -96,7 +98,21 @@ describe("Deep Agents managed MCP projection safety", () => {
});
expect(fifo.status).toBe(2);
expect(fifo.stdout.trim()).toBe("");
- expect(fifo.stderr).toContain("Could not inspect managed Deep Agents MCP state");
+ expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+
+ const danglingSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { danglingSymlink: true },
+ );
+ expect(danglingSymlink.status).toBe(2);
+ expect(danglingSymlink.stdout.trim()).toBe("");
+ expect(danglingSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
});
it.each([
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index 03518cab344..42eba383d8a 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -16,6 +16,8 @@ import {
// `.mcp.json` discovery is disabled in the managed image so user-authored MCP
// state can never be layered over the validated registry projection.
export const DEEPAGENTS_MCP_CONFIG_PATH = "/sandbox/.deepagents/.nemoclaw-mcp.json";
+export const DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX =
+ "Unsafe managed Deep Agents MCP projection path";
export const DEFAULT_OPENCLAW_CONFIG_DIR = "/sandbox/.openclaw";
export const HERMES_MCP_TRANSACTION_HELPER =
"/usr/local/lib/nemoclaw/hermes-mcp-config-transaction.py";
@@ -258,6 +260,23 @@ export function buildDeepAgentsMcpStatusCommand(
" raise SystemExit(2)",
"is_v2 = runtime_kind == 'v2'",
"config_path = managed_path if is_v2 else legacy_path",
+ "if is_v2:",
+ " try:",
+ " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
+ " except FileNotFoundError:",
+ " projection_mode = None",
+ " if projection_mode is not None:",
+ " if stat.S_ISLNK(projection_mode):",
+ " projection_kind = 'symbolic link'",
+ " elif stat.S_ISFIFO(projection_mode):",
+ " projection_kind = 'FIFO'",
+ " elif not stat.S_ISREG(projection_mode):",
+ " projection_kind = 'non-regular file'",
+ " else:",
+ " projection_kind = None",
+ " if projection_kind:",
+ ` print(f'${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: {projection_kind}', file=sys.stderr)`,
+ " raise SystemExit(2)",
"try:",
" data = read_managed_projection(config_path)[0] if is_v2 else read_legacy_config(config_path)[0]",
"except FileNotFoundError:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapters.ts b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
index a4fec84fc61..afc9657ce6e 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapters.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
@@ -62,6 +62,7 @@ export {
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
DEEPAGENTS_MCP_CONFIG_PATH,
+ DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
mcporterHeadersMatchExpected,
openClawMcporterRoot,
} from "./mcp-bridge-adapter-status";
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 09c024b10b2..18e6dc28cb6 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -190,6 +190,64 @@ ${body}
}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
+ it("returns a type-specific failure for unsafe Deep Agents projections (#10754)", () => {
+ const home = createTempHome("nemoclaw-mcp-status-projection-");
+ const { stdout } = runHarness(
+ home,
+ String.raw`
+ const current = registry.getSandbox("alpha");
+ registry.updateSandbox("alpha", {
+ agent: "langchain-deepagents-code",
+ mcp: {
+ bridges: {
+ github: {
+ ...current.mcp.bridges.github,
+ agent: "langchain-deepagents-code",
+ adapter: "deepagents-config",
+ },
+ },
+ managedServerNames: ["github"],
+ },
+ });
+ const outcomes = [];
+ for (const detail of [
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
+ ]) {
+ processRecovery.executeSandboxCommand = () => ({
+ status: 2,
+ stdout: "",
+ stderr: detail + "\n",
+ });
+ logLines.length = 0;
+ errorLines.length = 0;
+ await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
+ outcomes.push({
+ detail,
+ exitCode: process.exitCode ?? 0,
+ stdout: logLines.join("\n"),
+ stderr: errorLines.join("\n"),
+ });
+ process.exitCode = 0;
+ }
+ process.stdout.write(JSON.stringify(outcomes));
+`,
+ );
+ const outcomes = JSON.parse(stdout) as Array<{
+ detail: string;
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+ }>;
+
+ expect(outcomes).toHaveLength(2);
+ outcomes.forEach((outcome) => {
+ expect(outcome.exitCode).toBe(2);
+ expect(outcome.stdout).toBe("");
+ expect(outcome.stderr).toContain(outcome.detail);
+ });
+ });
+
it("probes by default for a single named server and surfaces the wire failure (#6379)", () => {
const home = createTempHome("nemoclaw-mcp-resolution-single-");
const { stdout } = runHarness(
diff --git a/src/lib/actions/sandbox/mcp-bridge-status.ts b/src/lib/actions/sandbox/mcp-bridge-status.ts
index a4580968b40..a030014be30 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status.ts
@@ -8,6 +8,7 @@ import {
buildHermesMcpStatusCommand,
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
+ DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
openClawMcporterRoot,
} from "./mcp-bridge-adapters";
import { isAgentMcpAdapter, McpBridgeError, type McpBridgeStatus } from "./mcp-bridge-contracts";
@@ -126,13 +127,21 @@ function getAdapterRegistration(
return { registered: false, detail: output || "not found" };
}
const envValues = resolvePersistedCredentialEnvForRedaction(entry.env);
+ const detail = redactBridgeSecretsForDisplay(
+ result.stderr || result.stdout || "not found",
+ entry,
+ envValues,
+ );
+ const normalizedDetail = detail.trim();
+ if (
+ adapter === "deepagents-config" &&
+ normalizedDetail.startsWith(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX)
+ ) {
+ throw new McpBridgeError(normalizedDetail, 2);
+ }
return {
registered: false,
- detail: redactBridgeSecretsForDisplay(
- result.stderr || result.stdout || "not found",
- entry,
- envValues,
- ),
+ detail,
};
}
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 6d869d0a57b..9a1d799c57c 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -35,6 +35,7 @@ export interface DeepAgentsConfigCommandResult {
}
export interface DeepAgentsManagedFixtureOptions {
+ danglingSymlink?: boolean;
fifo?: boolean;
mode?: number;
symlink?: boolean;
@@ -65,16 +66,20 @@ export function runDeepAgentsConfigCommand(
{ mode },
);
};
- const managedInitialPath = managedOptions.symlink ? managedSymlinkTarget : configPath;
+ const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
+ const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
if (managedOptions.fifo) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
const fifo = spawnSync("mkfifo", [configPath], { encoding: "utf-8", timeout: 5000 });
if (fifo.status !== 0) throw new Error(fifo.stderr || "could not create managed fixture FIFO");
fs.chmodSync(configPath, managedOptions.mode ?? 0o600);
} else {
- initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
- if (initialConfig !== undefined) fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
- if (managedOptions.symlink && initialConfig !== undefined) {
+ if (!managedOptions.danglingSymlink) {
+ initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
+ if (initialConfig !== undefined)
+ fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
+ }
+ if (managedSymlink) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.symlinkSync(managedSymlinkTarget, configPath);
}
From bd8b2a178f5a7229a5a2f4fd55e445e89fdae188 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 12:28:32 -0700
Subject: [PATCH 02/49] fix(mcp): preserve Deep Agents status failures
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 3 +-
...idge-adapter-deepagents-projection.test.ts | 28 +++++++++++++++
.../sandbox/mcp-bridge-adapter-status.ts | 34 +++++++++----------
.../mcp-bridge-adapter-deepagents-fixture.ts | 11 ++++--
4 files changed, 56 insertions(+), 20 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index ab0d6e96b4b..fa5c3f7dd1c 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -40,7 +40,8 @@ For a trusted private server, status resolves the endpoint without changing mana
Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code `mcp status` checks the path type without following a symbolic link or opening a FIFO.
-If the path is a symbolic link or FIFO, the command prints `Unsafe managed Deep Agents MCP projection path: symbolic link` or `Unsafe managed Deep Agents MCP projection path: FIFO`, writes no status output, and exits with status `2`.
+If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
+The diagnostics are `Unsafe managed Deep Agents MCP projection path: symbolic link`, `Unsafe managed Deep Agents MCP projection path: FIFO`, and `Unsafe managed Deep Agents MCP projection path: non-regular file`.
This exit status applies only to the unsafe path-type diagnostic.
Other adapter inspection failures retain their ordinary status behavior.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index b59687ea82b..cd293e76d44 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -100,6 +100,20 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(fifo.stdout.trim()).toBe("");
expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ const directory = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { directory: true },
+ );
+ expect(directory.status).toBe(2);
+ expect(directory.stdout.trim()).toBe("");
+ expect(directory.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
+
const danglingSymlink = runDeepAgentsConfigCommand(
statusCommand,
undefined,
@@ -115,6 +129,20 @@ describe("Deep Agents managed MCP projection safety", () => {
);
});
+ it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry).replace(
+ " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
+ " raise PermissionError('fixture denied')",
+ );
+ const result = runDeepAgentsConfigCommand(statusCommand, emptyProjection);
+
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Could not inspect managed Deep Agents MCP state");
+ expect(result.stderr).toContain("fixture denied");
+ expect(result.stderr).not.toContain("Traceback");
+ });
+
it.each([
["registration", registrationCommand],
["v2 rollback", rollbackCommand],
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index 42eba383d8a..510272737ed 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -260,24 +260,24 @@ export function buildDeepAgentsMcpStatusCommand(
" raise SystemExit(2)",
"is_v2 = runtime_kind == 'v2'",
"config_path = managed_path if is_v2 else legacy_path",
- "if is_v2:",
- " try:",
- " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
- " except FileNotFoundError:",
- " projection_mode = None",
- " if projection_mode is not None:",
- " if stat.S_ISLNK(projection_mode):",
- " projection_kind = 'symbolic link'",
- " elif stat.S_ISFIFO(projection_mode):",
- " projection_kind = 'FIFO'",
- " elif not stat.S_ISREG(projection_mode):",
- " projection_kind = 'non-regular file'",
- " else:",
- " projection_kind = None",
- " if projection_kind:",
- ` print(f'${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: {projection_kind}', file=sys.stderr)`,
- " raise SystemExit(2)",
"try:",
+ " if is_v2:",
+ " try:",
+ " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
+ " except FileNotFoundError:",
+ " projection_mode = None",
+ " if projection_mode is not None:",
+ " if stat.S_ISLNK(projection_mode):",
+ " projection_kind = 'symbolic link'",
+ " elif stat.S_ISFIFO(projection_mode):",
+ " projection_kind = 'FIFO'",
+ " elif not stat.S_ISREG(projection_mode):",
+ " projection_kind = 'non-regular file'",
+ " else:",
+ " projection_kind = None",
+ " if projection_kind:",
+ ` print(f'${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: {projection_kind}', file=sys.stderr)`,
+ " raise SystemExit(2)",
" data = read_managed_projection(config_path)[0] if is_v2 else read_legacy_config(config_path)[0]",
"except FileNotFoundError:",
" data = {}",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 9a1d799c57c..d2a7836f720 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -36,6 +36,7 @@ export interface DeepAgentsConfigCommandResult {
export interface DeepAgentsManagedFixtureOptions {
danglingSymlink?: boolean;
+ directory?: boolean;
fifo?: boolean;
mode?: number;
symlink?: boolean;
@@ -68,7 +69,9 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.fifo) {
+ if (managedOptions.directory) {
+ fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
+ } else if (managedOptions.fifo) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
const fifo = spawnSync("mkfifo", [configPath], { encoding: "utf-8", timeout: 5000 });
if (fifo.status !== 0) throw new Error(fifo.stderr || "could not create managed fixture FIFO");
@@ -99,7 +102,11 @@ export function runDeepAgentsConfigCommand(
const configExists = fs.existsSync(configPath);
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configExists && fs.lstatSync(configPath).isFIFO();
- const configText = configExists && !configIsFifo ? fs.readFileSync(configPath, "utf-8") : null;
+ const configIsDirectory = configExists && fs.lstatSync(configPath).isDirectory();
+ const configText =
+ configExists && !configIsFifo && !configIsDirectory
+ ? fs.readFileSync(configPath, "utf-8")
+ : null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
const managedSymlinkTargetText = managedSymlinkTargetExists
? fs.readFileSync(managedSymlinkTarget, "utf-8")
From c18198013540b1e3e97fb13286b35b9dffb3176f Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 13:12:40 -0700
Subject: [PATCH 03/49] fix(mcp): classify projection open races
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
...idge-adapter-deepagents-projection.test.ts | 45 ++++++++++++++++---
...cp-bridge-adapter-deepagents-projection.ts | 23 ++++++++--
.../sandbox/mcp-bridge-adapter-status.ts | 19 ++++----
.../mcp-bridge-adapter-deepagents-fixture.ts | 45 +++++++++++++++++--
4 files changed, 108 insertions(+), 24 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index cd293e76d44..da5e341fb35 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -76,8 +76,6 @@ describe("Deep Agents managed MCP projection safety", () => {
it("names unsafe projection types without following hostile paths (#10754)", () => {
const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- expect(statusCommand).toContain("os.O_NONBLOCK | os.O_NOFOLLOW");
- expect(statusCommand).not.toContain("config_path.read_text");
const symlink = runDeepAgentsConfigCommand(
statusCommand,
emptyProjection,
@@ -127,20 +125,53 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(danglingSymlink.stderr).toContain(
"Unsafe managed Deep Agents MCP projection path: symbolic link",
);
+
+ const racedSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "symlink" },
+ );
+ expect(racedSymlink.status).toBe(2);
+ expect(racedSymlink.stdout.trim()).toBe("");
+ expect(racedSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(racedSymlink.configIsSymlink).toBe(true);
+ expect(racedSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
+
+ const racedFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "fifo" },
+ );
+ expect(racedFifo.status).toBe(2);
+ expect(racedFifo.stdout.trim()).toBe("");
+ expect(racedFifo.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
+ );
+ expect(racedFifo.configIsFifo).toBe(true);
});
it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry).replace(
- " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
- " raise PermissionError('fixture denied')",
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ '{"mcpServers":',
);
- const result = runDeepAgentsConfigCommand(statusCommand, emptyProjection);
expect(result.status).toBe(2);
expect(result.stdout.trim()).toBe("");
expect(result.stderr).toContain("Could not inspect managed Deep Agents MCP state");
- expect(result.stderr).toContain("fixture denied");
+ expect(result.stderr).toContain("Expecting value");
expect(result.stderr).not.toContain("Traceback");
+ expect(result.configText).toBe('{"mcpServers":');
});
it.each([
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index 357bfcfbbc3..e22c242b9e9 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -18,7 +18,16 @@ export const DEEPAGENTS_STRICT_JSON_HELPERS = [
];
export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
+ "import errno",
"MANAGED_MCP_MAX_BYTES = 262144",
+ "def managed_projection_path_kind(mode):",
+ " if stat.S_ISLNK(mode):",
+ " return 'symbolic link'",
+ " if stat.S_ISFIFO(mode):",
+ " return 'FIFO'",
+ " if not stat.S_ISREG(mode):",
+ " return 'non-regular file'",
+ " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
"def managed_path_identity(path):",
@@ -41,7 +50,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not safe:",
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
- "def open_managed_projection(path, writable=False):",
+ "def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
" access = os.O_RDWR if writable else os.O_RDONLY",
" flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
@@ -49,8 +58,16 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" except FileNotFoundError:",
" assert_managed_source_stable(path, None)",
" return b'', None, None",
+ " except OSError as exc:",
+ " if unsafe_path_prefix and exc.errno == errno.ELOOP:",
+ " raise ValueError(f'{unsafe_path_prefix}: symbolic link') from exc",
+ " raise",
" try:",
" before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" validate_managed_descriptor_path(path, descriptor)",
" if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid size')",
@@ -87,8 +104,8 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" except Exception:",
" close_managed_projection_descriptor(descriptor)",
" raise",
- "def read_managed_projection(path):",
- " raw, identity, descriptor = open_managed_projection(path)",
+ "def read_managed_projection(path, unsafe_path_prefix=None):",
+ " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index 510272737ed..d82f323f7aa 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -267,21 +267,20 @@ export function buildDeepAgentsMcpStatusCommand(
" except FileNotFoundError:",
" projection_mode = None",
" if projection_mode is not None:",
- " if stat.S_ISLNK(projection_mode):",
- " projection_kind = 'symbolic link'",
- " elif stat.S_ISFIFO(projection_mode):",
- " projection_kind = 'FIFO'",
- " elif not stat.S_ISREG(projection_mode):",
- " projection_kind = 'non-regular file'",
- " else:",
- " projection_kind = None",
+ " projection_kind = managed_projection_path_kind(projection_mode)",
" if projection_kind:",
` print(f'${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: {projection_kind}', file=sys.stderr)`,
" raise SystemExit(2)",
- " data = read_managed_projection(config_path)[0] if is_v2 else read_legacy_config(config_path)[0]",
+ ` data = read_managed_projection(config_path, ${JSON.stringify(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX)})[0] if is_v2 else read_legacy_config(config_path)[0]`,
"except FileNotFoundError:",
" data = {}",
- "except (OSError, UnicodeDecodeError, ValueError) as exc:",
+ "except ValueError as exc:",
+ ` if str(exc).startswith('${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: '):`,
+ " print(str(exc), file=sys.stderr)",
+ " raise SystemExit(2)",
+ " print(f'Could not inspect managed Deep Agents MCP state at {config_path}: {exc}', file=sys.stderr)",
+ " raise SystemExit(2)",
+ "except (OSError, UnicodeDecodeError) as exc:",
" print(f'Could not inspect managed Deep Agents MCP state at {config_path}: {exc}', file=sys.stderr)",
" raise SystemExit(2)",
"servers = data.get('mcpServers') if isinstance(data, dict) else None",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index d2a7836f720..d0ecf4b6818 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -25,6 +25,8 @@ export interface DeepAgentsConfigCommandResult {
stdout: string;
stderr: string;
configExists: boolean;
+ configIsFifo: boolean;
+ configIsSymlink: boolean;
config: Record | null;
configText: string | null;
legacyConfigExists: boolean;
@@ -39,6 +41,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ swapOnManagedOpen?: "fifo" | "symlink";
symlink?: boolean;
}
@@ -87,10 +90,35 @@ export function runDeepAgentsConfigCommand(
fs.symlinkSync(managedSymlinkTarget, configPath);
}
}
+ if (managedOptions.swapOnManagedOpen === "symlink") {
+ initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
+ }
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
try {
+ const swapOnManagedOpenPrelude = managedOptions.swapOnManagedOpen
+ ? [
+ "import os as _nemoclaw_test_os",
+ `_nemoclaw_test_path = ${JSON.stringify(configPath)}`,
+ `_nemoclaw_test_target = ${JSON.stringify(managedSymlinkTarget)}`,
+ `_nemoclaw_test_swap = ${JSON.stringify(managedOptions.swapOnManagedOpen)}`,
+ "_nemoclaw_test_real_open = _nemoclaw_test_os.open",
+ "_nemoclaw_test_swapped = False",
+ "def _nemoclaw_test_open(path, flags, *args, **kwargs):",
+ " global _nemoclaw_test_swapped",
+ " if not _nemoclaw_test_swapped and _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path:",
+ " _nemoclaw_test_swapped = True",
+ " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
+ " if _nemoclaw_test_swap == 'symlink':",
+ " _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
+ " else:",
+ " _nemoclaw_test_os.mkfifo(_nemoclaw_test_path, 0o600)",
+ " return _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
+ "_nemoclaw_test_os.open = _nemoclaw_test_open",
+ ].join("\n")
+ : "";
const fixtureCommand = command
+ .replace("<<'PY'\n", `<<'PY'\n${swapOnManagedOpenPrelude}\n`)
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
.replaceAll("/opt/venv/bin/python3", "python3")
@@ -102,6 +130,7 @@ export function runDeepAgentsConfigCommand(
const configExists = fs.existsSync(configPath);
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configExists && fs.lstatSync(configPath).isFIFO();
+ const configIsSymlink = configExists && fs.lstatSync(configPath).isSymbolicLink();
const configIsDirectory = configExists && fs.lstatSync(configPath).isDirectory();
const configText =
configExists && !configIsFifo && !configIsDirectory
@@ -112,17 +141,25 @@ export function runDeepAgentsConfigCommand(
? fs.readFileSync(managedSymlinkTarget, "utf-8")
: null;
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
+ const parseConfigText = (text: string | null): Record | null => {
+ if (!text) return null;
+ try {
+ return JSON.parse(text) as Record;
+ } catch {
+ return null;
+ }
+ };
return {
status: result.status,
stdout: result.stdout,
stderr: result.stderr,
configExists,
- config: configText ? (JSON.parse(configText) as Record) : null,
+ configIsFifo,
+ configIsSymlink,
+ config: parseConfigText(configText),
configText,
legacyConfigExists,
- legacyConfig: legacyConfigText
- ? (JSON.parse(legacyConfigText) as Record)
- : null,
+ legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
From 00f5a639bf535304d74a44435b4e2f09f29021c8 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 13:20:52 -0700
Subject: [PATCH 04/49] fix(mcp): classify socket projection races
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 3 ++-
...idge-adapter-deepagents-projection.test.ts | 15 +++++++++++++++
...cp-bridge-adapter-deepagents-projection.ts | 11 +++++++++--
.../mcp-bridge-adapter-deepagents-fixture.ts | 19 ++++++++++++++-----
4 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index fa5c3f7dd1c..4fdb898ea95 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -39,7 +39,8 @@ For a trusted private server, status resolves the endpoint without changing mana
-Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code `mcp status` checks the path type without following a symbolic link or opening a FIFO.
+Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code `mcp status` checks the path type.
+It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
The diagnostics are `Unsafe managed Deep Agents MCP projection path: symbolic link`, `Unsafe managed Deep Agents MCP projection path: FIFO`, and `Unsafe managed Deep Agents MCP projection path: non-regular file`.
This exit status applies only to the unsafe path-type diagnostic.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index da5e341fb35..2f6114e32b3 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -158,6 +158,21 @@ describe("Deep Agents managed MCP projection safety", () => {
"Unsafe managed Deep Agents MCP projection path: FIFO",
);
expect(racedFifo.configIsFifo).toBe(true);
+
+ const racedSocket = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "socket" },
+ );
+ expect(racedSocket.status).toBe(2);
+ expect(racedSocket.stdout.trim()).toBe("");
+ expect(racedSocket.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
+ expect(racedSocket.configIsSocket).toBe(true);
});
it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index e22c242b9e9..672d74c7c4a 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -59,8 +59,15 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" assert_managed_source_stable(path, None)",
" return b'', None, None",
" except OSError as exc:",
- " if unsafe_path_prefix and exc.errno == errno.ELOOP:",
- " raise ValueError(f'{unsafe_path_prefix}: symbolic link') from exc",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
" raise",
" try:",
" before = os.fstat(descriptor)",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index d0ecf4b6818..b64c8f77556 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -26,6 +26,7 @@ export interface DeepAgentsConfigCommandResult {
stderr: string;
configExists: boolean;
configIsFifo: boolean;
+ configIsSocket: boolean;
configIsSymlink: boolean;
config: Record | null;
configText: string | null;
@@ -41,7 +42,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
- swapOnManagedOpen?: "fifo" | "symlink";
+ swapOnManagedOpen?: "fifo" | "socket" | "symlink";
symlink?: boolean;
}
@@ -53,7 +54,8 @@ export function runDeepAgentsConfigCommand(
initialLegacyMode = 0o600,
managedOptions: DeepAgentsManagedFixtureOptions = {},
): DeepAgentsConfigCommandResult {
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-deepagents-mcp-"));
+ const fixtureRoot = managedOptions.swapOnManagedOpen === "socket" ? "/tmp" : os.tmpdir();
+ const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
@@ -99,20 +101,25 @@ export function runDeepAgentsConfigCommand(
const swapOnManagedOpenPrelude = managedOptions.swapOnManagedOpen
? [
"import os as _nemoclaw_test_os",
+ "import socket as _nemoclaw_test_socket_module",
`_nemoclaw_test_path = ${JSON.stringify(configPath)}`,
`_nemoclaw_test_target = ${JSON.stringify(managedSymlinkTarget)}`,
`_nemoclaw_test_swap = ${JSON.stringify(managedOptions.swapOnManagedOpen)}`,
"_nemoclaw_test_real_open = _nemoclaw_test_os.open",
+ "_nemoclaw_test_socket = None",
"_nemoclaw_test_swapped = False",
"def _nemoclaw_test_open(path, flags, *args, **kwargs):",
- " global _nemoclaw_test_swapped",
+ " global _nemoclaw_test_socket, _nemoclaw_test_swapped",
" if not _nemoclaw_test_swapped and _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path:",
" _nemoclaw_test_swapped = True",
" _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
" if _nemoclaw_test_swap == 'symlink':",
" _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
- " else:",
+ " elif _nemoclaw_test_swap == 'fifo':",
" _nemoclaw_test_os.mkfifo(_nemoclaw_test_path, 0o600)",
+ " else:",
+ " _nemoclaw_test_socket = _nemoclaw_test_socket_module.socket(_nemoclaw_test_socket_module.AF_UNIX, _nemoclaw_test_socket_module.SOCK_STREAM)",
+ " _nemoclaw_test_socket.bind(_nemoclaw_test_path)",
" return _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
"_nemoclaw_test_os.open = _nemoclaw_test_open",
].join("\n")
@@ -130,10 +137,11 @@ export function runDeepAgentsConfigCommand(
const configExists = fs.existsSync(configPath);
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configExists && fs.lstatSync(configPath).isFIFO();
+ const configIsSocket = configExists && fs.lstatSync(configPath).isSocket();
const configIsSymlink = configExists && fs.lstatSync(configPath).isSymbolicLink();
const configIsDirectory = configExists && fs.lstatSync(configPath).isDirectory();
const configText =
- configExists && !configIsFifo && !configIsDirectory
+ configExists && !configIsFifo && !configIsSocket && !configIsDirectory
? fs.readFileSync(configPath, "utf-8")
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
@@ -155,6 +163,7 @@ export function runDeepAgentsConfigCommand(
stderr: result.stderr,
configExists,
configIsFifo,
+ configIsSocket,
configIsSymlink,
config: parseConfigText(configText),
configText,
From f1f854f0894b43c1acf50a14737ee61138cb01af Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 13:24:54 -0700
Subject: [PATCH 05/49] test(mcp): inspect dangling projection links
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
.../mcp-bridge-adapter-deepagents-fixture.ts | 22 ++++++++++++++-----
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index b64c8f77556..cf9e9206d09 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -134,14 +134,24 @@ export function runDeepAgentsConfigCommand(
`runtime_kind = "${runtimeKind}" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
);
const result = spawnSync("bash", ["-c", fixtureCommand], { encoding: "utf-8", timeout: 5000 });
- const configExists = fs.existsSync(configPath);
+ let configStat: fs.Stats | null = null;
+ try {
+ configStat = fs.lstatSync(configPath);
+ } catch {
+ // A missing projection has no path type to report.
+ }
+ const configExists = configStat !== null;
const legacyConfigExists = fs.existsSync(legacyConfigPath);
- const configIsFifo = configExists && fs.lstatSync(configPath).isFIFO();
- const configIsSocket = configExists && fs.lstatSync(configPath).isSocket();
- const configIsSymlink = configExists && fs.lstatSync(configPath).isSymbolicLink();
- const configIsDirectory = configExists && fs.lstatSync(configPath).isDirectory();
+ const configIsFifo = configStat?.isFIFO() === true;
+ const configIsSocket = configStat?.isSocket() === true;
+ const configIsSymlink = configStat?.isSymbolicLink() === true;
+ const configIsDirectory = configStat?.isDirectory() === true;
const configText =
- configExists && !configIsFifo && !configIsSocket && !configIsDirectory
+ configExists &&
+ !configIsFifo &&
+ !configIsSocket &&
+ !configIsSymlink &&
+ !configIsDirectory
? fs.readFileSync(configPath, "utf-8")
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
From 7293012ec1db18508e513c9b8fc54a1c50239606 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 14:18:17 -0700
Subject: [PATCH 06/49] fix(mcp): classify post-open projection races
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
...idge-adapter-deepagents-projection.test.ts | 51 ++++++++++++++
...cp-bridge-adapter-deepagents-projection.ts | 12 +++-
.../mcp-bridge-adapter-deepagents-fixture.ts | 67 ++++++++++++++-----
3 files changed, 110 insertions(+), 20 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 2f6114e32b3..f338cfa76ef 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -173,6 +173,57 @@ describe("Deep Agents managed MCP projection safety", () => {
"Unsafe managed Deep Agents MCP projection path: non-regular file",
);
expect(racedSocket.configIsSocket).toBe(true);
+
+ const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "symlink" },
+ );
+ expect(replacedAfterOpenWithSymlink.status).toBe(2);
+ expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
+
+ const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "fifo" },
+ );
+ expect(replacedAfterOpenWithFifo.status).toBe(2);
+ expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithFifo.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
+ );
+ expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
+
+ const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedRead: "symlink" },
+ );
+ expect(replacedWhileReadingWithSymlink.status).toBe(2);
+ expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
+ expect(replacedWhileReadingWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
});
it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index 672d74c7c4a..52f0bde24ea 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -43,9 +43,13 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(path, descriptor):",
+ "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
" linked = os.stat(path, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" safe = (stat.S_ISREG(opened.st_mode) and opened.st_uid == os.getuid() and stat.S_IMODE(opened.st_mode) == 0o600 and opened.st_nlink == 1 and (opened.st_dev, opened.st_ino) == (linked.st_dev, linked.st_ino))",
" if not safe:",
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
@@ -75,7 +79,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
" if unsafe_path_kind:",
" raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(path, descriptor)",
+ " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
" if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid size')",
" chunks = []",
@@ -88,6 +92,10 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" remaining -= len(chunk)",
" after = os.fstat(descriptor)",
" linked_after = os.stat(path, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
" if remaining or not stable:",
" raise ValueError('managed MCP projection changed while reading')",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index cf9e9206d09..12068ce7732 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -42,7 +42,9 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ swapAfterManagedOpen?: "fifo" | "symlink";
swapOnManagedOpen?: "fifo" | "socket" | "symlink";
+ swapOnManagedRead?: "fifo" | "symlink";
symlink?: boolean;
}
@@ -54,7 +56,14 @@ export function runDeepAgentsConfigCommand(
initialLegacyMode = 0o600,
managedOptions: DeepAgentsManagedFixtureOptions = {},
): DeepAgentsConfigCommandResult {
- const fixtureRoot = managedOptions.swapOnManagedOpen === "socket" ? "/tmp" : os.tmpdir();
+ const managedSwap = managedOptions.swapOnManagedOpen
+ ? { kind: managedOptions.swapOnManagedOpen, phase: "before-open" }
+ : managedOptions.swapAfterManagedOpen
+ ? { kind: managedOptions.swapAfterManagedOpen, phase: "after-open" }
+ : managedOptions.swapOnManagedRead
+ ? { kind: managedOptions.swapOnManagedRead, phase: "read" }
+ : undefined;
+ const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
@@ -92,40 +101,59 @@ export function runDeepAgentsConfigCommand(
fs.symlinkSync(managedSymlinkTarget, configPath);
}
}
- if (managedOptions.swapOnManagedOpen === "symlink") {
+ if (managedSwap?.kind === "symlink") {
initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
}
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
try {
- const swapOnManagedOpenPrelude = managedOptions.swapOnManagedOpen
+ const managedSwapPrelude = managedSwap
? [
"import os as _nemoclaw_test_os",
"import socket as _nemoclaw_test_socket_module",
`_nemoclaw_test_path = ${JSON.stringify(configPath)}`,
`_nemoclaw_test_target = ${JSON.stringify(managedSymlinkTarget)}`,
- `_nemoclaw_test_swap = ${JSON.stringify(managedOptions.swapOnManagedOpen)}`,
+ `_nemoclaw_test_swap = ${JSON.stringify(managedSwap.kind)}`,
+ `_nemoclaw_test_phase = ${JSON.stringify(managedSwap.phase)}`,
"_nemoclaw_test_real_open = _nemoclaw_test_os.open",
+ "_nemoclaw_test_real_read = _nemoclaw_test_os.read",
+ "_nemoclaw_test_descriptor = None",
"_nemoclaw_test_socket = None",
"_nemoclaw_test_swapped = False",
- "def _nemoclaw_test_open(path, flags, *args, **kwargs):",
+ "def _nemoclaw_test_replace_path():",
" global _nemoclaw_test_socket, _nemoclaw_test_swapped",
- " if not _nemoclaw_test_swapped and _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path:",
- " _nemoclaw_test_swapped = True",
- " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
- " if _nemoclaw_test_swap == 'symlink':",
- " _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
- " elif _nemoclaw_test_swap == 'fifo':",
- " _nemoclaw_test_os.mkfifo(_nemoclaw_test_path, 0o600)",
- " else:",
- " _nemoclaw_test_socket = _nemoclaw_test_socket_module.socket(_nemoclaw_test_socket_module.AF_UNIX, _nemoclaw_test_socket_module.SOCK_STREAM)",
- " _nemoclaw_test_socket.bind(_nemoclaw_test_path)",
- " return _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
+ " if _nemoclaw_test_swapped:",
+ " return",
+ " _nemoclaw_test_swapped = True",
+ " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
+ " if _nemoclaw_test_swap == 'symlink':",
+ " _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
+ " elif _nemoclaw_test_swap == 'fifo':",
+ " _nemoclaw_test_os.mkfifo(_nemoclaw_test_path, 0o600)",
+ " else:",
+ " _nemoclaw_test_socket = _nemoclaw_test_socket_module.socket(_nemoclaw_test_socket_module.AF_UNIX, _nemoclaw_test_socket_module.SOCK_STREAM)",
+ " _nemoclaw_test_socket.bind(_nemoclaw_test_path)",
+ "def _nemoclaw_test_open(path, flags, *args, **kwargs):",
+ " global _nemoclaw_test_descriptor",
+ " managed = _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path",
+ " if managed and _nemoclaw_test_phase == 'before-open':",
+ " _nemoclaw_test_replace_path()",
+ " descriptor = _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
+ " if managed:",
+ " _nemoclaw_test_descriptor = descriptor",
+ " if _nemoclaw_test_phase == 'after-open':",
+ " _nemoclaw_test_replace_path()",
+ " return descriptor",
+ "def _nemoclaw_test_read(descriptor, size):",
+ " if descriptor == _nemoclaw_test_descriptor and _nemoclaw_test_phase == 'read':",
+ " _nemoclaw_test_replace_path()",
+ " return _nemoclaw_test_real_read(descriptor, size)",
"_nemoclaw_test_os.open = _nemoclaw_test_open",
+ "_nemoclaw_test_os.read = _nemoclaw_test_read",
].join("\n")
: "";
const fixtureCommand = command
- .replace("<<'PY'\n", `<<'PY'\n${swapOnManagedOpenPrelude}\n`)
+ .replace("<<'PY'\n", `<<'PY'\n${managedSwapPrelude}\n`)
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
.replaceAll("/opt/venv/bin/python3", "python3")
@@ -162,7 +190,10 @@ export function runDeepAgentsConfigCommand(
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
try {
- return JSON.parse(text) as Record;
+ const parsed: unknown = JSON.parse(text);
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : null;
} catch {
return null;
}
From 0bdb3d47baaefed86abb1ba2fb2c8e6edb8e1150 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 15:19:55 -0700
Subject: [PATCH 07/49] refactor(mcp): centralize projection type validation
Signed-off-by: Prekshi Vyas
---
src/lib/actions/sandbox/mcp-bridge-adapter-status.ts | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index d82f323f7aa..e46d13bbcde 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -261,16 +261,6 @@ export function buildDeepAgentsMcpStatusCommand(
"is_v2 = runtime_kind == 'v2'",
"config_path = managed_path if is_v2 else legacy_path",
"try:",
- " if is_v2:",
- " try:",
- " projection_mode = os.stat(config_path, follow_symlinks=False).st_mode",
- " except FileNotFoundError:",
- " projection_mode = None",
- " if projection_mode is not None:",
- " projection_kind = managed_projection_path_kind(projection_mode)",
- " if projection_kind:",
- ` print(f'${DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX}: {projection_kind}', file=sys.stderr)`,
- " raise SystemExit(2)",
` data = read_managed_projection(config_path, ${JSON.stringify(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX)})[0] if is_v2 else read_legacy_config(config_path)[0]`,
"except FileNotFoundError:",
" data = {}",
From 391946b1214b21912eea32a4a6bd6f1ff4c16aff Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 18:10:55 -0700
Subject: [PATCH 08/49] test(mcp): exercise projection races at runtime
Signed-off-by: Prekshi Vyas
---
...idge-adapter-deepagents-projection.test.ts | 103 +++++------
.../mcp-bridge-adapter-deepagents-fixture.ts | 163 ++++++++++++------
2 files changed, 149 insertions(+), 117 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index f338cfa76ef..25d01223017 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -17,41 +17,13 @@ import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const emptyProjection = { mcpServers: {} };
const duplicateProjection = '{"mcpServers":{},"mcpServers":{"shadow":{}}}\n';
const attackerProjection = '{"mcpServers":{"attacker":{"type":"stdio"}}}\n';
+const managedCommandTimeoutMs = 30_000;
const registrationCommand = buildDeepAgentsMcpRegisterCommand(baseEntry);
const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true);
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it.each([
- { mutation: "registration", command: registrationCommand },
- { mutation: "rollback", command: rollbackCommand },
- { mutation: "removal", command: removalCommand },
- ])("uses one stable-read contract for $mutation", ({ command }) => {
- expect(command).toContain("os.O_NOFOLLOW");
- expect(command).toContain("os.fstat(descriptor)");
- expect(command).toContain("assert_managed_source_stable(path, identity)");
- expect(command).toContain("os.link(tmp_name, path, follow_symlinks=False)");
- expect(command).toContain("os.ftruncate(descriptor, 0)");
- expect(command).not.toContain("\n path.unlink()\n");
- expect(command).not.toContain("config_path.read_text");
- });
-
- it("uses the isolated runtime and bounds registration before mutation", () => {
- expect(registrationCommand).toMatch(/^\/opt\/venv\/bin\/python3 -I - <<'PY'/);
- expect(buildDeepAgentsMcpStatusCommand(baseEntry)).toMatch(
- /^\/opt\/venv\/bin\/python3 -I - <<'PY'/,
- );
- expect(rollbackCommand).toContain(
- `len(payload['expectedServers']) > ${String(DEEPAGENTS_MCP_MAX_SERVERS)}`,
- );
- const sizeCheckIndex = registrationCommand.indexOf("len(payload) > MANAGED_MCP_MAX_BYTES");
- const truncateIndex = registrationCommand.indexOf("os.ftruncate(descriptor, 0)");
- expect(sizeCheckIndex).toBeGreaterThanOrEqual(0);
- expect(truncateIndex).toBeGreaterThanOrEqual(0);
- expect(sizeCheckIndex).toBeLessThan(truncateIndex);
- });
-
it("applies the shared server cap before normal and rollback v2 publication", () => {
const entries = Array.from(
{ length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
@@ -82,7 +54,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { symlink: true },
+ { symlink: true, timeoutMs: managedCommandTimeoutMs },
);
expect(symlink.status).toBe(2);
expect(symlink.stdout.trim()).toBe("");
@@ -93,19 +65,16 @@ describe("Deep Agents managed MCP projection safety", () => {
const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
fifo: true,
+ timeoutMs: managedCommandTimeoutMs,
});
expect(fifo.status).toBe(2);
expect(fifo.stdout.trim()).toBe("");
expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- const directory = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { directory: true },
- );
+ const directory = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ directory: true,
+ timeoutMs: managedCommandTimeoutMs,
+ });
expect(directory.status).toBe(2);
expect(directory.stdout.trim()).toBe("");
expect(directory.stderr).toContain(
@@ -118,7 +87,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { danglingSymlink: true },
+ { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
);
expect(danglingSymlink.status).toBe(2);
expect(danglingSymlink.stdout.trim()).toBe("");
@@ -132,7 +101,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapOnManagedOpen: "symlink" },
+ { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
);
expect(racedSymlink.status).toBe(2);
expect(racedSymlink.stdout.trim()).toBe("");
@@ -150,13 +119,11 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapOnManagedOpen: "fifo" },
+ { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
);
expect(racedFifo.status).toBe(2);
expect(racedFifo.stdout.trim()).toBe("");
- expect(racedFifo.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: FIFO",
- );
+ expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
expect(racedFifo.configIsFifo).toBe(true);
const racedSocket = runDeepAgentsConfigCommand(
@@ -165,7 +132,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapOnManagedOpen: "socket" },
+ { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
);
expect(racedSocket.status).toBe(2);
expect(racedSocket.stdout.trim()).toBe("");
@@ -180,7 +147,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapAfterManagedOpen: "symlink" },
+ { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
);
expect(replacedAfterOpenWithSymlink.status).toBe(2);
expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
@@ -198,7 +165,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapAfterManagedOpen: "fifo" },
+ { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
);
expect(replacedAfterOpenWithFifo.status).toBe(2);
expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
@@ -213,7 +180,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { swapOnManagedRead: "symlink" },
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
);
expect(replacedWhileReadingWithSymlink.status).toBe(2);
expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
@@ -269,37 +236,49 @@ describe("Deep Agents managed MCP projection safety", () => {
});
it("never clobbers a projection that appears during absent publication or fd rewrite", () => {
- const absentRace = registrationCommand.replace(
- " write_managed_projection(config_path, data, source_identity, source_descriptor)",
- ` config_path.write_text(${JSON.stringify(attackerProjection)}, encoding='utf-8')\n os.chmod(config_path, 0o600)\n write_managed_projection(config_path, data, source_identity, source_descriptor)`,
+ const absentResult = runDeepAgentsConfigCommand(
+ registrationCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { swapBeforeManagedLink: attackerProjection, timeoutMs: managedCommandTimeoutMs },
);
- const absentResult = runDeepAgentsConfigCommand(absentRace);
expect(absentResult.status).toBe(2);
- expect(absentResult.stderr).toContain("appeared during mutation");
+ expect(absentResult.stderr).toContain("appeared during publication");
expect(absentResult.configText).toBe(attackerProjection);
- const existingRace = registrationCommand.replace(
- " payload = managed_projection_bytes(value)\n os.lseek(descriptor, 0, os.SEEK_SET)",
- ` payload = managed_projection_bytes(value)\n path.unlink()\n path.write_text(${JSON.stringify(attackerProjection)}, encoding='utf-8')\n os.chmod(path, 0o600)\n os.lseek(descriptor, 0, os.SEEK_SET)`,
+ const existingResult = runDeepAgentsConfigCommand(
+ registrationCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
);
- const existingResult = runDeepAgentsConfigCommand(existingRace, emptyProjection);
expect(existingResult.status).toBe(2);
expect(existingResult.stderr).toContain("links, or path identity");
expect(existingResult.configText).toBe(attackerProjection);
});
- it("keeps forced removal identity-bound for malformed files and symlinks", () => {
+ it("keeps forced removal identity-bound during a descriptor rewrite (#10754)", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
- const racedCommand = forcedCommand.replace(
- " payload = managed_projection_bytes(value)\n os.lseek(descriptor, 0, os.SEEK_SET)",
- ` payload = managed_projection_bytes(value)\n path.unlink()\n path.write_text(${JSON.stringify(attackerProjection)}, encoding='utf-8')\n os.chmod(path, 0o600)\n os.lseek(descriptor, 0, os.SEEK_SET)`,
+ const raced = runDeepAgentsConfigCommand(
+ forcedCommand,
+ { ui: { theme: "dark" } },
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
);
- const raced = runDeepAgentsConfigCommand(racedCommand, { ui: { theme: "dark" } });
expect(raced.status).toBe(2);
expect(raced.stderr).toContain("Refusing unsafe managed MCP v2 repair");
expect(raced.stderr).not.toContain("Traceback");
expect(raced.configText).toBe(attackerProjection);
+ });
+ it("keeps forced removal identity-bound for malformed files and symlinks", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedSymlink = runDeepAgentsConfigCommand(
forcedCommand,
emptyProjection,
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 12068ce7732..1a0f8bcb61b 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -43,9 +43,88 @@ export interface DeepAgentsManagedFixtureOptions {
fifo?: boolean;
mode?: number;
swapAfterManagedOpen?: "fifo" | "symlink";
+ swapBeforeManagedLink?: string;
swapOnManagedOpen?: "fifo" | "socket" | "symlink";
swapOnManagedRead?: "fifo" | "symlink";
+ swapOnManagedSeek?: string;
symlink?: boolean;
+ timeoutMs?: number;
+}
+
+type ManagedSwap = {
+ content?: string;
+ kind: "fifo" | "regular" | "socket" | "symlink";
+ phase: "after-open" | "before-link" | "before-open" | "read" | "seek";
+};
+
+function createManagedSwapPythonExecutable(
+ fixtureRoot: string,
+ configPath: string,
+ managedSymlinkTarget: string,
+ managedSwap: ManagedSwap,
+): { executablePath: string; scriptPath: string } {
+ const executablePath = path.join(fixtureRoot, "managed-swap-python");
+ const scriptPath = path.join(fixtureRoot, "managed-swap-wrapper.py");
+ const wrapper = [
+ "import os",
+ "import socket",
+ "import sys",
+ `managed_path = ${JSON.stringify(configPath)}`,
+ `managed_target = ${JSON.stringify(managedSymlinkTarget)}`,
+ `managed_swap_kind = ${JSON.stringify(managedSwap.kind)}`,
+ `managed_swap_phase = ${JSON.stringify(managedSwap.phase)}`,
+ `managed_swap_content = ${JSON.stringify(managedSwap.content ?? "")}`,
+ "managed_socket = None",
+ "managed_swapped = False",
+ "def replace_managed_path():",
+ " global managed_socket, managed_swapped",
+ " if managed_swapped:",
+ " return",
+ " managed_swapped = True",
+ " try:",
+ " os.unlink(managed_path)",
+ " except FileNotFoundError:",
+ " pass",
+ " if managed_swap_kind == 'symlink':",
+ " os.symlink(managed_target, managed_path)",
+ " elif managed_swap_kind == 'fifo':",
+ " os.mkfifo(managed_path, 0o600)",
+ " elif managed_swap_kind == 'socket':",
+ " managed_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)",
+ " managed_socket.bind(managed_path)",
+ " else:",
+ " with open(managed_path, 'w', encoding='utf-8') as replacement:",
+ " replacement.write(managed_swap_content)",
+ " os.chmod(managed_path, 0o600)",
+ "def managed_profile(_frame, event, target):",
+ " if managed_swapped:",
+ " return",
+ " if managed_swap_phase == 'before-open' and event == 'c_call' and target is os.open:",
+ " replace_managed_path()",
+ " elif managed_swap_phase == 'after-open' and event == 'c_return' and target is os.open:",
+ " replace_managed_path()",
+ " elif managed_swap_phase == 'read' and event == 'c_call' and target is os.read:",
+ " replace_managed_path()",
+ " elif managed_swap_phase == 'before-link' and event == 'c_call' and target is os.link:",
+ " replace_managed_path()",
+ " elif managed_swap_phase == 'seek' and event == 'c_call' and target is os.lseek:",
+ " replace_managed_path()",
+ "source = sys.stdin.read()",
+ "namespace = {'__name__': '__main__'}",
+ "sys.setprofile(managed_profile)",
+ "try:",
+ " exec(compile(source, '', 'exec'), namespace)",
+ "finally:",
+ " sys.setprofile(None)",
+ "",
+ ].join("\n");
+ fs.writeFileSync(scriptPath, wrapper, { mode: 0o600 });
+ fs.writeFileSync(
+ executablePath,
+ '#!/bin/sh\nexec python3 -I "$DEEPAGENTS_FIXTURE_PYTHON_WRAPPER" "$@"\n',
+ { mode: 0o700 },
+ );
+ return { executablePath, scriptPath };
}
export function runDeepAgentsConfigCommand(
@@ -56,13 +135,25 @@ export function runDeepAgentsConfigCommand(
initialLegacyMode = 0o600,
managedOptions: DeepAgentsManagedFixtureOptions = {},
): DeepAgentsConfigCommandResult {
- const managedSwap = managedOptions.swapOnManagedOpen
+ const managedSwap: ManagedSwap | undefined = managedOptions.swapOnManagedOpen
? { kind: managedOptions.swapOnManagedOpen, phase: "before-open" }
: managedOptions.swapAfterManagedOpen
? { kind: managedOptions.swapAfterManagedOpen, phase: "after-open" }
: managedOptions.swapOnManagedRead
? { kind: managedOptions.swapOnManagedRead, phase: "read" }
- : undefined;
+ : managedOptions.swapBeforeManagedLink !== undefined
+ ? {
+ content: managedOptions.swapBeforeManagedLink,
+ kind: "regular" as const,
+ phase: "before-link" as const,
+ }
+ : managedOptions.swapOnManagedSeek !== undefined
+ ? {
+ content: managedOptions.swapOnManagedSeek,
+ kind: "regular" as const,
+ phase: "seek" as const,
+ }
+ : undefined;
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
@@ -107,61 +198,27 @@ export function runDeepAgentsConfigCommand(
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
try {
- const managedSwapPrelude = managedSwap
- ? [
- "import os as _nemoclaw_test_os",
- "import socket as _nemoclaw_test_socket_module",
- `_nemoclaw_test_path = ${JSON.stringify(configPath)}`,
- `_nemoclaw_test_target = ${JSON.stringify(managedSymlinkTarget)}`,
- `_nemoclaw_test_swap = ${JSON.stringify(managedSwap.kind)}`,
- `_nemoclaw_test_phase = ${JSON.stringify(managedSwap.phase)}`,
- "_nemoclaw_test_real_open = _nemoclaw_test_os.open",
- "_nemoclaw_test_real_read = _nemoclaw_test_os.read",
- "_nemoclaw_test_descriptor = None",
- "_nemoclaw_test_socket = None",
- "_nemoclaw_test_swapped = False",
- "def _nemoclaw_test_replace_path():",
- " global _nemoclaw_test_socket, _nemoclaw_test_swapped",
- " if _nemoclaw_test_swapped:",
- " return",
- " _nemoclaw_test_swapped = True",
- " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
- " if _nemoclaw_test_swap == 'symlink':",
- " _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
- " elif _nemoclaw_test_swap == 'fifo':",
- " _nemoclaw_test_os.mkfifo(_nemoclaw_test_path, 0o600)",
- " else:",
- " _nemoclaw_test_socket = _nemoclaw_test_socket_module.socket(_nemoclaw_test_socket_module.AF_UNIX, _nemoclaw_test_socket_module.SOCK_STREAM)",
- " _nemoclaw_test_socket.bind(_nemoclaw_test_path)",
- "def _nemoclaw_test_open(path, flags, *args, **kwargs):",
- " global _nemoclaw_test_descriptor",
- " managed = _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path",
- " if managed and _nemoclaw_test_phase == 'before-open':",
- " _nemoclaw_test_replace_path()",
- " descriptor = _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
- " if managed:",
- " _nemoclaw_test_descriptor = descriptor",
- " if _nemoclaw_test_phase == 'after-open':",
- " _nemoclaw_test_replace_path()",
- " return descriptor",
- "def _nemoclaw_test_read(descriptor, size):",
- " if descriptor == _nemoclaw_test_descriptor and _nemoclaw_test_phase == 'read':",
- " _nemoclaw_test_replace_path()",
- " return _nemoclaw_test_real_read(descriptor, size)",
- "_nemoclaw_test_os.open = _nemoclaw_test_open",
- "_nemoclaw_test_os.read = _nemoclaw_test_read",
- ].join("\n")
- : "";
+ const managedSwapPython = managedSwap
+ ? createManagedSwapPythonExecutable(tmp, configPath, managedSymlinkTarget, managedSwap)
+ : null;
const fixtureCommand = command
- .replace("<<'PY'\n", `<<'PY'\n${managedSwapPrelude}\n`)
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
- .replaceAll("/opt/venv/bin/python3", "python3")
+ .replaceAll("/opt/venv/bin/python3", managedSwapPython?.executablePath ?? "python3")
.replace(
'runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR',
`runtime_kind = "${runtimeKind}" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
);
- const result = spawnSync("bash", ["-c", fixtureCommand], { encoding: "utf-8", timeout: 5000 });
+ const result = spawnSync("bash", ["-c", fixtureCommand], {
+ encoding: "utf-8",
+ env: {
+ ...process.env,
+ ...(managedSwapPython
+ ? { DEEPAGENTS_FIXTURE_PYTHON_WRAPPER: managedSwapPython.scriptPath }
+ : {}),
+ },
+ timeout: managedOptions.timeoutMs ?? 5000,
+ });
let configStat: fs.Stats | null = null;
try {
configStat = fs.lstatSync(configPath);
@@ -175,11 +232,7 @@ export function runDeepAgentsConfigCommand(
const configIsSymlink = configStat?.isSymbolicLink() === true;
const configIsDirectory = configStat?.isDirectory() === true;
const configText =
- configExists &&
- !configIsFifo &&
- !configIsSocket &&
- !configIsSymlink &&
- !configIsDirectory
+ configExists && !configIsFifo && !configIsSocket && !configIsSymlink && !configIsDirectory
? fs.readFileSync(configPath, "utf-8")
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
From bc8973302cc0d3d1a4f7851b2cb544d9731db815 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 18:33:31 -0700
Subject: [PATCH 09/49] fix(mcp): preserve unsafe projection race types
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
...idge-adapter-deepagents-projection.test.ts | 57 +++++++++++++++----
...cp-bridge-adapter-deepagents-projection.ts | 26 +++++----
.../mcp-bridge-status-resolution.test.ts | 35 ++++++++++--
.../mcp-bridge-adapter-deepagents-fixture.ts | 55 +++++++++++++-----
4 files changed, 131 insertions(+), 42 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index f338cfa76ef..0a87c310218 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -98,14 +98,9 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(fifo.stdout.trim()).toBe("");
expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- const directory = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { directory: true },
- );
+ const directory = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ directory: true,
+ });
expect(directory.status).toBe(2);
expect(directory.stdout.trim()).toBe("");
expect(directory.stderr).toContain(
@@ -154,9 +149,7 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(racedFifo.status).toBe(2);
expect(racedFifo.stdout.trim()).toBe("");
- expect(racedFifo.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: FIFO",
- );
+ expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
expect(racedFifo.configIsFifo).toBe(true);
const racedSocket = runDeepAgentsConfigCommand(
@@ -224,8 +217,50 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
`${JSON.stringify(emptyProjection, null, 2)}\n`,
);
+
+ const replacedAfterEloop = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { statAfterManagedEloopAsRegular: true, symlink: true },
+ );
+ expect(replacedAfterEloop.status).toBe(2);
+ expect(replacedAfterEloop.stdout.trim()).toBe("");
+ expect(replacedAfterEloop.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterEloop.configIsSymlink).toBe(true);
+ expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
});
+ it.each([
+ { appearedType: "symlink" as const, diagnostic: "symbolic link" },
+ { appearedType: "fifo" as const, diagnostic: "FIFO" },
+ ])(
+ "classifies a $diagnostic that appears after a missing-path open (#10754)",
+ ({ appearedType, diagnostic }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterMissingManagedOpen: appearedType },
+ );
+
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
+ expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
+ },
+ );
+
it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index 52f0bde24ea..d484ec70a7c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -30,15 +30,17 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def managed_path_identity(path):",
+ "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " return managed_fingerprint(os.stat(path, follow_symlinks=False))",
+ " current_metadata = os.stat(path, follow_symlinks=False)",
" except FileNotFoundError:",
- " return None",
- "def assert_managed_source_stable(path, identity):",
- " current = managed_path_identity(path)",
+ " current_metadata = None",
+ " current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
" if identity is None:",
" if current is not None:",
+ " unsafe_path_kind = managed_projection_path_kind(current_metadata.st_mode) if unsafe_path_prefix else None",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" raise ValueError('managed MCP projection appeared during mutation')",
" return",
" if current != identity:",
@@ -60,16 +62,18 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" try:",
" descriptor = os.open(path, flags)",
" except FileNotFoundError:",
- " assert_managed_source_stable(path, None)",
+ " assert_managed_source_stable(path, None, unsafe_path_prefix)",
" return b'', None, None",
" except OSError as exc:",
" unsafe_path_kind = None",
" if unsafe_path_prefix:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
- " except OSError:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
" if unsafe_path_kind:",
" raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
" raise",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 18e6dc28cb6..c2d0565db17 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,6 +7,12 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
+import {
+ baseEntry,
+ runDeepAgentsConfigCommand,
+} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
+
+import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -190,7 +196,27 @@ ${body}
}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
- it("returns a type-specific failure for unsafe Deep Agents projections (#10754)", () => {
+ it("returns a type-specific failure for unsafe Deep Agents projection races (#10754)", () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const projection = { mcpServers: {} };
+ const unsafeResults = [
+ ...(["symlink", "fifo"] as const).map((appearedType) =>
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterMissingManagedOpen: appearedType,
+ }),
+ ),
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ statAfterManagedEloopAsRegular: true,
+ symlink: true,
+ }),
+ ];
+ unsafeResults.forEach((result) => {
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
+ expect(result.configIsSymlink || result.configIsFifo).toBe(true);
+ });
+ const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -210,10 +236,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
},
});
const outcomes = [];
- for (const detail of [
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- "Unsafe managed Deep Agents MCP projection path: FIFO",
- ]) {
+ for (const detail of ${JSON.stringify(unsafeDetails)}) {
processRecovery.executeSandboxCommand = () => ({
status: 2,
stdout: "",
@@ -240,7 +263,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(2);
+ expect(outcomes).toHaveLength(3);
outcomes.forEach((outcome) => {
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 12068ce7732..6318bca93ee 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -42,7 +42,9 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ statAfterManagedEloopAsRegular?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
+ swapAfterMissingManagedOpen?: "fifo" | "symlink";
swapOnManagedOpen?: "fifo" | "socket" | "symlink";
swapOnManagedRead?: "fifo" | "symlink";
symlink?: boolean;
@@ -58,11 +60,15 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = managedOptions.swapOnManagedOpen
? { kind: managedOptions.swapOnManagedOpen, phase: "before-open" }
- : managedOptions.swapAfterManagedOpen
- ? { kind: managedOptions.swapAfterManagedOpen, phase: "after-open" }
- : managedOptions.swapOnManagedRead
- ? { kind: managedOptions.swapOnManagedRead, phase: "read" }
- : undefined;
+ : managedOptions.swapAfterMissingManagedOpen
+ ? { kind: managedOptions.swapAfterMissingManagedOpen, phase: "after-missing-open" }
+ : managedOptions.swapAfterManagedOpen
+ ? { kind: managedOptions.swapAfterManagedOpen, phase: "after-open" }
+ : managedOptions.swapOnManagedRead
+ ? { kind: managedOptions.swapOnManagedRead, phase: "read" }
+ : managedOptions.statAfterManagedEloopAsRegular
+ ? { kind: "symlink", phase: "eloop-regular-stat" }
+ : undefined;
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
@@ -91,11 +97,14 @@ export function runDeepAgentsConfigCommand(
if (fifo.status !== 0) throw new Error(fifo.stderr || "could not create managed fixture FIFO");
fs.chmodSync(configPath, managedOptions.mode ?? 0o600);
} else {
- if (!managedOptions.danglingSymlink) {
+ if (!managedOptions.danglingSymlink && managedSwap?.phase !== "after-missing-open") {
initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
if (initialConfig !== undefined)
fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
}
+ if (managedSwap?.phase === "after-missing-open") {
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
+ }
if (managedSymlink) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.symlinkSync(managedSymlinkTarget, configPath);
@@ -110,6 +119,7 @@ export function runDeepAgentsConfigCommand(
const managedSwapPrelude = managedSwap
? [
"import os as _nemoclaw_test_os",
+ "import errno as _nemoclaw_test_errno",
"import socket as _nemoclaw_test_socket_module",
`_nemoclaw_test_path = ${JSON.stringify(configPath)}`,
`_nemoclaw_test_target = ${JSON.stringify(managedSymlinkTarget)}`,
@@ -117,7 +127,10 @@ export function runDeepAgentsConfigCommand(
`_nemoclaw_test_phase = ${JSON.stringify(managedSwap.phase)}`,
"_nemoclaw_test_real_open = _nemoclaw_test_os.open",
"_nemoclaw_test_real_read = _nemoclaw_test_os.read",
+ "_nemoclaw_test_real_stat = _nemoclaw_test_os.stat",
"_nemoclaw_test_descriptor = None",
+ "_nemoclaw_test_saw_eloop = False",
+ "_nemoclaw_test_returned_regular_stat = False",
"_nemoclaw_test_socket = None",
"_nemoclaw_test_swapped = False",
"def _nemoclaw_test_replace_path():",
@@ -125,7 +138,10 @@ export function runDeepAgentsConfigCommand(
" if _nemoclaw_test_swapped:",
" return",
" _nemoclaw_test_swapped = True",
- " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
+ " try:",
+ " _nemoclaw_test_os.unlink(_nemoclaw_test_path)",
+ " except FileNotFoundError:",
+ " pass",
" if _nemoclaw_test_swap == 'symlink':",
" _nemoclaw_test_os.symlink(_nemoclaw_test_target, _nemoclaw_test_path)",
" elif _nemoclaw_test_swap == 'fifo':",
@@ -134,11 +150,18 @@ export function runDeepAgentsConfigCommand(
" _nemoclaw_test_socket = _nemoclaw_test_socket_module.socket(_nemoclaw_test_socket_module.AF_UNIX, _nemoclaw_test_socket_module.SOCK_STREAM)",
" _nemoclaw_test_socket.bind(_nemoclaw_test_path)",
"def _nemoclaw_test_open(path, flags, *args, **kwargs):",
- " global _nemoclaw_test_descriptor",
+ " global _nemoclaw_test_descriptor, _nemoclaw_test_saw_eloop",
" managed = _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path",
" if managed and _nemoclaw_test_phase == 'before-open':",
" _nemoclaw_test_replace_path()",
- " descriptor = _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
+ " try:",
+ " descriptor = _nemoclaw_test_real_open(path, flags, *args, **kwargs)",
+ " except OSError as exc:",
+ " if managed and _nemoclaw_test_phase == 'after-missing-open' and exc.errno == _nemoclaw_test_errno.ENOENT:",
+ " _nemoclaw_test_replace_path()",
+ " if managed and _nemoclaw_test_phase == 'eloop-regular-stat' and exc.errno == _nemoclaw_test_errno.ELOOP:",
+ " _nemoclaw_test_saw_eloop = True",
+ " raise",
" if managed:",
" _nemoclaw_test_descriptor = descriptor",
" if _nemoclaw_test_phase == 'after-open':",
@@ -148,8 +171,16 @@ export function runDeepAgentsConfigCommand(
" if descriptor == _nemoclaw_test_descriptor and _nemoclaw_test_phase == 'read':",
" _nemoclaw_test_replace_path()",
" return _nemoclaw_test_real_read(descriptor, size)",
+ "def _nemoclaw_test_stat(path, *args, **kwargs):",
+ " global _nemoclaw_test_returned_regular_stat",
+ " managed = _nemoclaw_test_os.fspath(path) == _nemoclaw_test_path",
+ " if managed and _nemoclaw_test_phase == 'eloop-regular-stat' and _nemoclaw_test_saw_eloop and not _nemoclaw_test_returned_regular_stat:",
+ " _nemoclaw_test_returned_regular_stat = True",
+ " return _nemoclaw_test_real_stat(_nemoclaw_test_target, follow_symlinks=False)",
+ " return _nemoclaw_test_real_stat(path, *args, **kwargs)",
"_nemoclaw_test_os.open = _nemoclaw_test_open",
"_nemoclaw_test_os.read = _nemoclaw_test_read",
+ "_nemoclaw_test_os.stat = _nemoclaw_test_stat",
].join("\n")
: "";
const fixtureCommand = command
@@ -175,11 +206,7 @@ export function runDeepAgentsConfigCommand(
const configIsSymlink = configStat?.isSymbolicLink() === true;
const configIsDirectory = configStat?.isDirectory() === true;
const configText =
- configExists &&
- !configIsFifo &&
- !configIsSocket &&
- !configIsSymlink &&
- !configIsDirectory
+ configExists && !configIsFifo && !configIsSocket && !configIsSymlink && !configIsDirectory
? fs.readFileSync(configPath, "utf-8")
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
From b77384e3a8dc95f382e301282fc5d475e250da4a Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 18:53:30 -0700
Subject: [PATCH 10/49] fix(mcp): preserve unsafe race classification
---
...idge-adapter-deepagents-projection.test.ts | 48 ++++++++++++
...cp-bridge-adapter-deepagents-projection.ts | 21 +++---
.../mcp-bridge-status-resolution.test.ts | 13 +++-
.../mcp-bridge-adapter-deepagents-fixture.ts | 75 +++++++++++++------
4 files changed, 122 insertions(+), 35 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 25d01223017..3f0ba6c8cf8 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -193,6 +193,54 @@ describe("Deep Agents managed MCP projection safety", () => {
);
});
+ it("preserves unsafe classification when a hostile path appears after an absent open (#10754)", () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const racedSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpenError: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedSymlink.status).toBe(2);
+ expect(racedSymlink.stdout.trim()).toBe("");
+ expect(racedSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(racedSymlink.configIsSymlink).toBe(true);
+ expect(racedSymlink.managedSymlinkTargetExists).toBe(false);
+
+ const racedFifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ swapAfterManagedOpenError: "fifo",
+ timeoutMs: managedCommandTimeoutMs,
+ });
+ expect(racedFifo.status).toBe(2);
+ expect(racedFifo.stdout.trim()).toBe("");
+ expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ expect(racedFifo.configIsFifo).toBe(true);
+ });
+
+ it("preserves ELOOP classification when a rejected symlink is briefly masked (#10754)", () => {
+ const raced = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ swapRejectedManagedSymlinkToRegular: attackerProjection,
+ timeoutMs: managedCommandTimeoutMs,
+ },
+ );
+
+ expect(raced.status).toBe(2);
+ expect(raced.stdout.trim()).toBe("");
+ expect(raced.stderr).toContain("Unsafe managed Deep Agents MCP projection path: symbolic link");
+ expect(raced.configIsSymlink).toBe(true);
+ expect(raced.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ });
+
it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index 52f0bde24ea..9b4c36eff79 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -30,15 +30,17 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def managed_path_identity(path):",
+ "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " return managed_fingerprint(os.stat(path, follow_symlinks=False))",
+ " current_metadata = os.stat(path, follow_symlinks=False)",
" except FileNotFoundError:",
- " return None",
- "def assert_managed_source_stable(path, identity):",
- " current = managed_path_identity(path)",
+ " current_metadata = None",
+ " current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
" if identity is None:",
" if current is not None:",
+ " unsafe_path_kind = managed_projection_path_kind(current_metadata.st_mode) if unsafe_path_prefix else None",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" raise ValueError('managed MCP projection appeared during mutation')",
" return",
" if current != identity:",
@@ -60,16 +62,15 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" try:",
" descriptor = os.open(path, flags)",
" except FileNotFoundError:",
- " assert_managed_source_stable(path, None)",
+ " assert_managed_source_stable(path, None, unsafe_path_prefix)",
" return b'', None, None",
" except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
+ " unsafe_path_kind = 'symbolic link' if unsafe_path_prefix and exc.errno == errno.ELOOP else None",
+ " if unsafe_path_prefix and unsafe_path_kind is None:",
" try:",
" unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
" except OSError:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
+ " pass",
" if unsafe_path_kind:",
" raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
" raise",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 18e6dc28cb6..4dd8b07ea1b 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -210,9 +210,11 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
},
});
const outcomes = [];
- for (const detail of [
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- "Unsafe managed Deep Agents MCP projection path: FIFO",
+ for (const [scenario, detail] of [
+ ["static symlink", "Unsafe managed Deep Agents MCP projection path: symbolic link"],
+ ["static FIFO", "Unsafe managed Deep Agents MCP projection path: FIFO"],
+ ["symlink after absent open", "Unsafe managed Deep Agents MCP projection path: symbolic link"],
+ ["masked ELOOP symlink", "Unsafe managed Deep Agents MCP projection path: symbolic link"],
]) {
processRecovery.executeSandboxCommand = () => ({
status: 2,
@@ -223,6 +225,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
errorLines.length = 0;
await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
outcomes.push({
+ scenario,
detail,
exitCode: process.exitCode ?? 0,
stdout: logLines.join("\n"),
@@ -234,14 +237,16 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
`,
);
const outcomes = JSON.parse(stdout) as Array<{
+ scenario: string;
detail: string;
exitCode: number;
stdout: string;
stderr: string;
}>;
- expect(outcomes).toHaveLength(2);
+ expect(outcomes).toHaveLength(4);
outcomes.forEach((outcome) => {
+ expect(outcome.scenario).toBeTruthy();
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
expect(outcome.stderr).toContain(outcome.detail);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 1a0f8bcb61b..82f144112f6 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -43,7 +43,9 @@ export interface DeepAgentsManagedFixtureOptions {
fifo?: boolean;
mode?: number;
swapAfterManagedOpen?: "fifo" | "symlink";
+ swapAfterManagedOpenError?: "fifo" | "symlink";
swapBeforeManagedLink?: string;
+ swapRejectedManagedSymlinkToRegular?: string;
swapOnManagedOpen?: "fifo" | "socket" | "symlink";
swapOnManagedRead?: "fifo" | "symlink";
swapOnManagedSeek?: string;
@@ -54,9 +56,40 @@ export interface DeepAgentsManagedFixtureOptions {
type ManagedSwap = {
content?: string;
kind: "fifo" | "regular" | "socket" | "symlink";
- phase: "after-open" | "before-link" | "before-open" | "read" | "seek";
+ phase: "after-open" | "before-link" | "before-open" | "open-error" | "read" | "seek";
+ restoreSymlink?: boolean;
};
+function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSwap | undefined {
+ if (options.swapOnManagedOpen) {
+ return { kind: options.swapOnManagedOpen, phase: "before-open" };
+ }
+ if (options.swapAfterManagedOpen) {
+ return { kind: options.swapAfterManagedOpen, phase: "after-open" };
+ }
+ if (options.swapAfterManagedOpenError) {
+ return { kind: options.swapAfterManagedOpenError, phase: "open-error" };
+ }
+ if (options.swapRejectedManagedSymlinkToRegular !== undefined) {
+ return {
+ content: options.swapRejectedManagedSymlinkToRegular,
+ kind: "regular",
+ phase: "open-error",
+ restoreSymlink: true,
+ };
+ }
+ if (options.swapOnManagedRead) {
+ return { kind: options.swapOnManagedRead, phase: "read" };
+ }
+ if (options.swapBeforeManagedLink !== undefined) {
+ return { content: options.swapBeforeManagedLink, kind: "regular", phase: "before-link" };
+ }
+ if (options.swapOnManagedSeek !== undefined) {
+ return { content: options.swapOnManagedSeek, kind: "regular", phase: "seek" };
+ }
+ return undefined;
+}
+
function createManagedSwapPythonExecutable(
fixtureRoot: string,
configPath: string,
@@ -74,6 +107,7 @@ function createManagedSwapPythonExecutable(
`managed_swap_kind = ${JSON.stringify(managedSwap.kind)}`,
`managed_swap_phase = ${JSON.stringify(managedSwap.phase)}`,
`managed_swap_content = ${JSON.stringify(managedSwap.content ?? "")}`,
+ `managed_restore_symlink = ${managedSwap.restoreSymlink === true ? "True" : "False"}`,
"managed_socket = None",
"managed_swapped = False",
"def replace_managed_path():",
@@ -96,6 +130,14 @@ function createManagedSwapPythonExecutable(
" with open(managed_path, 'w', encoding='utf-8') as replacement:",
" replacement.write(managed_swap_content)",
" os.chmod(managed_path, 0o600)",
+ "def restore_managed_symlink():",
+ " if not managed_restore_symlink or not managed_swapped:",
+ " return",
+ " try:",
+ " os.unlink(managed_path)",
+ " except FileNotFoundError:",
+ " pass",
+ " os.symlink(managed_target, managed_path)",
"def managed_profile(_frame, event, target):",
" if managed_swapped:",
" return",
@@ -103,6 +145,8 @@ function createManagedSwapPythonExecutable(
" replace_managed_path()",
" elif managed_swap_phase == 'after-open' and event == 'c_return' and target is os.open:",
" replace_managed_path()",
+ " elif managed_swap_phase == 'open-error' and event == 'c_exception' and target is os.open:",
+ " replace_managed_path()",
" elif managed_swap_phase == 'read' and event == 'c_call' and target is os.read:",
" replace_managed_path()",
" elif managed_swap_phase == 'before-link' and event == 'c_call' and target is os.link:",
@@ -116,6 +160,7 @@ function createManagedSwapPythonExecutable(
" exec(compile(source, '', 'exec'), namespace)",
"finally:",
" sys.setprofile(None)",
+ " restore_managed_symlink()",
"",
].join("\n");
fs.writeFileSync(scriptPath, wrapper, { mode: 0o600 });
@@ -135,25 +180,7 @@ export function runDeepAgentsConfigCommand(
initialLegacyMode = 0o600,
managedOptions: DeepAgentsManagedFixtureOptions = {},
): DeepAgentsConfigCommandResult {
- const managedSwap: ManagedSwap | undefined = managedOptions.swapOnManagedOpen
- ? { kind: managedOptions.swapOnManagedOpen, phase: "before-open" }
- : managedOptions.swapAfterManagedOpen
- ? { kind: managedOptions.swapAfterManagedOpen, phase: "after-open" }
- : managedOptions.swapOnManagedRead
- ? { kind: managedOptions.swapOnManagedRead, phase: "read" }
- : managedOptions.swapBeforeManagedLink !== undefined
- ? {
- content: managedOptions.swapBeforeManagedLink,
- kind: "regular" as const,
- phase: "before-link" as const,
- }
- : managedOptions.swapOnManagedSeek !== undefined
- ? {
- content: managedOptions.swapOnManagedSeek,
- kind: "regular" as const,
- phase: "seek" as const,
- }
- : undefined;
+ const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
@@ -172,7 +199,10 @@ export function runDeepAgentsConfigCommand(
{ mode },
);
};
- const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
+ const managedSymlink =
+ managedOptions.symlink === true ||
+ managedOptions.danglingSymlink === true ||
+ managedOptions.swapRejectedManagedSymlinkToRegular !== undefined;
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
@@ -195,6 +225,9 @@ export function runDeepAgentsConfigCommand(
if (managedSwap?.kind === "symlink") {
initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
}
+ if (managedSwap?.phase === "open-error") {
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
+ }
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
try {
From 89e98a7f589f97e8c1b90d0386d6174ce8f17014 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 19:58:31 -0700
Subject: [PATCH 11/49] fix(mcp): prioritize unsafe status paths
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 3 +-
.../mcp-bridge-status-resolution.test.ts | 50 +++++++++++--------
src/lib/actions/sandbox/mcp-bridge-status.ts | 19 ++++---
3 files changed, 43 insertions(+), 29 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 4fdb898ea95..cbe29b0ad18 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -39,12 +39,13 @@ For a trusted private server, status resolves the endpoint without changing mana
-Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code `mcp status` checks the path type.
+Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
The diagnostics are `Unsafe managed Deep Agents MCP projection path: symbolic link`, `Unsafe managed Deep Agents MCP projection path: FIFO`, and `Unsafe managed Deep Agents MCP projection path: non-regular file`.
This exit status applies only to the unsafe path-type diagnostic.
Other adapter inspection failures retain their ordinary status behavior.
+Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index baa3e18d562..e022775ee82 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -97,11 +97,13 @@ let providerCredentialObservation = "v11";
let credentialObservationCount = 0;
processRecovery.executeSandboxExecCommand = () => {
credentialObservationCount += 1;
- return {
- status: 0,
- stdout: providerCredentialObservation,
- stderr: "",
- };
+ return providerCredentialObservation === null
+ ? undefined
+ : {
+ status: 0,
+ stdout: providerCredentialObservation,
+ stderr: "",
+ };
};
processRecovery.executeSandboxCommand = (sandboxName, command) => {
executedSandboxCommands.push(command);
@@ -242,34 +244,40 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
});
const outcomes = [];
for (const detail of ${JSON.stringify(unsafeDetails)}) {
- processRecovery.executeSandboxCommand = () => ({
- status: 2,
- stdout: "",
- stderr: detail + "\n",
- });
- logLines.length = 0;
- errorLines.length = 0;
- await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
- outcomes.push({
- detail,
- exitCode: process.exitCode ?? 0,
- stdout: logLines.join("\n"),
- stderr: errorLines.join("\n"),
- });
- process.exitCode = 0;
+ for (const observation of [null, "absent", "canonical"]) {
+ providerCredentialObservation = observation;
+ processRecovery.executeSandboxCommand = () => ({
+ status: 2,
+ stdout: "",
+ stderr: detail + "\n",
+ });
+ logLines.length = 0;
+ errorLines.length = 0;
+ await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
+ outcomes.push({
+ detail,
+ observation: observation ?? "unavailable",
+ exitCode: process.exitCode ?? 0,
+ stdout: logLines.join("\n"),
+ stderr: errorLines.join("\n"),
+ });
+ process.exitCode = 0;
+ }
}
process.stdout.write(JSON.stringify(outcomes));
`,
);
const outcomes = JSON.parse(stdout) as Array<{
detail: string;
+ observation: string;
exitCode: number;
stdout: string;
stderr: string;
}>;
- expect(outcomes).toHaveLength(3);
+ expect(outcomes).toHaveLength(9);
outcomes.forEach((outcome) => {
+ expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
expect(outcome.stderr).toContain(outcome.detail);
diff --git a/src/lib/actions/sandbox/mcp-bridge-status.ts b/src/lib/actions/sandbox/mcp-bridge-status.ts
index a030014be30..7d04efa0f55 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status.ts
@@ -97,12 +97,14 @@ function getAdapterRegistration(
): McpBridgeStatus["adapter"] {
if (!entry) return { registered: null };
if (!adapter) return { registered: null, detail: "MCP adapter is not declared" };
- if (credentialObservationDetail) {
- return {
- registered: null,
- detail: `Adapter inspection was skipped because ${credentialObservationDetail}.`,
- };
- }
+ const credentialInspectionSkip: McpBridgeStatus["adapter"] | null =
+ credentialObservationDetail === undefined
+ ? null
+ : {
+ registered: null,
+ detail: `Adapter inspection was skipped because ${credentialObservationDetail}.`,
+ };
+ if (credentialInspectionSkip && adapter !== "deepagents-config") return credentialInspectionSkip;
if (adapter === "hermes-config" && hermesReconciliation) {
return hermesReconciliation.ok
? { registered: true }
@@ -120,8 +122,10 @@ function getAdapterRegistration(
? buildHermesMcpStatusCommand(entry, credentialRevision)
: buildDeepAgentsMcpStatusCommand(entry, credentialRevision);
const result = executeSandboxCommand(sandboxName, command);
- if (!result) return { registered: null, detail: "sandbox unreachable" };
+ if (!result)
+ return credentialInspectionSkip ?? { registered: null, detail: "sandbox unreachable" };
if (result.status === 0) {
+ if (credentialInspectionSkip) return credentialInspectionSkip;
const output = result.stdout.trim();
if (output === "registered") return { registered: true };
return { registered: false, detail: output || "not found" };
@@ -139,6 +143,7 @@ function getAdapterRegistration(
) {
throw new McpBridgeError(normalizedDetail, 2);
}
+ if (credentialInspectionSkip) return credentialInspectionSkip;
return {
registered: false,
detail,
From 79cccd9099e5698af6eb9aa5d3ae5a09758b9bc7 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 22:15:43 -0700
Subject: [PATCH 12/49] fix(mcp): pin managed projection parents
---
.../mcp-bridge-adapter-deepagents-legacy.ts | 10 +-
...idge-adapter-deepagents-projection.test.ts | 447 +++++++++++-------
...cp-bridge-adapter-deepagents-projection.ts | 286 +++++++----
...-bridge-adapter-deepagents-registration.ts | 10 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 22 +-
.../mcp-bridge-status-resolution.test.ts | 36 +-
.../mcp-bridge-adapter-deepagents-fixture.ts | 25 +-
7 files changed, 508 insertions(+), 328 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index 50251a1de33..f7d3e18acd6 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -71,14 +71,16 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
+ " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
" elif os.path.lexists(config_path):",
" data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
@@ -108,8 +110,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
+ " config_path.parent.mkdir(parents=True, exist_ok=True)",
" tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
" os.fchmod(tmp_fd, 0o600)",
@@ -131,7 +133,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 02d46fbda32..b020684c52b 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -24,203 +24,243 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it("applies the shared server cap before normal and rollback v2 publication", () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it(
+ "applies the shared server cap before normal and rollback v2 publication",
+ { timeout: 60_000 },
+ () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- });
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ },
+ );
- it("names unsafe projection types without following hostile paths (#10754)", () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const symlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { symlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(symlink.status).toBe(2);
- expect(symlink.stdout.trim()).toBe("");
- expect(symlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ it(
+ "names unsafe projection types without following hostile paths (#10754)",
+ { timeout: 60_000 },
+ () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const symlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { symlink: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(symlink.status).toBe(2);
+ expect(symlink.stdout.trim()).toBe("");
+ expect(symlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(symlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
- fifo: true,
- timeoutMs: managedCommandTimeoutMs,
- });
- expect(fifo.status).toBe(2);
- expect(fifo.stdout.trim()).toBe("");
- expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ fifo: true,
+ timeoutMs: managedCommandTimeoutMs,
+ });
+ expect(fifo.status).toBe(2);
+ expect(fifo.stdout.trim()).toBe("");
+ expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- const directory = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
- directory: true,
- timeoutMs: managedCommandTimeoutMs,
- });
- expect(directory.status).toBe(2);
- expect(directory.stdout.trim()).toBe("");
- expect(directory.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
+ const directory = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ directory: true,
+ timeoutMs: managedCommandTimeoutMs,
+ },
+ );
+ expect(directory.status).toBe(2);
+ expect(directory.stdout.trim()).toBe("");
+ expect(directory.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
- const danglingSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(danglingSymlink.status).toBe(2);
- expect(danglingSymlink.stdout.trim()).toBe("");
- expect(danglingSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
+ const danglingSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(danglingSymlink.status).toBe(2);
+ expect(danglingSymlink.stdout.trim()).toBe("");
+ expect(danglingSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
- const racedSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSymlink.status).toBe(2);
- expect(racedSymlink.stdout.trim()).toBe("");
- expect(racedSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(racedSymlink.configIsSymlink).toBe(true);
- expect(racedSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const racedSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedSymlink.status).toBe(2);
+ expect(racedSymlink.stdout.trim()).toBe("");
+ expect(racedSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(racedSymlink.configIsSymlink).toBe(true);
+ expect(racedSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const racedFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedFifo.status).toBe(2);
- expect(racedFifo.stdout.trim()).toBe("");
- expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- expect(racedFifo.configIsFifo).toBe(true);
+ const racedFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedFifo.status).toBe(2);
+ expect(racedFifo.stdout.trim()).toBe("");
+ expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ expect(racedFifo.configIsFifo).toBe(true);
- const racedSocket = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSocket.status).toBe(2);
- expect(racedSocket.stdout.trim()).toBe("");
- expect(racedSocket.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
- expect(racedSocket.configIsSocket).toBe(true);
+ const racedSocket = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedSocket.status).toBe(2);
+ expect(racedSocket.stdout.trim()).toBe("");
+ expect(racedSocket.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
+ expect(racedSocket.configIsSocket).toBe(true);
- const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedAfterOpenWithSymlink.status).toBe(2);
- expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
- expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedAfterOpenWithSymlink.status).toBe(2);
+ expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedAfterOpenWithFifo.status).toBe(2);
- expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithFifo.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: FIFO",
- );
- expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
+ const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedAfterOpenWithFifo.status).toBe(2);
+ expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithFifo.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
+ );
+ expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
- const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedWhileReadingWithSymlink.status).toBe(2);
- expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
- expect(replacedWhileReadingWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
- expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedWhileReadingWithSymlink.status).toBe(2);
+ expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
+ expect(replacedWhileReadingWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const replacedAfterEloop = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- {
- statAfterManagedEloopAsRegular: true,
- symlink: true,
- timeoutMs: managedCommandTimeoutMs,
- },
- );
- expect(replacedAfterEloop.status).toBe(2);
- expect(replacedAfterEloop.stdout.trim()).toBe("");
- expect(replacedAfterEloop.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedAfterEloop.configIsSymlink).toBe(true);
- expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
- });
+ const replacedAfterEloop = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ statAfterManagedEloopAsRegular: true,
+ symlink: true,
+ timeoutMs: managedCommandTimeoutMs,
+ },
+ );
+ expect(replacedAfterEloop.status).toBe(2);
+ expect(replacedAfterEloop.stdout.trim()).toBe("");
+ expect(replacedAfterEloop.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterEloop.configIsSymlink).toBe(true);
+ expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
+ },
+ );
it.each([
- { appearedType: "symlink" as const, diagnostic: "symbolic link" },
- { appearedType: "fifo" as const, diagnostic: "FIFO" },
+ {
+ appearedType: "symlink" as const,
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ },
+ {
+ appearedType: "fifo" as const,
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ },
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({ appearedType, diagnostic }) => {
+ ({
+ appearedType,
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -235,10 +275,49 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(
- appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ it("rejects a symlinked projection parent without reading its target (#10754)", () => {
+ const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
+ const status = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true },
+ );
+ expect(status.status).toBe(2);
+ expect(status.stdout.trim()).toBe("");
+ expect(status.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(status.managedParentIsSymlink).toBe(true);
+ expect(status.managedParentTargetText).toBe(initialText);
+ });
+
+ it.each([
+ ["registration", registrationCommand],
+ ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
+ ])(
+ "rejects a symlinked projection parent during %s without mutating its target (#10754)",
+ (_operation, command) => {
+ const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
+ const mutation = runDeepAgentsConfigCommand(
+ command,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true },
);
+ expect(mutation.status).toBe(2);
+ expect(mutation.managedParentIsSymlink).toBe(true);
+ expect(mutation.managedParentTargetText).toBe(initialText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index d484ec70a7c..c21db5ed120 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,11 +28,76 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
+ "def managed_parent_path_kind(mode):",
+ " if stat.S_ISLNK(mode):",
+ " return 'symbolic link'",
+ " if not stat.S_ISDIR(mode):",
+ " return 'non-directory parent'",
+ " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
+ "def managed_parent_fingerprint(metadata):",
+ " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
+ "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
+ " unsafe_path_kind = None",
+ " try:",
+ " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind and unsafe_path_prefix:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
+ " raise exc",
+ "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
+ " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
+ " raise ValueError('managed MCP projection path must be absolute')",
+ " components = path.parts[1:-1]",
+ " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
+ " created = False",
+ " try:",
+ " for index, component in enumerate(components):",
+ " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except FileNotFoundError:",
+ " if not create or index != len(components) - 1:",
+ " raise",
+ " try:",
+ " os.mkdir(component, 0o700, dir_fd=descriptor)",
+ " created = True",
+ " except FileExistsError:",
+ " pass",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " os.close(descriptor)",
+ " descriptor = next_descriptor",
+ " metadata = os.fstat(descriptor)",
+ " if not stat.S_ISDIR(metadata.st_mode):",
+ " raise ValueError('managed MCP projection parent is not a directory')",
+ " return descriptor, managed_parent_fingerprint(metadata), created",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
+ "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " current_metadata = os.stat(path, follow_symlinks=False)",
+ " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " except FileNotFoundError:",
+ " current = None",
+ " descriptor = None",
+ " try:",
+ " if current != identity:",
+ " raise ValueError('managed MCP projection parent changed during operation')",
+ " finally:",
+ " if descriptor is not None:",
+ " os.close(descriptor)",
+ "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
+ " try:",
+ " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -45,9 +110,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(path, follow_symlinks=False)",
+ " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -57,78 +122,90 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " access = os.O_RDWR if writable else os.O_RDONLY",
- " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " descriptor = os.open(path, flags)",
+ " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
" except FileNotFoundError:",
- " assert_managed_source_stable(path, None, unsafe_path_prefix)",
- " return b'', None, None",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
+ " return b'', None, None, None, None",
+ " entry_name = path.name",
+ " access = os.O_RDWR if writable else os.O_RDONLY",
+ " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(path, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " try:",
+ " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b'', None, None, parent_descriptor, parent_identity",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " return b''.join(chunks), managed_fingerprint(after), descriptor",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
" except Exception:",
- " os.close(descriptor)",
+ " os.close(parent_descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptor(descriptor):",
- " if descriptor is None:",
- " return",
- " try:",
- " os.close(descriptor)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
+ " for current in (descriptor, parent_descriptor):",
+ " if current is None:",
+ " continue",
+ " try:",
+ " os.close(current)",
+ " except OSError:",
+ " pass",
"def load_managed_projection_for_update(path):",
- " raw, identity, descriptor = open_managed_projection(path, True)",
+ " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
" try:",
- " return decode_managed_projection(raw), identity, descriptor",
+ " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
" except Exception:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
" raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
@@ -137,10 +214,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor):",
- " if identity is None or descriptor is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_source_stable(path, identity)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -155,40 +233,62 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path, descriptor)",
- "def publish_absent_managed_projection(path, value):",
- " assert_managed_source_stable(path, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
- " try:",
- " os.fchmod(tmp_fd, 0o600)",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
+ " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ "def create_managed_projection_temp(parent_descriptor):",
+ " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " for _ in range(16):",
+ " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
" try:",
- " os.link(tmp_name, path, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name)",
- " finally:",
- " try:",
- " os.unlink(tmp_name)",
- " except FileNotFoundError:",
+ " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
+ " except FileExistsError:",
" pass",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- "def write_managed_projection(path, value, identity, descriptor):",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value)",
- " else:",
+ " raise FileExistsError('could not allocate managed MCP projection temporary file')",
+ "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
+ " owns_parent_descriptor = parent_descriptor is None",
+ " if parent_descriptor is None:",
+ " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
+ " if not created:",
+ " os.close(parent_descriptor)",
+ " raise ValueError('managed MCP projection parent appeared during publication')",
+ " try:",
+ " if not owns_parent_descriptor:",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
" try:",
- " rewrite_managed_projection(path, value, identity, descriptor)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " try:",
+ " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " try:",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " pass",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ " finally:",
+ " if owns_parent_descriptor:",
+ " os.close(parent_descriptor)",
+ "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " try:",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
+ " else:",
+ " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
+ " finally:",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 0b9365de1b0..5657a097489 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,19 +52,20 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys, tempfile",
+ "import json, os, pathlib, stat, sys",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
+ "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptor(source_descriptor)",
+ " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -95,9 +96,8 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index c27d89dc2f3..4ffe28abe34 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -61,18 +61,20 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def finish(outcome):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor):",
+ "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
"def write_legacy_data(value):",
@@ -97,7 +99,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -106,7 +108,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
@@ -118,7 +120,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('unowned')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -128,7 +130,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -138,7 +140,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -158,7 +160,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
" write_legacy_data(data)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index e022775ee82..b61e9e82284 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,12 +7,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
-import {
- baseEntry,
- runDeepAgentsConfigCommand,
-} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
-
-import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -199,31 +193,13 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
- { timeout: 120_000 },
+ "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
+ { timeout: 30_000 },
() => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const projection = { mcpServers: {} };
- const unsafeResults = [
- ...(["symlink", "fifo"] as const).map((appearedType) =>
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterMissingManagedOpen: appearedType,
- timeoutMs: 30_000,
- }),
- ),
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- statAfterManagedEloopAsRegular: true,
- symlink: true,
- timeoutMs: 30_000,
- }),
+ const unsafeDetails = [
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
];
- unsafeResults.forEach((result) => {
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
- expect(result.configIsSymlink || result.configIsFifo).toBe(true);
- });
- const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -275,7 +251,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(9);
+ expect(outcomes).toHaveLength(6);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 3952fc954d3..0513903b952 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -33,6 +33,8 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
+ managedParentIsSymlink: boolean;
+ managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
}
@@ -42,6 +44,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ parentSymlink?: boolean;
statAfterManagedEloopAsRegular?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -194,8 +197,11 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
- const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
+ const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
+ const managedParentPath = path.dirname(configPath);
+ const managedParentTarget = path.join(tmp, "managed-parent-target");
+ const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
const initializeConfig = (
@@ -213,7 +219,11 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.directory) {
+ if (managedOptions.parentSymlink) {
+ fs.mkdirSync(managedParentTarget, { recursive: true });
+ initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
+ fs.symlinkSync(managedParentTarget, managedParentPath);
+ } else if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -268,6 +278,12 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
+ let managedParentIsSymlink = false;
+ try {
+ managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
+ } catch {
+ // A missing projection parent cannot redirect the managed path.
+ }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
@@ -281,6 +297,9 @@ export function runDeepAgentsConfigCommand(
const managedSymlinkTargetText = managedSymlinkTargetExists
? fs.readFileSync(managedSymlinkTarget, "utf-8")
: null;
+ const managedParentTargetText = fs.existsSync(managedParentTargetConfig)
+ ? fs.readFileSync(managedParentTargetConfig, "utf-8")
+ : null;
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
@@ -306,6 +325,8 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
+ managedParentIsSymlink,
+ managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
};
From 854c257c075cc4949ad7903ddf2ae19b8aee72d0 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Tue, 1 Sep 2026 22:50:01 -0700
Subject: [PATCH 13/49] fix(mcp): keep projection safety fix scoped
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
.../mcp-bridge-adapter-deepagents-legacy.ts | 10 +-
...idge-adapter-deepagents-projection.test.ts | 447 +++++++-----------
...cp-bridge-adapter-deepagents-projection.ts | 286 ++++-------
...-bridge-adapter-deepagents-registration.ts | 10 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 22 +-
.../mcp-bridge-status-resolution.test.ts | 36 +-
.../mcp-bridge-adapter-deepagents-fixture.ts | 25 +-
7 files changed, 328 insertions(+), 508 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index f7d3e18acd6..50251a1de33 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -71,16 +71,14 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
+ " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
" elif os.path.lexists(config_path):",
" data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
@@ -110,8 +108,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
- " config_path.parent.mkdir(parents=True, exist_ok=True)",
" tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
" os.fchmod(tmp_fd, 0o600)",
@@ -133,7 +131,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index b020684c52b..02d46fbda32 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -24,243 +24,203 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it(
- "applies the shared server cap before normal and rollback v2 publication",
- { timeout: 60_000 },
- () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it("applies the shared server cap before normal and rollback v2 publication", () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- undefined,
- "v2",
- undefined,
- 0o600,
- { timeoutMs: managedCommandTimeoutMs },
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- },
- );
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ });
- it(
- "names unsafe projection types without following hostile paths (#10754)",
- { timeout: 60_000 },
- () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const symlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { symlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(symlink.status).toBe(2);
- expect(symlink.stdout.trim()).toBe("");
- expect(symlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(symlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ it("names unsafe projection types without following hostile paths (#10754)", () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const symlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { symlink: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(symlink.status).toBe(2);
+ expect(symlink.stdout.trim()).toBe("");
+ expect(symlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
- const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
- fifo: true,
- timeoutMs: managedCommandTimeoutMs,
- });
- expect(fifo.status).toBe(2);
- expect(fifo.stdout.trim()).toBe("");
- expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ fifo: true,
+ timeoutMs: managedCommandTimeoutMs,
+ });
+ expect(fifo.status).toBe(2);
+ expect(fifo.stdout.trim()).toBe("");
+ expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- const directory = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- {
- directory: true,
- timeoutMs: managedCommandTimeoutMs,
- },
- );
- expect(directory.status).toBe(2);
- expect(directory.stdout.trim()).toBe("");
- expect(directory.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
+ const directory = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
+ directory: true,
+ timeoutMs: managedCommandTimeoutMs,
+ });
+ expect(directory.status).toBe(2);
+ expect(directory.stdout.trim()).toBe("");
+ expect(directory.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
- const danglingSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(danglingSymlink.status).toBe(2);
- expect(danglingSymlink.stdout.trim()).toBe("");
- expect(danglingSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
+ const danglingSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(danglingSymlink.status).toBe(2);
+ expect(danglingSymlink.stdout.trim()).toBe("");
+ expect(danglingSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
- const racedSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSymlink.status).toBe(2);
- expect(racedSymlink.stdout.trim()).toBe("");
- expect(racedSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(racedSymlink.configIsSymlink).toBe(true);
- expect(racedSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const racedSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedSymlink.status).toBe(2);
+ expect(racedSymlink.stdout.trim()).toBe("");
+ expect(racedSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(racedSymlink.configIsSymlink).toBe(true);
+ expect(racedSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const racedFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedFifo.status).toBe(2);
- expect(racedFifo.stdout.trim()).toBe("");
- expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- expect(racedFifo.configIsFifo).toBe(true);
+ const racedFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedFifo.status).toBe(2);
+ expect(racedFifo.stdout.trim()).toBe("");
+ expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
+ expect(racedFifo.configIsFifo).toBe(true);
- const racedSocket = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSocket.status).toBe(2);
- expect(racedSocket.stdout.trim()).toBe("");
- expect(racedSocket.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
- expect(racedSocket.configIsSocket).toBe(true);
+ const racedSocket = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(racedSocket.status).toBe(2);
+ expect(racedSocket.stdout.trim()).toBe("");
+ expect(racedSocket.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
+ );
+ expect(racedSocket.configIsSocket).toBe(true);
- const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedAfterOpenWithSymlink.status).toBe(2);
- expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
- expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedAfterOpenWithSymlink.status).toBe(2);
+ expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedAfterOpenWithFifo.status).toBe(2);
- expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithFifo.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: FIFO",
- );
- expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
+ const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedAfterOpenWithFifo.status).toBe(2);
+ expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
+ expect(replacedAfterOpenWithFifo.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
+ );
+ expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
- const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedWhileReadingWithSymlink.status).toBe(2);
- expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
- expect(replacedWhileReadingWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
- expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
+ const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(replacedWhileReadingWithSymlink.status).toBe(2);
+ expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
+ expect(replacedWhileReadingWithSymlink.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
+ expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
- const replacedAfterEloop = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- {
- statAfterManagedEloopAsRegular: true,
- symlink: true,
- timeoutMs: managedCommandTimeoutMs,
- },
- );
- expect(replacedAfterEloop.status).toBe(2);
- expect(replacedAfterEloop.stdout.trim()).toBe("");
- expect(replacedAfterEloop.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedAfterEloop.configIsSymlink).toBe(true);
- expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
- },
- );
+ const replacedAfterEloop = runDeepAgentsConfigCommand(
+ statusCommand,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ statAfterManagedEloopAsRegular: true,
+ symlink: true,
+ timeoutMs: managedCommandTimeoutMs,
+ },
+ );
+ expect(replacedAfterEloop.status).toBe(2);
+ expect(replacedAfterEloop.stdout.trim()).toBe("");
+ expect(replacedAfterEloop.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(replacedAfterEloop.configIsSymlink).toBe(true);
+ expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ );
+ });
it.each([
- {
- appearedType: "symlink" as const,
- diagnostic: "symbolic link",
- expectedConfigIsFifo: false,
- expectedConfigIsSymlink: true,
- expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
- },
- {
- appearedType: "fifo" as const,
- diagnostic: "FIFO",
- expectedConfigIsFifo: true,
- expectedConfigIsSymlink: false,
- expectedTargetText: null,
- },
+ { appearedType: "symlink" as const, diagnostic: "symbolic link" },
+ { appearedType: "fifo" as const, diagnostic: "FIFO" },
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({
- appearedType,
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSymlink,
- expectedTargetText,
- }) => {
+ ({ appearedType, diagnostic }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -275,49 +235,10 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(result.configIsFifo).toBe(expectedConfigIsFifo);
- expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
- expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
- },
- );
-
- it("rejects a symlinked projection parent without reading its target (#10754)", () => {
- const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
- const status = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { parentSymlink: true },
- );
- expect(status.status).toBe(2);
- expect(status.stdout.trim()).toBe("");
- expect(status.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(status.managedParentIsSymlink).toBe(true);
- expect(status.managedParentTargetText).toBe(initialText);
- });
-
- it.each([
- ["registration", registrationCommand],
- ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
- ])(
- "rejects a symlinked projection parent during %s without mutating its target (#10754)",
- (_operation, command) => {
- const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
- const mutation = runDeepAgentsConfigCommand(
- command,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { parentSymlink: true },
+ expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(
+ appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
);
- expect(mutation.status).toBe(2);
- expect(mutation.managedParentIsSymlink).toBe(true);
- expect(mutation.managedParentTargetText).toBe(initialText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index c21db5ed120..d484ec70a7c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,76 +28,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
- "def managed_parent_path_kind(mode):",
- " if stat.S_ISLNK(mode):",
- " return 'symbolic link'",
- " if not stat.S_ISDIR(mode):",
- " return 'non-directory parent'",
- " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def managed_parent_fingerprint(metadata):",
- " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
- "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
- " unsafe_path_kind = None",
- " try:",
- " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind and unsafe_path_prefix:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " if unsafe_path_kind:",
- " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
- " raise exc",
- "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
- " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
- " raise ValueError('managed MCP projection path must be absolute')",
- " components = path.parts[1:-1]",
- " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
- " created = False",
- " try:",
- " for index, component in enumerate(components):",
- " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except FileNotFoundError:",
- " if not create or index != len(components) - 1:",
- " raise",
- " try:",
- " os.mkdir(component, 0o700, dir_fd=descriptor)",
- " created = True",
- " except FileExistsError:",
- " pass",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " os.close(descriptor)",
- " descriptor = next_descriptor",
- " metadata = os.fstat(descriptor)",
- " if not stat.S_ISDIR(metadata.st_mode):",
- " raise ValueError('managed MCP projection parent is not a directory')",
- " return descriptor, managed_parent_fingerprint(metadata), created",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
- "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
+ "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " current = None",
- " descriptor = None",
- " try:",
- " if current != identity:",
- " raise ValueError('managed MCP projection parent changed during operation')",
- " finally:",
- " if descriptor is not None:",
- " os.close(descriptor)",
- "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
- " try:",
- " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " current_metadata = os.stat(path, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -110,9 +45,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " linked = os.stat(path, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -122,90 +57,78 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " try:",
- " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " return b'', None, None, None, None",
- " entry_name = path.name",
" access = os.O_RDWR if writable else os.O_RDONLY",
" flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " try:",
- " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b'', None, None, parent_descriptor, parent_identity",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
+ " descriptor = os.open(path, flags)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(path, None, unsafe_path_prefix)",
+ " return b'', None, None",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
- " try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(path, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor",
" except Exception:",
- " os.close(parent_descriptor)",
+ " os.close(descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
- " for current in (descriptor, parent_descriptor):",
- " if current is None:",
- " continue",
- " try:",
- " os.close(current)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptor(descriptor):",
+ " if descriptor is None:",
+ " return",
+ " try:",
+ " os.close(descriptor)",
+ " except OSError:",
+ " pass",
"def load_managed_projection_for_update(path):",
- " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
+ " raw, identity, descriptor = open_managed_projection(path, True)",
" try:",
- " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
+ " return decode_managed_projection(raw), identity, descriptor",
" except Exception:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
" raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
@@ -214,11 +137,10 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor):",
+ " if identity is None or descriptor is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, identity)",
+ " assert_managed_source_stable(path, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -233,62 +155,40 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
- " assert_managed_parent_stable(path, parent_identity)",
- "def create_managed_projection_temp(parent_descriptor):",
- " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
- " for _ in range(16):",
- " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
+ " validate_managed_descriptor_path(path, descriptor)",
+ "def publish_absent_managed_projection(path, value):",
+ " assert_managed_source_stable(path, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
+ " try:",
+ " os.fchmod(tmp_fd, 0o600)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
" try:",
- " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
- " except FileExistsError:",
+ " os.link(tmp_name, path, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name)",
+ " finally:",
+ " try:",
+ " os.unlink(tmp_name)",
+ " except FileNotFoundError:",
" pass",
- " raise FileExistsError('could not allocate managed MCP projection temporary file')",
- "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
- " owns_parent_descriptor = parent_descriptor is None",
- " if parent_descriptor is None:",
- " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
- " if not created:",
- " os.close(parent_descriptor)",
- " raise ValueError('managed MCP projection parent appeared during publication')",
- " try:",
- " if not owns_parent_descriptor:",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ "def write_managed_projection(path, value, identity, descriptor):",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value)",
+ " else:",
" try:",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
- " try:",
- " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " rewrite_managed_projection(path, value, identity, descriptor)",
" finally:",
- " try:",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " pass",
- " assert_managed_parent_stable(path, parent_identity)",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- " finally:",
- " if owns_parent_descriptor:",
- " os.close(parent_descriptor)",
- "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " try:",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
- " else:",
- " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
- " finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 5657a097489..0b9365de1b0 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,20 +52,19 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys",
+ "import json, os, pathlib, stat, sys, tempfile",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
- "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
+ " close_managed_projection_descriptor(source_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -96,8 +95,9 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index 4ffe28abe34..c27d89dc2f3 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -61,20 +61,18 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
"def finish(outcome):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
+ "def repair_v2_projection(identity, descriptor):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
"def write_legacy_data(value):",
@@ -99,7 +97,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -108,7 +106,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
@@ -120,7 +118,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('unowned')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -130,7 +128,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -140,7 +138,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -160,7 +158,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
" write_legacy_data(data)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index b61e9e82284..e022775ee82 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,6 +7,12 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
+import {
+ baseEntry,
+ runDeepAgentsConfigCommand,
+} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
+
+import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -193,13 +199,31 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
- { timeout: 30_000 },
+ "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
+ { timeout: 120_000 },
() => {
- const unsafeDetails = [
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- "Unsafe managed Deep Agents MCP projection path: FIFO",
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const projection = { mcpServers: {} };
+ const unsafeResults = [
+ ...(["symlink", "fifo"] as const).map((appearedType) =>
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterMissingManagedOpen: appearedType,
+ timeoutMs: 30_000,
+ }),
+ ),
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ statAfterManagedEloopAsRegular: true,
+ symlink: true,
+ timeoutMs: 30_000,
+ }),
];
+ unsafeResults.forEach((result) => {
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
+ expect(result.configIsSymlink || result.configIsFifo).toBe(true);
+ });
+ const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -251,7 +275,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(6);
+ expect(outcomes).toHaveLength(9);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 0513903b952..3952fc954d3 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -33,8 +33,6 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
- managedParentIsSymlink: boolean;
- managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
}
@@ -44,7 +42,6 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
- parentSymlink?: boolean;
statAfterManagedEloopAsRegular?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -197,11 +194,8 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
- const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
+ const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
- const managedParentPath = path.dirname(configPath);
- const managedParentTarget = path.join(tmp, "managed-parent-target");
- const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
const initializeConfig = (
@@ -219,11 +213,7 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.parentSymlink) {
- fs.mkdirSync(managedParentTarget, { recursive: true });
- initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
- fs.symlinkSync(managedParentTarget, managedParentPath);
- } else if (managedOptions.directory) {
+ if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -278,12 +268,6 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
- let managedParentIsSymlink = false;
- try {
- managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
- } catch {
- // A missing projection parent cannot redirect the managed path.
- }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
@@ -297,9 +281,6 @@ export function runDeepAgentsConfigCommand(
const managedSymlinkTargetText = managedSymlinkTargetExists
? fs.readFileSync(managedSymlinkTarget, "utf-8")
: null;
- const managedParentTargetText = fs.existsSync(managedParentTargetConfig)
- ? fs.readFileSync(managedParentTargetConfig, "utf-8")
- : null;
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
@@ -325,8 +306,6 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
- managedParentIsSymlink,
- managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
};
From 4d8f0aa1828c2ff4ab46beb51daf5adff41903a5 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 23:02:18 -0700
Subject: [PATCH 14/49] test(mcp): separate unsafe projection scenarios
---
...idge-adapter-deepagents-projection.test.ts | 351 ++++++++++--------
.../mcp-bridge-adapter-deepagents-fixture.ts | 2 +
2 files changed, 192 insertions(+), 161 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index b020684c52b..173152e49cc 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -55,188 +55,216 @@ describe("Deep Agents managed MCP projection safety", () => {
},
);
- it(
- "names unsafe projection types without following hostile paths (#10754)",
- { timeout: 60_000 },
- () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const symlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { symlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(symlink.status).toBe(2);
- expect(symlink.stdout.trim()).toBe("");
- expect(symlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(symlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
-
- const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
- fifo: true,
- timeoutMs: managedCommandTimeoutMs,
- });
- expect(fifo.status).toBe(2);
- expect(fifo.stdout.trim()).toBe("");
- expect(fifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
-
- const directory = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- {
- directory: true,
- timeoutMs: managedCommandTimeoutMs,
- },
- );
- expect(directory.status).toBe(2);
- expect(directory.stdout.trim()).toBe("");
- expect(directory.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
-
- const danglingSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { danglingSymlink: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(danglingSymlink.status).toBe(2);
- expect(danglingSymlink.stdout.trim()).toBe("");
- expect(danglingSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
-
- const racedSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSymlink.status).toBe(2);
- expect(racedSymlink.stdout.trim()).toBe("");
- expect(racedSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(racedSymlink.configIsSymlink).toBe(true);
- expect(racedSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
-
- const racedFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedFifo.status).toBe(2);
- expect(racedFifo.stdout.trim()).toBe("");
- expect(racedFifo.stderr).toContain("Unsafe managed Deep Agents MCP projection path: FIFO");
- expect(racedFifo.configIsFifo).toBe(true);
-
- const racedSocket = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: "socket", timeoutMs: managedCommandTimeoutMs },
- );
- expect(racedSocket.status).toBe(2);
- expect(racedSocket.stdout.trim()).toBe("");
- expect(racedSocket.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
- expect(racedSocket.configIsSocket).toBe(true);
-
- const replacedAfterOpenWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedAfterOpenWithSymlink.status).toBe(2);
- expect(replacedAfterOpenWithSymlink.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(replacedAfterOpenWithSymlink.configIsSymlink).toBe(true);
- expect(replacedAfterOpenWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
- );
-
- const replacedAfterOpenWithFifo = runDeepAgentsConfigCommand(
- statusCommand,
- emptyProjection,
+ it.each([
+ {
+ caseName: "symbolic link",
+ diagnostic: "symbolic link",
+ expectedConfigIsDirectory: false,
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ initialConfig: emptyProjection,
+ options: { symlink: true },
+ },
+ {
+ caseName: "FIFO",
+ diagnostic: "FIFO",
+ expectedConfigIsDirectory: false,
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ initialConfig: undefined,
+ options: { fifo: true },
+ },
+ {
+ caseName: "directory",
+ diagnostic: "non-regular file",
+ expectedConfigIsDirectory: true,
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ initialConfig: undefined,
+ options: { directory: true },
+ },
+ {
+ caseName: "dangling symbolic link",
+ diagnostic: "symbolic link",
+ expectedConfigIsDirectory: false,
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: null,
+ initialConfig: undefined,
+ options: { danglingSymlink: true },
+ },
+ ])(
+ "rejects an existing $caseName projection path without following it (#10754)",
+ ({
+ diagnostic,
+ expectedConfigIsDirectory,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ initialConfig,
+ options,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ initialConfig,
"v2",
undefined,
0o600,
- { swapAfterManagedOpen: "fifo", timeoutMs: managedCommandTimeoutMs },
+ { ...options, timeoutMs: managedCommandTimeoutMs },
);
- expect(replacedAfterOpenWithFifo.status).toBe(2);
- expect(replacedAfterOpenWithFifo.stdout.trim()).toBe("");
- expect(replacedAfterOpenWithFifo.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: FIFO",
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(replacedAfterOpenWithFifo.configIsFifo).toBe(true);
+ expect(result.configIsDirectory).toBe(expectedConfigIsDirectory);
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
- const replacedWhileReadingWithSymlink = runDeepAgentsConfigCommand(
- statusCommand,
+ it.each([
+ {
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSocket: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ replacementType: "symlink" as const,
+ },
+ {
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSocket: false,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ replacementType: "fifo" as const,
+ },
+ {
+ diagnostic: "non-regular file",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSocket: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ replacementType: "socket" as const,
+ },
+ ])(
+ "rejects a $replacementType replacement before projection open (#10754)",
+ ({
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSocket,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ replacementType,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
"v2",
undefined,
0o600,
- { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(replacedWhileReadingWithSymlink.status).toBe(2);
- expect(replacedWhileReadingWithSymlink.stdout.trim()).toBe("");
- expect(replacedWhileReadingWithSymlink.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ { swapOnManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
);
- expect(replacedWhileReadingWithSymlink.configIsSymlink).toBe(true);
- expect(replacedWhileReadingWithSymlink.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSocket).toBe(expectedConfigIsSocket);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
- const replacedAfterEloop = runDeepAgentsConfigCommand(
- statusCommand,
+ it.each([
+ {
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ replacementType: "symlink" as const,
+ },
+ {
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ replacementType: "fifo" as const,
+ },
+ ])(
+ "rejects a $replacementType replacement after projection open (#10754)",
+ ({
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ replacementType,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
"v2",
undefined,
0o600,
- {
- statAfterManagedEloopAsRegular: true,
- symlink: true,
- timeoutMs: managedCommandTimeoutMs,
- },
- );
- expect(replacedAfterEloop.status).toBe(2);
- expect(replacedAfterEloop.stdout.trim()).toBe("");
- expect(replacedAfterEloop.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ { swapAfterManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
);
- expect(replacedAfterEloop.configIsSymlink).toBe(true);
- expect(replacedAfterEloop.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
},
);
+ it("rejects a symbolic-link replacement while reading the projection (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(result.configIsSymlink).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ });
+
+ it("preserves conclusive ELOOP classification across a follow-up path swap (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ statAfterManagedEloopAsRegular: true,
+ symlink: true,
+ timeoutMs: managedCommandTimeoutMs,
+ },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(result.configIsSymlink).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ });
+
it.each([
{
appearedType: "symlink" as const,
@@ -405,7 +433,7 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(raced.configText).toBe(attackerProjection);
});
- it("keeps forced removal identity-bound for malformed files and symlinks", () => {
+ it("keeps forced removal identity-bound for malformed and unsafe projections", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedSymlink = runDeepAgentsConfigCommand(
forcedCommand,
@@ -443,6 +471,7 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(forcedFifo.status).toBe(2);
expect(forcedFifo.configExists).toBe(true);
+ expect(forcedFifo.configIsFifo).toBe(true);
const duplicate = runDeepAgentsConfigCommand(removalCommand, duplicateProjection);
expect(duplicate.status).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 0513903b952..ded631c8e90 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -25,6 +25,7 @@ export interface DeepAgentsConfigCommandResult {
stdout: string;
stderr: string;
configExists: boolean;
+ configIsDirectory: boolean;
configIsFifo: boolean;
configIsSocket: boolean;
configIsSymlink: boolean;
@@ -317,6 +318,7 @@ export function runDeepAgentsConfigCommand(
stdout: result.stdout,
stderr: result.stderr,
configExists,
+ configIsDirectory,
configIsFifo,
configIsSocket,
configIsSymlink,
From dac7b3754be1f89bec01342deef5a8801207b526 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 23:11:20 -0700
Subject: [PATCH 15/49] fix(mcp): restore projection parent trust boundary
Reverts content from 854c257c075cc4949ad7903ddf2ae19b8aee72d0.
That change removes the Advisor-required parent-descriptor boundary.
---
.../mcp-bridge-adapter-deepagents-legacy.ts | 10 +-
...idge-adapter-deepagents-projection.test.ts | 102 +++++--
...cp-bridge-adapter-deepagents-projection.ts | 286 ++++++++++++------
...-bridge-adapter-deepagents-registration.ts | 10 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 22 +-
.../mcp-bridge-status-resolution.test.ts | 36 +--
.../mcp-bridge-adapter-deepagents-fixture.ts | 25 +-
7 files changed, 323 insertions(+), 168 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index 50251a1de33..f7d3e18acd6 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -71,14 +71,16 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
+ " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
" elif os.path.lexists(config_path):",
" data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
@@ -108,8 +110,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
+ " config_path.parent.mkdir(parents=True, exist_ok=True)",
" tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
" os.fchmod(tmp_fd, 0o600)",
@@ -131,7 +133,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index d48631b5734..173152e49cc 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -24,27 +24,36 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it("applies the shared server cap before normal and rollback v2 publication", () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it(
+ "applies the shared server cap before normal and rollback v2 publication",
+ { timeout: 60_000 },
+ () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- });
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ },
+ );
it.each([
{
@@ -273,7 +282,13 @@ describe("Deep Agents managed MCP projection safety", () => {
},
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({ appearedType, diagnostic }) => {
+ ({
+ appearedType,
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -288,10 +303,49 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(
- appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ it("rejects a symlinked projection parent without reading its target (#10754)", () => {
+ const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
+ const status = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true },
+ );
+ expect(status.status).toBe(2);
+ expect(status.stdout.trim()).toBe("");
+ expect(status.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(status.managedParentIsSymlink).toBe(true);
+ expect(status.managedParentTargetText).toBe(initialText);
+ });
+
+ it.each([
+ ["registration", registrationCommand],
+ ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
+ ])(
+ "rejects a symlinked projection parent during %s without mutating its target (#10754)",
+ (_operation, command) => {
+ const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
+ const mutation = runDeepAgentsConfigCommand(
+ command,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true },
);
+ expect(mutation.status).toBe(2);
+ expect(mutation.managedParentIsSymlink).toBe(true);
+ expect(mutation.managedParentTargetText).toBe(initialText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index d484ec70a7c..c21db5ed120 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,11 +28,76 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
+ "def managed_parent_path_kind(mode):",
+ " if stat.S_ISLNK(mode):",
+ " return 'symbolic link'",
+ " if not stat.S_ISDIR(mode):",
+ " return 'non-directory parent'",
+ " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
+ "def managed_parent_fingerprint(metadata):",
+ " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
+ "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
+ " unsafe_path_kind = None",
+ " try:",
+ " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind and unsafe_path_prefix:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
+ " raise exc",
+ "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
+ " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
+ " raise ValueError('managed MCP projection path must be absolute')",
+ " components = path.parts[1:-1]",
+ " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
+ " created = False",
+ " try:",
+ " for index, component in enumerate(components):",
+ " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except FileNotFoundError:",
+ " if not create or index != len(components) - 1:",
+ " raise",
+ " try:",
+ " os.mkdir(component, 0o700, dir_fd=descriptor)",
+ " created = True",
+ " except FileExistsError:",
+ " pass",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " os.close(descriptor)",
+ " descriptor = next_descriptor",
+ " metadata = os.fstat(descriptor)",
+ " if not stat.S_ISDIR(metadata.st_mode):",
+ " raise ValueError('managed MCP projection parent is not a directory')",
+ " return descriptor, managed_parent_fingerprint(metadata), created",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
+ "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " current_metadata = os.stat(path, follow_symlinks=False)",
+ " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " except FileNotFoundError:",
+ " current = None",
+ " descriptor = None",
+ " try:",
+ " if current != identity:",
+ " raise ValueError('managed MCP projection parent changed during operation')",
+ " finally:",
+ " if descriptor is not None:",
+ " os.close(descriptor)",
+ "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
+ " try:",
+ " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -45,9 +110,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(path, follow_symlinks=False)",
+ " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -57,78 +122,90 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " access = os.O_RDWR if writable else os.O_RDONLY",
- " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " descriptor = os.open(path, flags)",
+ " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
" except FileNotFoundError:",
- " assert_managed_source_stable(path, None, unsafe_path_prefix)",
- " return b'', None, None",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
+ " return b'', None, None, None, None",
+ " entry_name = path.name",
+ " access = os.O_RDWR if writable else os.O_RDONLY",
+ " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(path, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " try:",
+ " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b'', None, None, parent_descriptor, parent_identity",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " return b''.join(chunks), managed_fingerprint(after), descriptor",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
" except Exception:",
- " os.close(descriptor)",
+ " os.close(parent_descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptor(descriptor):",
- " if descriptor is None:",
- " return",
- " try:",
- " os.close(descriptor)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
+ " for current in (descriptor, parent_descriptor):",
+ " if current is None:",
+ " continue",
+ " try:",
+ " os.close(current)",
+ " except OSError:",
+ " pass",
"def load_managed_projection_for_update(path):",
- " raw, identity, descriptor = open_managed_projection(path, True)",
+ " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
" try:",
- " return decode_managed_projection(raw), identity, descriptor",
+ " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
" except Exception:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
" raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
@@ -137,10 +214,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor):",
- " if identity is None or descriptor is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_source_stable(path, identity)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -155,40 +233,62 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path, descriptor)",
- "def publish_absent_managed_projection(path, value):",
- " assert_managed_source_stable(path, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
- " try:",
- " os.fchmod(tmp_fd, 0o600)",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
+ " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ "def create_managed_projection_temp(parent_descriptor):",
+ " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " for _ in range(16):",
+ " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
" try:",
- " os.link(tmp_name, path, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name)",
- " finally:",
- " try:",
- " os.unlink(tmp_name)",
- " except FileNotFoundError:",
+ " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
+ " except FileExistsError:",
" pass",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- "def write_managed_projection(path, value, identity, descriptor):",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value)",
- " else:",
+ " raise FileExistsError('could not allocate managed MCP projection temporary file')",
+ "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
+ " owns_parent_descriptor = parent_descriptor is None",
+ " if parent_descriptor is None:",
+ " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
+ " if not created:",
+ " os.close(parent_descriptor)",
+ " raise ValueError('managed MCP projection parent appeared during publication')",
+ " try:",
+ " if not owns_parent_descriptor:",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
" try:",
- " rewrite_managed_projection(path, value, identity, descriptor)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " try:",
+ " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " try:",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " pass",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ " finally:",
+ " if owns_parent_descriptor:",
+ " os.close(parent_descriptor)",
+ "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " try:",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
+ " else:",
+ " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
+ " finally:",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 0b9365de1b0..5657a097489 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,19 +52,20 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys, tempfile",
+ "import json, os, pathlib, stat, sys",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
+ "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptor(source_descriptor)",
+ " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -95,9 +96,8 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index c27d89dc2f3..4ffe28abe34 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -61,18 +61,20 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def finish(outcome):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor):",
+ "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
"def write_legacy_data(value):",
@@ -97,7 +99,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -106,7 +108,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
@@ -118,7 +120,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('unowned')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -128,7 +130,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -138,7 +140,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -158,7 +160,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
" write_legacy_data(data)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index e022775ee82..b61e9e82284 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,12 +7,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
-import {
- baseEntry,
- runDeepAgentsConfigCommand,
-} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
-
-import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -199,31 +193,13 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
- { timeout: 120_000 },
+ "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
+ { timeout: 30_000 },
() => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const projection = { mcpServers: {} };
- const unsafeResults = [
- ...(["symlink", "fifo"] as const).map((appearedType) =>
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterMissingManagedOpen: appearedType,
- timeoutMs: 30_000,
- }),
- ),
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- statAfterManagedEloopAsRegular: true,
- symlink: true,
- timeoutMs: 30_000,
- }),
+ const unsafeDetails = [
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
];
- unsafeResults.forEach((result) => {
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
- expect(result.configIsSymlink || result.configIsFifo).toBe(true);
- });
- const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -275,7 +251,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(9);
+ expect(outcomes).toHaveLength(6);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 107c0609b1d..ded631c8e90 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -34,6 +34,8 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
+ managedParentIsSymlink: boolean;
+ managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
}
@@ -43,6 +45,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ parentSymlink?: boolean;
statAfterManagedEloopAsRegular?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -195,8 +198,11 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
- const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
+ const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
+ const managedParentPath = path.dirname(configPath);
+ const managedParentTarget = path.join(tmp, "managed-parent-target");
+ const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
const initializeConfig = (
@@ -214,7 +220,11 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.directory) {
+ if (managedOptions.parentSymlink) {
+ fs.mkdirSync(managedParentTarget, { recursive: true });
+ initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
+ fs.symlinkSync(managedParentTarget, managedParentPath);
+ } else if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -269,6 +279,12 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
+ let managedParentIsSymlink = false;
+ try {
+ managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
+ } catch {
+ // A missing projection parent cannot redirect the managed path.
+ }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
@@ -282,6 +298,9 @@ export function runDeepAgentsConfigCommand(
const managedSymlinkTargetText = managedSymlinkTargetExists
? fs.readFileSync(managedSymlinkTarget, "utf-8")
: null;
+ const managedParentTargetText = fs.existsSync(managedParentTargetConfig)
+ ? fs.readFileSync(managedParentTargetConfig, "utf-8")
+ : null;
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
@@ -308,6 +327,8 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
+ managedParentIsSymlink,
+ managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
};
From 35318a04a5e02dfa5a5b4f37cf7d857362ab9886 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Tue, 1 Sep 2026 23:39:55 -0700
Subject: [PATCH 16/49] test(mcp): exercise runtime detection in fixtures
Signed-off-by: Prekshi Vyas
---
.../mcp-bridge-adapter-deepagents-legacy.ts | 2 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 2 +-
.../sandbox/mcp-bridge-adapter-status.ts | 2 +-
.../mcp-bridge-adapter-deepagents-fixture.ts | 52 +++++++++++++------
4 files changed, 38 insertions(+), 20 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index f7d3e18acd6..bcce0026c77 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -48,7 +48,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ `runtime_kind = "auto"`,
"if runtime_kind == 'auto':",
" runtime_kind = 'unknown'",
" try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index 4ffe28abe34..94135102f84 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -42,7 +42,7 @@ export function buildDeepAgentsMcpRemoveCommand(
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "${adaptiveTeardown ? "auto" : "v2"}" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ `runtime_kind = "${adaptiveTeardown ? "auto" : "v2"}"`,
"if runtime_kind == 'auto':",
" runtime_kind = 'unknown'",
" try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index e46d13bbcde..3bca5c8c498 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -243,7 +243,7 @@ export function buildDeepAgentsMcpStatusCommand(
...DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ `runtime_kind = "auto"`,
"if runtime_kind == 'auto':",
" runtime_kind = 'unknown'",
" try:",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index ded631c8e90..29464d39306 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -95,23 +95,40 @@ function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSw
return undefined;
}
-function createManagedSwapPythonExecutable(
+function createDeepAgentsFixturePythonExecutable(
fixtureRoot: string,
configPath: string,
+ legacyConfigPath: string,
managedSymlinkTarget: string,
- managedSwap: ManagedSwap,
+ runtimeKind: "v2" | "legacy" | "unknown",
+ managedSwap?: ManagedSwap,
): { executablePath: string; scriptPath: string } {
const executablePath = path.join(fixtureRoot, "managed-swap-python");
const scriptPath = path.join(fixtureRoot, "managed-swap-wrapper.py");
+ const runtimeConfigPath =
+ runtimeKind === "v2"
+ ? configPath
+ : runtimeKind === "legacy"
+ ? legacyConfigPath
+ : path.join(fixtureRoot, "unknown-runtime-config.json");
+ const runtimePackagePath = path.join(fixtureRoot, "deepagents_code");
+ fs.mkdirSync(runtimePackagePath);
+ fs.writeFileSync(path.join(runtimePackagePath, "__init__.py"), "", { mode: 0o600 });
+ fs.writeFileSync(
+ path.join(runtimePackagePath, "_nemoclaw_managed.py"),
+ `_MCP_CONFIG_FILE = ${JSON.stringify(runtimeConfigPath)}\n`,
+ { mode: 0o600 },
+ );
const wrapper = [
"import os",
"import socket",
"import sys",
+ `fixture_root = ${JSON.stringify(fixtureRoot)}`,
`managed_path = ${JSON.stringify(configPath)}`,
`managed_target = ${JSON.stringify(managedSymlinkTarget)}`,
- `managed_swap_kind = ${JSON.stringify(managedSwap.kind)}`,
- `managed_swap_phase = ${JSON.stringify(managedSwap.phase)}`,
- `managed_swap_content = ${JSON.stringify(managedSwap.content ?? "")}`,
+ `managed_swap_kind = ${JSON.stringify(managedSwap?.kind ?? "")}`,
+ `managed_swap_phase = ${JSON.stringify(managedSwap?.phase ?? "")}`,
+ `managed_swap_content = ${JSON.stringify(managedSwap?.content ?? "")}`,
"managed_eloop_stage = 0",
"managed_socket = None",
"managed_swapped = False",
@@ -170,7 +187,9 @@ function createManagedSwapPythonExecutable(
" replace_managed_path()",
"source = sys.stdin.read()",
"namespace = {'__name__': '__main__'}",
- "sys.setprofile(managed_profile)",
+ "sys.path.insert(0, fixture_root)",
+ "if managed_swap_phase:",
+ " sys.setprofile(managed_profile)",
"try:",
" exec(compile(source, '', 'exec'), namespace)",
"finally:",
@@ -251,24 +270,23 @@ export function runDeepAgentsConfigCommand(
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
try {
- const managedSwapPython = managedSwap
- ? createManagedSwapPythonExecutable(tmp, configPath, managedSymlinkTarget, managedSwap)
- : null;
+ const fixturePython = createDeepAgentsFixturePythonExecutable(
+ tmp,
+ configPath,
+ legacyConfigPath,
+ managedSymlinkTarget,
+ runtimeKind,
+ managedSwap,
+ );
const fixtureCommand = command
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
- .replaceAll("/opt/venv/bin/python3", managedSwapPython?.executablePath ?? "python3")
- .replace(
- 'runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR',
- `runtime_kind = "${runtimeKind}" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
- );
+ .replaceAll("/opt/venv/bin/python3", fixturePython.executablePath);
const result = spawnSync("bash", ["-c", fixtureCommand], {
encoding: "utf-8",
env: {
...process.env,
- ...(managedSwapPython
- ? { DEEPAGENTS_FIXTURE_PYTHON_WRAPPER: managedSwapPython.scriptPath }
- : {}),
+ DEEPAGENTS_FIXTURE_PYTHON_WRAPPER: fixturePython.scriptPath,
},
timeout: managedOptions.timeoutMs ?? 5000,
});
From 825f6aae37d2c5363cc0faf2f8c1039aa7fd8535 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 00:02:41 -0700
Subject: [PATCH 17/49] test(mcp): isolate projection safety scenarios
Signed-off-by: Prekshi Vyas
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 4 +--
...idge-adapter-deepagents-projection.test.ts | 29 +++++++++++++++++--
2 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index ee2a0473450..16aab5bdf19 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -43,8 +43,8 @@ Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
The diagnostics are `Unsafe managed Deep Agents MCP projection path: symbolic link`, `Unsafe managed Deep Agents MCP projection path: FIFO`, and `Unsafe managed Deep Agents MCP projection path: non-regular file`.
-This exit status applies only to the unsafe path-type diagnostic.
-Other adapter inspection failures retain their ordinary status behavior.
+Other Deep Agents inspection failures can also exit with status `2`.
+Use the standard error diagnostic to distinguish an unsafe path from another inspection failure.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 173152e49cc..ecde1cfaec1 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -366,12 +366,17 @@ describe("Deep Agents managed MCP projection safety", () => {
it.each([
["registration", registrationCommand],
["v2 rollback", rollbackCommand],
- ])("rejects duplicate JSON and unsafe projection metadata during %s", (_name, command) => {
+ ])("rejects duplicate JSON during %s", (_name, command) => {
const duplicate = runDeepAgentsConfigCommand(command, duplicateProjection);
expect(duplicate.status).toBe(2);
expect(duplicate.stderr).toContain("duplicate JSON key: mcpServers");
expect(duplicate.configText).toBe(duplicateProjection);
+ });
+ it.each([
+ ["registration", registrationCommand],
+ ["v2 rollback", rollbackCommand],
+ ])("rejects an unsafe projection mode during %s", (_name, command) => {
const unsafeMode = runDeepAgentsConfigCommand(
command,
emptyProjection,
@@ -383,7 +388,12 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(unsafeMode.status).toBe(2);
expect(unsafeMode.stderr).toContain("unsafe ownership, mode, type, links, or path identity");
expect(unsafeMode.config).toEqual(emptyProjection);
+ });
+ it.each([
+ ["registration", registrationCommand],
+ ["v2 rollback", rollbackCommand],
+ ])("rejects a projection symlink during %s without mutating its target", (_name, command) => {
const symlink = runDeepAgentsConfigCommand(command, emptyProjection, "v2", undefined, 0o600, {
symlink: true,
});
@@ -391,7 +401,7 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
});
- it("never clobbers a projection that appears during absent publication or fd rewrite", () => {
+ it("preserves a projection that appears during absent publication", () => {
const absentResult = runDeepAgentsConfigCommand(
registrationCommand,
undefined,
@@ -403,7 +413,9 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(absentResult.status).toBe(2);
expect(absentResult.stderr).toContain("appeared during publication");
expect(absentResult.configText).toBe(attackerProjection);
+ });
+ it("preserves a replacement during a descriptor rewrite", () => {
const existingResult = runDeepAgentsConfigCommand(
registrationCommand,
emptyProjection,
@@ -433,7 +445,7 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(raced.configText).toBe(attackerProjection);
});
- it("keeps forced removal identity-bound for malformed and unsafe projections", () => {
+ it("preserves a projection symlink during forced removal", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedSymlink = runDeepAgentsConfigCommand(
forcedCommand,
@@ -449,7 +461,10 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(forcedSymlink.managedSymlinkTargetText).toBe(
`${JSON.stringify(emptyProjection, null, 2)}\n`,
);
+ });
+ it("preserves an unsafe projection mode during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedUnsafeMode = runDeepAgentsConfigCommand(
forcedCommand,
emptyProjection,
@@ -460,7 +475,10 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(forcedUnsafeMode.status).toBe(2);
expect(forcedUnsafeMode.config).toEqual(emptyProjection);
+ });
+ it("preserves a projection FIFO during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedFifo = runDeepAgentsConfigCommand(
forcedCommand,
undefined,
@@ -472,11 +490,16 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(forcedFifo.status).toBe(2);
expect(forcedFifo.configExists).toBe(true);
expect(forcedFifo.configIsFifo).toBe(true);
+ });
+ it("rejects duplicate projection JSON during ordinary removal", () => {
const duplicate = runDeepAgentsConfigCommand(removalCommand, duplicateProjection);
expect(duplicate.status).toBe(2);
expect(duplicate.configText).toBe(duplicateProjection);
+ });
+ it("repairs duplicate projection JSON during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedDuplicate = runDeepAgentsConfigCommand(forcedCommand, duplicateProjection);
expect(forcedDuplicate.status, forcedDuplicate.stderr).toBe(0);
expect(forcedDuplicate.config).toEqual(emptyProjection);
From 6429a92852eebc5b82eaa8ab60207aaf7a23bc26 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 00:36:14 -0700
Subject: [PATCH 18/49] test(mcp): prove symlink targets stay unread
Signed-off-by: Prekshi Vyas
---
...idge-adapter-deepagents-projection.test.ts | 27 +++++-
.../mcp-bridge-adapter-deepagents-fixture.ts | 97 ++++++++++++++++---
2 files changed, 106 insertions(+), 18 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index ecde1cfaec1..9350e67ab9c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -97,7 +97,7 @@ describe("Deep Agents managed MCP projection safety", () => {
options: { danglingSymlink: true },
},
])(
- "rejects an existing $caseName projection path without following it (#10754)",
+ "rejects an existing $caseName projection path (#10754)",
({
diagnostic,
expectedConfigIsDirectory,
@@ -127,6 +127,24 @@ describe("Deep Agents managed MCP projection safety", () => {
},
);
+ it("rejects a projection symlink without reading its target (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { symlink: true, targetReadProbe: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(result.managedSymlinkTargetExists).toBe(true);
+ expect(result.managedTargetReadAccessed).toBe(false);
+ });
+
it.each([
{
diagnostic: "symbolic link",
@@ -310,14 +328,13 @@ describe("Deep Agents managed MCP projection safety", () => {
);
it("rejects a symlinked projection parent without reading its target (#10754)", () => {
- const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
const status = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
+ undefined,
"v2",
undefined,
0o600,
- { parentSymlink: true },
+ { parentSymlink: true, targetReadProbe: true, timeoutMs: managedCommandTimeoutMs },
);
expect(status.status).toBe(2);
expect(status.stdout.trim()).toBe("");
@@ -325,7 +342,7 @@ describe("Deep Agents managed MCP projection safety", () => {
"Unsafe managed Deep Agents MCP projection path: symbolic link",
);
expect(status.managedParentIsSymlink).toBe(true);
- expect(status.managedParentTargetText).toBe(initialText);
+ expect(status.managedTargetReadAccessed).toBe(false);
});
it.each([
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 29464d39306..2971de0ec39 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import { spawnSync } from "node:child_process";
+import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -38,6 +38,7 @@ export interface DeepAgentsConfigCommandResult {
managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
+ managedTargetReadAccessed: boolean;
}
export interface DeepAgentsManagedFixtureOptions {
@@ -54,6 +55,7 @@ export interface DeepAgentsManagedFixtureOptions {
swapOnManagedRead?: "fifo" | "symlink";
swapOnManagedSeek?: string;
symlink?: boolean;
+ targetReadProbe?: boolean;
timeoutMs?: number;
}
@@ -95,6 +97,45 @@ function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSw
return undefined;
}
+function createFifo(target: string, mode = 0o600): void {
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ const fifo = spawnSync("mkfifo", [target], { encoding: "utf-8", timeout: 5000 });
+ if (fifo.status !== 0) throw new Error(fifo.stderr || "could not create managed fixture FIFO");
+ fs.chmodSync(target, mode);
+}
+
+function pause(milliseconds: number): void {
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
+}
+
+function waitForPath(target: string, timeoutMs: number): boolean {
+ const deadline = Date.now() + timeoutMs;
+ do {
+ if (fs.existsSync(target)) return true;
+ pause(10);
+ } while (Date.now() < deadline);
+ return fs.existsSync(target);
+}
+
+function startTargetReadProbe(target: string, ready: string, accessed: string) {
+ const script = [
+ "import os, sys",
+ "open(sys.argv[2], 'x').close()",
+ "descriptor = os.open(sys.argv[1], os.O_WRONLY)",
+ "os.close(descriptor)",
+ "open(sys.argv[3], 'x').close()",
+ ].join("\n");
+ const probe = spawn("python3", ["-I", "-c", script, target, ready, accessed], {
+ stdio: "ignore",
+ });
+ if (!waitForPath(ready, 1000)) {
+ probe.kill("SIGTERM");
+ throw new Error("managed target-read probe did not become ready");
+ }
+ pause(50);
+ return probe;
+}
+
function createDeepAgentsFixturePythonExecutable(
fixtureRoot: string,
configPath: string,
@@ -223,6 +264,8 @@ export function runDeepAgentsConfigCommand(
const managedParentTarget = path.join(tmp, "managed-parent-target");
const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
+ const managedTargetReadReady = path.join(tmp, "managed-target-read-ready");
+ const managedTargetReadAccess = path.join(tmp, "managed-target-read-accessed");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
const initializeConfig = (
target: string,
@@ -238,20 +281,33 @@ export function runDeepAgentsConfigCommand(
);
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
+ const managedTargetReadPath = managedOptions.targetReadProbe
+ ? managedOptions.parentSymlink
+ ? managedParentTargetConfig
+ : managedSymlink
+ ? managedSymlinkTarget
+ : null
+ : null;
+ if (managedOptions.targetReadProbe && managedTargetReadPath === null) {
+ throw new Error("targetReadProbe requires a projection or parent symlink");
+ }
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
if (managedOptions.parentSymlink) {
fs.mkdirSync(managedParentTarget, { recursive: true });
- initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
+ if (managedTargetReadPath === managedParentTargetConfig) {
+ createFifo(managedParentTargetConfig, managedOptions.mode);
+ } else {
+ initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
+ }
fs.symlinkSync(managedParentTarget, managedParentPath);
} else if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
- const fifo = spawnSync("mkfifo", [configPath], { encoding: "utf-8", timeout: 5000 });
- if (fifo.status !== 0) throw new Error(fifo.stderr || "could not create managed fixture FIFO");
- fs.chmodSync(configPath, managedOptions.mode ?? 0o600);
+ createFifo(configPath, managedOptions.mode);
} else {
- if (!managedOptions.danglingSymlink && managedSwap?.phase !== "after-missing-open") {
+ if (managedTargetReadPath === managedSymlinkTarget) {
+ createFifo(managedSymlinkTarget, managedOptions.mode);
+ } else if (!managedOptions.danglingSymlink && managedSwap?.phase !== "after-missing-open") {
initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
if (initialConfig !== undefined)
fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
@@ -269,7 +325,15 @@ export function runDeepAgentsConfigCommand(
}
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
+ let targetReadProbe: ReturnType | null = null;
try {
+ if (managedTargetReadPath !== null) {
+ targetReadProbe = startTargetReadProbe(
+ managedTargetReadPath,
+ managedTargetReadReady,
+ managedTargetReadAccess,
+ );
+ }
const fixturePython = createDeepAgentsFixturePythonExecutable(
tmp,
configPath,
@@ -313,12 +377,17 @@ export function runDeepAgentsConfigCommand(
? fs.readFileSync(configPath, "utf-8")
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
- const managedSymlinkTargetText = managedSymlinkTargetExists
- ? fs.readFileSync(managedSymlinkTarget, "utf-8")
- : null;
- const managedParentTargetText = fs.existsSync(managedParentTargetConfig)
- ? fs.readFileSync(managedParentTargetConfig, "utf-8")
- : null;
+ const readRegularFile = (target: string): string | null => {
+ try {
+ return fs.lstatSync(target).isFile() ? fs.readFileSync(target, "utf-8") : null;
+ } catch {
+ return null;
+ }
+ };
+ const managedSymlinkTargetText = readRegularFile(managedSymlinkTarget);
+ const managedParentTargetText = readRegularFile(managedParentTargetConfig);
+ const managedTargetReadAccessed =
+ managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
@@ -349,8 +418,10 @@ export function runDeepAgentsConfigCommand(
managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
+ managedTargetReadAccessed,
};
} finally {
+ targetReadProbe?.kill("SIGTERM");
fs.rmSync(tmp, { recursive: true, force: true });
}
}
From 66e0d23550ae1eac8492a43c5794587c6ce7a387 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 01:01:19 -0700
Subject: [PATCH 19/49] docs(mcp): include parent path diagnostic
Signed-off-by: Prekshi Vyas
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 16aab5bdf19..88b9211c356 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,7 +42,7 @@ For a trusted private server, status resolves the endpoint without changing mana
Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
-The diagnostics are `Unsafe managed Deep Agents MCP projection path: symbolic link`, `Unsafe managed Deep Agents MCP projection path: FIFO`, and `Unsafe managed Deep Agents MCP projection path: non-regular file`.
+The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, `non-regular file`, or `non-directory parent`.
Other Deep Agents inspection failures can also exit with status `2`.
Use the standard error diagnostic to distinguish an unsafe path from another inspection failure.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
From f2fe68d2377649db2dabbca391533f9e4474ecfd Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 01:10:23 -0700
Subject: [PATCH 20/49] test(mcp): target post-open projection swap
Signed-off-by: Prekshi Vyas
---
test/helpers/mcp-bridge-adapter-deepagents-fixture.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 2971de0ec39..9982848dc17 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -216,7 +216,7 @@ function createDeepAgentsFixturePythonExecutable(
" return",
" if managed_swap_phase == 'before-open' and event == 'c_call' and target is os.open:",
" replace_managed_path()",
- " elif managed_swap_phase == 'after-open' and event == 'c_return' and target is os.open:",
+ " elif managed_swap_phase == 'after-open' and event == 'c_return' and target is os.open and _frame.f_code.co_name == 'open_managed_projection':",
" replace_managed_path()",
" elif managed_swap_phase == 'after-missing-open' and event == 'c_exception' and target is os.open:",
" replace_managed_path()",
From 8373ae12be547ca16d50875f1b8b49f7b9678b88 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 01:25:29 -0700
Subject: [PATCH 21/49] test(mcp): use behavior-named race boundaries
Signed-off-by: Prekshi Vyas
---
...idge-adapter-deepagents-projection.test.ts | 2 +-
.../mcp-bridge-adapter-deepagents-fixture.ts | 127 ++++++++++++------
2 files changed, 89 insertions(+), 40 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 9350e67ab9c..76b9435c0cf 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -269,7 +269,7 @@ describe("Deep Agents managed MCP projection safety", () => {
undefined,
0o600,
{
- statAfterManagedEloopAsRegular: true,
+ swapAfterManagedEloop: true,
symlink: true,
timeoutMs: managedCommandTimeoutMs,
},
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 9982848dc17..926361b55e2 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -47,7 +47,7 @@ export interface DeepAgentsManagedFixtureOptions {
fifo?: boolean;
mode?: number;
parentSymlink?: boolean;
- statAfterManagedEloopAsRegular?: boolean;
+ swapAfterManagedEloop?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
swapBeforeManagedLink?: string;
@@ -63,36 +63,47 @@ type ManagedSwap = {
content?: string;
kind: "fifo" | "regular" | "socket" | "symlink";
phase:
- | "after-missing-open"
- | "after-open"
- | "before-link"
- | "before-open"
- | "eloop-regular-stat"
- | "read"
- | "seek";
+ | "after-missing-projection-open"
+ | "after-projection-eloop"
+ | "after-projection-open"
+ | "before-projection-open"
+ | "projection-publication-begins"
+ | "projection-read-begins"
+ | "projection-rewrite-begins";
};
function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSwap | undefined {
if (options.swapOnManagedOpen) {
- return { kind: options.swapOnManagedOpen, phase: "before-open" };
+ return { kind: options.swapOnManagedOpen, phase: "before-projection-open" };
}
if (options.swapAfterManagedOpen) {
- return { kind: options.swapAfterManagedOpen, phase: "after-open" };
+ return { kind: options.swapAfterManagedOpen, phase: "after-projection-open" };
}
if (options.swapAfterMissingManagedOpen) {
- return { kind: options.swapAfterMissingManagedOpen, phase: "after-missing-open" };
+ return {
+ kind: options.swapAfterMissingManagedOpen,
+ phase: "after-missing-projection-open",
+ };
}
if (options.swapOnManagedRead) {
- return { kind: options.swapOnManagedRead, phase: "read" };
+ return { kind: options.swapOnManagedRead, phase: "projection-read-begins" };
}
- if (options.statAfterManagedEloopAsRegular) {
- return { kind: "symlink", phase: "eloop-regular-stat" };
+ if (options.swapAfterManagedEloop) {
+ return { kind: "symlink", phase: "after-projection-eloop" };
}
if (options.swapBeforeManagedLink !== undefined) {
- return { content: options.swapBeforeManagedLink, kind: "regular", phase: "before-link" };
+ return {
+ content: options.swapBeforeManagedLink,
+ kind: "regular",
+ phase: "projection-publication-begins",
+ };
}
if (options.swapOnManagedSeek !== undefined) {
- return { content: options.swapOnManagedSeek, kind: "regular", phase: "seek" };
+ return {
+ content: options.swapOnManagedSeek,
+ kind: "regular",
+ phase: "projection-rewrite-begins",
+ };
}
return undefined;
}
@@ -161,6 +172,7 @@ function createDeepAgentsFixturePythonExecutable(
{ mode: 0o600 },
);
const wrapper = [
+ "import errno",
"import os",
"import socket",
"import sys",
@@ -173,6 +185,12 @@ function createDeepAgentsFixturePythonExecutable(
"managed_eloop_stage = 0",
"managed_socket = None",
"managed_swapped = False",
+ "managed_descriptor = None",
+ "managed_entry_name = os.path.basename(managed_path)",
+ "managed_real_link = os.link",
+ "managed_real_lseek = os.lseek",
+ "managed_real_open = os.open",
+ "managed_real_read = os.read",
"def replace_managed_path():",
" global managed_socket, managed_swapped",
" if managed_swapped:",
@@ -205,37 +223,65 @@ function createDeepAgentsFixturePythonExecutable(
" os.unlink(managed_path)",
" os.symlink(managed_target, managed_path)",
" managed_eloop_stage = 2",
- "def managed_profile(_frame, event, target):",
- " if managed_swap_phase == 'eloop-regular-stat':",
- " if managed_eloop_stage == 0 and event == 'c_exception' and target is os.open:",
- " replace_managed_symlink_with_regular()",
- " elif managed_eloop_stage == 1 and event == 'c_return' and target is os.stat:",
- " restore_managed_symlink()",
- " return",
- " if managed_swapped:",
- " return",
- " if managed_swap_phase == 'before-open' and event == 'c_call' and target is os.open:",
- " replace_managed_path()",
- " elif managed_swap_phase == 'after-open' and event == 'c_return' and target is os.open and _frame.f_code.co_name == 'open_managed_projection':",
- " replace_managed_path()",
- " elif managed_swap_phase == 'after-missing-open' and event == 'c_exception' and target is os.open:",
+ "def is_managed_entry(value, directory_descriptor):",
+ " try:",
+ " return directory_descriptor is not None and os.fspath(value) == managed_entry_name",
+ " except TypeError:",
+ " return False",
+ "def managed_boundary_open(value, flags, mode=0o777, *, dir_fd=None):",
+ " global managed_descriptor",
+ " projection_entry = is_managed_entry(value, dir_fd)",
+ " if projection_entry and managed_swap_phase == 'before-projection-open':",
" replace_managed_path()",
- " elif managed_swap_phase == 'read' and event == 'c_call' and target is os.read:",
+ " try:",
+ " if dir_fd is None:",
+ " descriptor = managed_real_open(value, flags, mode)",
+ " else:",
+ " descriptor = managed_real_open(value, flags, mode, dir_fd=dir_fd)",
+ " except OSError as exc:",
+ " if projection_entry and managed_swap_phase == 'after-missing-projection-open' and isinstance(exc, FileNotFoundError):",
+ " replace_managed_path()",
+ " elif projection_entry and managed_swap_phase == 'after-projection-eloop' and exc.errno == errno.ELOOP:",
+ " replace_managed_symlink_with_regular()",
+ " raise",
+ " if projection_entry:",
+ " managed_descriptor = descriptor",
+ " if managed_swap_phase == 'after-projection-open':",
+ " replace_managed_path()",
+ " return descriptor",
+ "def managed_boundary_read(descriptor, length):",
+ " if managed_swap_phase == 'projection-read-begins' and descriptor == managed_descriptor:",
" replace_managed_path()",
- " elif managed_swap_phase == 'before-link' and event == 'c_call' and target is os.link:",
+ " return managed_real_read(descriptor, length)",
+ "def managed_boundary_link(source, destination, *args, **kwargs):",
+ " if managed_swap_phase == 'projection-publication-begins' and is_managed_entry(destination, kwargs.get('dst_dir_fd')):",
" replace_managed_path()",
- " elif managed_swap_phase == 'seek' and event == 'c_call' and target is os.lseek:",
+ " return managed_real_link(source, destination, *args, **kwargs)",
+ "def managed_boundary_lseek(descriptor, position, how):",
+ " if managed_swap_phase == 'projection-rewrite-begins' and descriptor == managed_descriptor:",
" replace_managed_path()",
+ " return managed_real_lseek(descriptor, position, how)",
+ "def install_managed_boundaries():",
+ " if not managed_swap_phase:",
+ " return",
+ " os.link = managed_boundary_link",
+ " os.lseek = managed_boundary_lseek",
+ " os.open = managed_boundary_open",
+ " os.read = managed_boundary_read",
+ "def restore_managed_boundaries():",
+ " os.link = managed_real_link",
+ " os.lseek = managed_real_lseek",
+ " os.open = managed_real_open",
+ " os.read = managed_real_read",
"source = sys.stdin.read()",
"namespace = {'__name__': '__main__'}",
"sys.path.insert(0, fixture_root)",
- "if managed_swap_phase:",
- " sys.setprofile(managed_profile)",
+ "install_managed_boundaries()",
"try:",
" exec(compile(source, '', 'exec'), namespace)",
"finally:",
- " sys.setprofile(None)",
- " if managed_swap_phase == 'eloop-regular-stat' and managed_eloop_stage == 1:",
+ " restore_managed_boundaries()",
+ " if managed_swap_phase == 'after-projection-eloop' and managed_eloop_stage == 1:",
" restore_managed_symlink()",
"",
].join("\n");
@@ -307,12 +353,15 @@ export function runDeepAgentsConfigCommand(
} else {
if (managedTargetReadPath === managedSymlinkTarget) {
createFifo(managedSymlinkTarget, managedOptions.mode);
- } else if (!managedOptions.danglingSymlink && managedSwap?.phase !== "after-missing-open") {
+ } else if (
+ !managedOptions.danglingSymlink &&
+ managedSwap?.phase !== "after-missing-projection-open"
+ ) {
initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
if (initialConfig !== undefined)
fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
}
- if (managedSwap?.phase === "after-missing-open") {
+ if (managedSwap?.phase === "after-missing-projection-open") {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
}
if (managedSymlink) {
From fa29f6c080d41e5d740724a6348aba12701c6173 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 01:33:59 -0700
Subject: [PATCH 22/49] test(mcp): harden projection fixture reads
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
.../mcp-bridge-adapter-deepagents-fixture.ts | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 926361b55e2..f5937ef469b 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -395,7 +395,9 @@ export function runDeepAgentsConfigCommand(
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
.replaceAll("/opt/venv/bin/python3", fixturePython.executablePath);
- const result = spawnSync("bash", ["-c", fixtureCommand], {
+ const fixtureCommandPath = path.join(tmp, "managed-command.sh");
+ fs.writeFileSync(fixtureCommandPath, fixtureCommand, { mode: 0o600 });
+ const result = spawnSync("bash", [fixtureCommandPath], {
encoding: "utf-8",
env: {
...process.env,
@@ -427,10 +429,18 @@ export function runDeepAgentsConfigCommand(
: null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
const readRegularFile = (target: string): string | null => {
+ let descriptor: number | null = null;
try {
- return fs.lstatSync(target).isFile() ? fs.readFileSync(target, "utf-8") : null;
+ if (!fs.lstatSync(target).isFile()) return null;
+ descriptor = fs.openSync(
+ target,
+ fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
+ );
+ return fs.fstatSync(descriptor).isFile() ? fs.readFileSync(descriptor, "utf-8") : null;
} catch {
return null;
+ } finally {
+ if (descriptor !== null) fs.closeSync(descriptor);
}
};
const managedSymlinkTargetText = readRegularFile(managedSymlinkTarget);
From 524d337e77842a417b96c1907da85165cc4c447f Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 01:55:58 -0700
Subject: [PATCH 23/49] fix(mcp): remove projection parent expansion
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 5 +-
.../mcp-bridge-adapter-deepagents-legacy.ts | 10 +-
...idge-adapter-deepagents-projection.test.ts | 101 ++-----
...cp-bridge-adapter-deepagents-projection.ts | 286 ++++++------------
...-bridge-adapter-deepagents-registration.ts | 10 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 22 +-
.../mcp-bridge-status-resolution.test.ts | 36 ++-
.../mcp-bridge-adapter-deepagents-fixture.ts | 40 +--
8 files changed, 176 insertions(+), 334 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 88b9211c356..e7a43e373f3 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,9 +42,8 @@ For a trusted private server, status resolves the endpoint without changing mana
Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
-The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, `non-regular file`, or `non-directory parent`.
-Other Deep Agents inspection failures can also exit with status `2`.
-Use the standard error diagnostic to distinguish an unsafe path from another inspection failure.
+The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
+Exit status `2` is specific to this unsafe path-type result.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index bcce0026c77..4c2bd48ac55 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -71,16 +71,14 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
+ " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
" elif os.path.lexists(config_path):",
" data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
@@ -110,8 +108,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
- " config_path.parent.mkdir(parents=True, exist_ok=True)",
" tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
" os.fchmod(tmp_fd, 0o600)",
@@ -133,7 +131,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 76b9435c0cf..f0a4ad5d8c5 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -24,36 +24,27 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it(
- "applies the shared server cap before normal and rollback v2 publication",
- { timeout: 60_000 },
- () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it("applies the shared server cap before normal and rollback v2 publication", () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- undefined,
- "v2",
- undefined,
- 0o600,
- { timeoutMs: managedCommandTimeoutMs },
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- },
- );
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ });
it.each([
{
@@ -300,13 +291,7 @@ describe("Deep Agents managed MCP projection safety", () => {
},
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({
- appearedType,
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSymlink,
- expectedTargetText,
- }) => {
+ ({ appearedType, diagnostic }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -321,48 +306,10 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(result.configIsFifo).toBe(expectedConfigIsFifo);
- expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
- expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
- },
- );
-
- it("rejects a symlinked projection parent without reading its target (#10754)", () => {
- const status = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- undefined,
- "v2",
- undefined,
- 0o600,
- { parentSymlink: true, targetReadProbe: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(status.status).toBe(2);
- expect(status.stdout.trim()).toBe("");
- expect(status.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(status.managedParentIsSymlink).toBe(true);
- expect(status.managedTargetReadAccessed).toBe(false);
- });
-
- it.each([
- ["registration", registrationCommand],
- ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
- ])(
- "rejects a symlinked projection parent during %s without mutating its target (#10754)",
- (_operation, command) => {
- const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
- const mutation = runDeepAgentsConfigCommand(
- command,
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { parentSymlink: true },
+ expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(
+ appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
);
- expect(mutation.status).toBe(2);
- expect(mutation.managedParentIsSymlink).toBe(true);
- expect(mutation.managedParentTargetText).toBe(initialText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index c21db5ed120..d484ec70a7c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,76 +28,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
- "def managed_parent_path_kind(mode):",
- " if stat.S_ISLNK(mode):",
- " return 'symbolic link'",
- " if not stat.S_ISDIR(mode):",
- " return 'non-directory parent'",
- " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def managed_parent_fingerprint(metadata):",
- " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
- "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
- " unsafe_path_kind = None",
- " try:",
- " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind and unsafe_path_prefix:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " if unsafe_path_kind:",
- " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
- " raise exc",
- "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
- " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
- " raise ValueError('managed MCP projection path must be absolute')",
- " components = path.parts[1:-1]",
- " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
- " created = False",
- " try:",
- " for index, component in enumerate(components):",
- " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except FileNotFoundError:",
- " if not create or index != len(components) - 1:",
- " raise",
- " try:",
- " os.mkdir(component, 0o700, dir_fd=descriptor)",
- " created = True",
- " except FileExistsError:",
- " pass",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " os.close(descriptor)",
- " descriptor = next_descriptor",
- " metadata = os.fstat(descriptor)",
- " if not stat.S_ISDIR(metadata.st_mode):",
- " raise ValueError('managed MCP projection parent is not a directory')",
- " return descriptor, managed_parent_fingerprint(metadata), created",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
- "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
+ "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " current = None",
- " descriptor = None",
- " try:",
- " if current != identity:",
- " raise ValueError('managed MCP projection parent changed during operation')",
- " finally:",
- " if descriptor is not None:",
- " os.close(descriptor)",
- "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
- " try:",
- " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " current_metadata = os.stat(path, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -110,9 +45,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " linked = os.stat(path, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -122,90 +57,78 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " try:",
- " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " return b'', None, None, None, None",
- " entry_name = path.name",
" access = os.O_RDWR if writable else os.O_RDONLY",
" flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " try:",
- " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b'', None, None, parent_descriptor, parent_identity",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
+ " descriptor = os.open(path, flags)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(path, None, unsafe_path_prefix)",
+ " return b'', None, None",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
- " try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(path, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor",
" except Exception:",
- " os.close(parent_descriptor)",
+ " os.close(descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
- " for current in (descriptor, parent_descriptor):",
- " if current is None:",
- " continue",
- " try:",
- " os.close(current)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptor(descriptor):",
+ " if descriptor is None:",
+ " return",
+ " try:",
+ " os.close(descriptor)",
+ " except OSError:",
+ " pass",
"def load_managed_projection_for_update(path):",
- " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
+ " raw, identity, descriptor = open_managed_projection(path, True)",
" try:",
- " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
+ " return decode_managed_projection(raw), identity, descriptor",
" except Exception:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
" raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
@@ -214,11 +137,10 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor):",
+ " if identity is None or descriptor is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, identity)",
+ " assert_managed_source_stable(path, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -233,62 +155,40 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
- " assert_managed_parent_stable(path, parent_identity)",
- "def create_managed_projection_temp(parent_descriptor):",
- " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
- " for _ in range(16):",
- " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
+ " validate_managed_descriptor_path(path, descriptor)",
+ "def publish_absent_managed_projection(path, value):",
+ " assert_managed_source_stable(path, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
+ " try:",
+ " os.fchmod(tmp_fd, 0o600)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
" try:",
- " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
- " except FileExistsError:",
+ " os.link(tmp_name, path, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name)",
+ " finally:",
+ " try:",
+ " os.unlink(tmp_name)",
+ " except FileNotFoundError:",
" pass",
- " raise FileExistsError('could not allocate managed MCP projection temporary file')",
- "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
- " owns_parent_descriptor = parent_descriptor is None",
- " if parent_descriptor is None:",
- " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
- " if not created:",
- " os.close(parent_descriptor)",
- " raise ValueError('managed MCP projection parent appeared during publication')",
- " try:",
- " if not owns_parent_descriptor:",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ "def write_managed_projection(path, value, identity, descriptor):",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value)",
+ " else:",
" try:",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
- " try:",
- " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " rewrite_managed_projection(path, value, identity, descriptor)",
" finally:",
- " try:",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " pass",
- " assert_managed_parent_stable(path, parent_identity)",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- " finally:",
- " if owns_parent_descriptor:",
- " os.close(parent_descriptor)",
- "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " try:",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
- " else:",
- " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
- " finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 5657a097489..0b9365de1b0 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,20 +52,19 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys",
+ "import json, os, pathlib, stat, sys, tempfile",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
- "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
+ " close_managed_projection_descriptor(source_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -96,8 +95,9 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index 94135102f84..2df3550c3b1 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -61,20 +61,18 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
"def finish(outcome):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
+ "def repair_v2_projection(identity, descriptor):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
"def write_legacy_data(value):",
@@ -99,7 +97,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -108,7 +106,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
@@ -120,7 +118,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('unowned')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -130,7 +128,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -140,7 +138,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -160,7 +158,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
" write_legacy_data(data)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index e4b521a09fa..3582140ab2c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,6 +7,12 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
+import {
+ baseEntry,
+ runDeepAgentsConfigCommand,
+} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
+
+import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -199,13 +205,31 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
- { timeout: 30_000 },
+ "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
+ { timeout: 120_000 },
() => {
- const unsafeDetails = [
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- "Unsafe managed Deep Agents MCP projection path: FIFO",
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const projection = { mcpServers: {} };
+ const unsafeResults = [
+ ...(["symlink", "fifo"] as const).map((appearedType) =>
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterMissingManagedOpen: appearedType,
+ timeoutMs: 30_000,
+ }),
+ ),
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterManagedEloop: true,
+ symlink: true,
+ timeoutMs: 30_000,
+ }),
];
+ unsafeResults.forEach((result) => {
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
+ expect(result.configIsSymlink || result.configIsFifo).toBe(true);
+ });
+ const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -257,7 +281,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(6);
+ expect(outcomes).toHaveLength(9);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 7fcdaa16fa7..3b1d9aa4076 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -34,8 +34,6 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
- managedParentIsSymlink: boolean;
- managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
managedTargetReadAccessed: boolean;
@@ -46,7 +44,6 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
- parentSymlink?: boolean;
swapAfterManagedEloop?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -225,7 +222,8 @@ function createDeepAgentsFixturePythonExecutable(
" managed_eloop_stage = 2",
"def is_managed_entry(value, directory_descriptor):",
" try:",
- " return directory_descriptor is not None and os.fspath(value) == managed_entry_name",
+ " candidate = os.fspath(value)",
+ " return candidate == managed_path if directory_descriptor is None else candidate == managed_entry_name",
" except TypeError:",
" return False",
"def managed_boundary_open(value, flags, mode=0o777, *, dir_fd=None):",
@@ -305,11 +303,8 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
- const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
+ const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
- const managedParentPath = path.dirname(configPath);
- const managedParentTarget = path.join(tmp, "managed-parent-target");
- const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const managedTargetReadReady = path.join(tmp, "managed-target-read-ready");
const managedTargetReadAccess = path.join(tmp, "managed-target-read-accessed");
@@ -329,25 +324,15 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedTargetReadPath = managedOptions.targetReadProbe
- ? managedOptions.parentSymlink
- ? managedParentTargetConfig
- : managedSymlink
- ? managedSymlinkTarget
- : null
+ ? managedSymlink
+ ? managedSymlinkTarget
+ : null
: null;
if (managedOptions.targetReadProbe && managedTargetReadPath === null) {
- throw new Error("targetReadProbe requires a projection or parent symlink");
+ throw new Error("targetReadProbe requires a projection symlink");
}
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.parentSymlink) {
- fs.mkdirSync(managedParentTarget, { recursive: true });
- if (managedTargetReadPath === managedParentTargetConfig) {
- createFifo(managedParentTargetConfig, managedOptions.mode);
- } else {
- initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
- }
- fs.symlinkSync(managedParentTarget, managedParentPath);
- } else if (managedOptions.directory) {
+ if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
createFifo(configPath, managedOptions.mode);
@@ -420,12 +405,6 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
- let managedParentIsSymlink = false;
- try {
- managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
- } catch {
- // A missing projection parent cannot redirect the managed path.
- }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
@@ -452,7 +431,6 @@ export function runDeepAgentsConfigCommand(
}
};
const managedSymlinkTargetText = readRegularFile(managedSymlinkTarget);
- const managedParentTargetText = readRegularFile(managedParentTargetConfig);
const managedTargetReadAccessed =
managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
@@ -481,8 +459,6 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
- managedParentIsSymlink,
- managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
managedTargetReadAccessed,
From bd6bd83f461a829140c2c13e65ecf05224d5aecb Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 02:01:05 -0700
Subject: [PATCH 24/49] fix(mcp): retain descriptor-bound projection parents
Signed-off-by: Prekshi Vyas
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 5 +-
.../mcp-bridge-adapter-deepagents-legacy.ts | 10 +-
...idge-adapter-deepagents-projection.test.ts | 101 +++++--
...cp-bridge-adapter-deepagents-projection.ts | 286 ++++++++++++------
...-bridge-adapter-deepagents-registration.ts | 10 +-
.../mcp-bridge-adapter-deepagents-teardown.ts | 22 +-
.../mcp-bridge-status-resolution.test.ts | 36 +--
.../mcp-bridge-adapter-deepagents-fixture.ts | 40 ++-
8 files changed, 334 insertions(+), 176 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index e7a43e373f3..88b9211c356 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,8 +42,9 @@ For a trusted private server, status resolves the endpoint without changing mana
Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
-The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
-Exit status `2` is specific to this unsafe path-type result.
+The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, `non-regular file`, or `non-directory parent`.
+Other Deep Agents inspection failures can also exit with status `2`.
+Use the standard error diagnostic to distinguish an unsafe path from another inspection failure.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index 4c2bd48ac55..bcce0026c77 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -71,14 +71,16 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
+ " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
" elif os.path.lexists(config_path):",
" data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
@@ -108,8 +110,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
+ " config_path.parent.mkdir(parents=True, exist_ok=True)",
" tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
" os.fchmod(tmp_fd, 0o600)",
@@ -131,7 +133,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"try:",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index f0a4ad5d8c5..76b9435c0cf 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -24,27 +24,36 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it("applies the shared server cap before normal and rollback v2 publication", () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it(
+ "applies the shared server cap before normal and rollback v2 publication",
+ { timeout: 60_000 },
+ () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- });
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ },
+ );
it.each([
{
@@ -291,7 +300,13 @@ describe("Deep Agents managed MCP projection safety", () => {
},
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({ appearedType, diagnostic }) => {
+ ({
+ appearedType,
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -306,10 +321,48 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(
- appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ it("rejects a symlinked projection parent without reading its target (#10754)", () => {
+ const status = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true, targetReadProbe: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(status.status).toBe(2);
+ expect(status.stdout.trim()).toBe("");
+ expect(status.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(status.managedParentIsSymlink).toBe(true);
+ expect(status.managedTargetReadAccessed).toBe(false);
+ });
+
+ it.each([
+ ["registration", registrationCommand],
+ ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
+ ])(
+ "rejects a symlinked projection parent during %s without mutating its target (#10754)",
+ (_operation, command) => {
+ const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
+ const mutation = runDeepAgentsConfigCommand(
+ command,
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { parentSymlink: true },
);
+ expect(mutation.status).toBe(2);
+ expect(mutation.managedParentIsSymlink).toBe(true);
+ expect(mutation.managedParentTargetText).toBe(initialText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index d484ec70a7c..c21db5ed120 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,11 +28,76 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
+ "def managed_parent_path_kind(mode):",
+ " if stat.S_ISLNK(mode):",
+ " return 'symbolic link'",
+ " if not stat.S_ISDIR(mode):",
+ " return 'non-directory parent'",
+ " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
+ "def managed_parent_fingerprint(metadata):",
+ " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
+ "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
+ " unsafe_path_kind = None",
+ " try:",
+ " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind and unsafe_path_prefix:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
+ " raise exc",
+ "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
+ " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
+ " raise ValueError('managed MCP projection path must be absolute')",
+ " components = path.parts[1:-1]",
+ " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
+ " created = False",
+ " try:",
+ " for index, component in enumerate(components):",
+ " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except FileNotFoundError:",
+ " if not create or index != len(components) - 1:",
+ " raise",
+ " try:",
+ " os.mkdir(component, 0o700, dir_fd=descriptor)",
+ " created = True",
+ " except FileExistsError:",
+ " pass",
+ " try:",
+ " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " except OSError as exc:",
+ " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
+ " os.close(descriptor)",
+ " descriptor = next_descriptor",
+ " metadata = os.fstat(descriptor)",
+ " if not stat.S_ISDIR(metadata.st_mode):",
+ " raise ValueError('managed MCP projection parent is not a directory')",
+ " return descriptor, managed_parent_fingerprint(metadata), created",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
+ "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " current_metadata = os.stat(path, follow_symlinks=False)",
+ " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " except FileNotFoundError:",
+ " current = None",
+ " descriptor = None",
+ " try:",
+ " if current != identity:",
+ " raise ValueError('managed MCP projection parent changed during operation')",
+ " finally:",
+ " if descriptor is not None:",
+ " os.close(descriptor)",
+ "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
+ " try:",
+ " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -45,9 +110,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(path, follow_symlinks=False)",
+ " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -57,78 +122,90 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " access = os.O_RDWR if writable else os.O_RDONLY",
- " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " descriptor = os.open(path, flags)",
+ " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
" except FileNotFoundError:",
- " assert_managed_source_stable(path, None, unsafe_path_prefix)",
- " return b'', None, None",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
+ " return b'', None, None, None, None",
+ " entry_name = path.name",
+ " access = os.O_RDWR if writable else os.O_RDONLY",
+ " flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(path, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " try:",
+ " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b'', None, None, parent_descriptor, parent_identity",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " return b''.join(chunks), managed_fingerprint(after), descriptor",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
" except Exception:",
- " os.close(descriptor)",
+ " os.close(parent_descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptor(descriptor):",
- " if descriptor is None:",
- " return",
- " try:",
- " os.close(descriptor)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
+ " for current in (descriptor, parent_descriptor):",
+ " if current is None:",
+ " continue",
+ " try:",
+ " os.close(current)",
+ " except OSError:",
+ " pass",
"def load_managed_projection_for_update(path):",
- " raw, identity, descriptor = open_managed_projection(path, True)",
+ " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
" try:",
- " return decode_managed_projection(raw), identity, descriptor",
+ " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
" except Exception:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
" raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
@@ -137,10 +214,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor):",
- " if identity is None or descriptor is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_source_stable(path, identity)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -155,40 +233,62 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path, descriptor)",
- "def publish_absent_managed_projection(path, value):",
- " assert_managed_source_stable(path, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
- " try:",
- " os.fchmod(tmp_fd, 0o600)",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
+ " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
+ " assert_managed_parent_stable(path, parent_identity)",
+ "def create_managed_projection_temp(parent_descriptor):",
+ " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " for _ in range(16):",
+ " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
" try:",
- " os.link(tmp_name, path, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name)",
- " finally:",
- " try:",
- " os.unlink(tmp_name)",
- " except FileNotFoundError:",
+ " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
+ " except FileExistsError:",
" pass",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- "def write_managed_projection(path, value, identity, descriptor):",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value)",
- " else:",
+ " raise FileExistsError('could not allocate managed MCP projection temporary file')",
+ "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
+ " owns_parent_descriptor = parent_descriptor is None",
+ " if parent_descriptor is None:",
+ " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
+ " if not created:",
+ " os.close(parent_descriptor)",
+ " raise ValueError('managed MCP projection parent appeared during publication')",
+ " try:",
+ " if not owns_parent_descriptor:",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " assert_managed_source_stable(path.name, parent_descriptor, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
" try:",
- " rewrite_managed_projection(path, value, identity, descriptor)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " try:",
+ " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
" finally:",
- " close_managed_projection_descriptor(descriptor)",
+ " try:",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " pass",
+ " assert_managed_parent_stable(path, parent_identity)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ " finally:",
+ " if owns_parent_descriptor:",
+ " os.close(parent_descriptor)",
+ "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " try:",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
+ " else:",
+ " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
+ " finally:",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 0b9365de1b0..5657a097489 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,19 +52,20 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys, tempfile",
+ "import json, os, pathlib, stat, sys",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
+ "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptor(source_descriptor)",
+ " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -95,9 +96,8 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
- "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index 2df3550c3b1..94135102f84 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -61,18 +61,20 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
+ "managed_parent_identity = None",
+ "managed_parent_descriptor = None",
"legacy_identity = None",
"def finish(outcome):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptor(managed_descriptor)",
+ " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor):",
+ "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
"def write_legacy_data(value):",
@@ -97,7 +99,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -106,7 +108,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
@@ -118,7 +120,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('unowned')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -128,7 +130,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -138,7 +140,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor)",
+ " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -158,7 +160,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
" write_legacy_data(data)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 3582140ab2c..e4b521a09fa 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,12 +7,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
-import {
- baseEntry,
- runDeepAgentsConfigCommand,
-} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
-
-import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -205,31 +199,13 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
- { timeout: 120_000 },
+ "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
+ { timeout: 30_000 },
() => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const projection = { mcpServers: {} };
- const unsafeResults = [
- ...(["symlink", "fifo"] as const).map((appearedType) =>
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterMissingManagedOpen: appearedType,
- timeoutMs: 30_000,
- }),
- ),
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterManagedEloop: true,
- symlink: true,
- timeoutMs: 30_000,
- }),
+ const unsafeDetails = [
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ "Unsafe managed Deep Agents MCP projection path: FIFO",
];
- unsafeResults.forEach((result) => {
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
- expect(result.configIsSymlink || result.configIsFifo).toBe(true);
- });
- const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -281,7 +257,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(9);
+ expect(outcomes).toHaveLength(6);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 3b1d9aa4076..7fcdaa16fa7 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -34,6 +34,8 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
+ managedParentIsSymlink: boolean;
+ managedParentTargetText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
managedTargetReadAccessed: boolean;
@@ -44,6 +46,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ parentSymlink?: boolean;
swapAfterManagedEloop?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -222,8 +225,7 @@ function createDeepAgentsFixturePythonExecutable(
" managed_eloop_stage = 2",
"def is_managed_entry(value, directory_descriptor):",
" try:",
- " candidate = os.fspath(value)",
- " return candidate == managed_path if directory_descriptor is None else candidate == managed_entry_name",
+ " return directory_descriptor is not None and os.fspath(value) == managed_entry_name",
" except TypeError:",
" return False",
"def managed_boundary_open(value, flags, mode=0o777, *, dir_fd=None):",
@@ -303,8 +305,11 @@ export function runDeepAgentsConfigCommand(
): DeepAgentsConfigCommandResult {
const managedSwap = resolveManagedSwap(managedOptions);
const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
- const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
+ const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
+ const managedParentPath = path.dirname(configPath);
+ const managedParentTarget = path.join(tmp, "managed-parent-target");
+ const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const managedTargetReadReady = path.join(tmp, "managed-target-read-ready");
const managedTargetReadAccess = path.join(tmp, "managed-target-read-accessed");
@@ -324,15 +329,25 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
const managedTargetReadPath = managedOptions.targetReadProbe
- ? managedSymlink
- ? managedSymlinkTarget
- : null
+ ? managedOptions.parentSymlink
+ ? managedParentTargetConfig
+ : managedSymlink
+ ? managedSymlinkTarget
+ : null
: null;
if (managedOptions.targetReadProbe && managedTargetReadPath === null) {
- throw new Error("targetReadProbe requires a projection symlink");
+ throw new Error("targetReadProbe requires a projection or parent symlink");
}
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.directory) {
+ if (managedOptions.parentSymlink) {
+ fs.mkdirSync(managedParentTarget, { recursive: true });
+ if (managedTargetReadPath === managedParentTargetConfig) {
+ createFifo(managedParentTargetConfig, managedOptions.mode);
+ } else {
+ initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
+ }
+ fs.symlinkSync(managedParentTarget, managedParentPath);
+ } else if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
createFifo(configPath, managedOptions.mode);
@@ -405,6 +420,12 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
+ let managedParentIsSymlink = false;
+ try {
+ managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
+ } catch {
+ // A missing projection parent cannot redirect the managed path.
+ }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
@@ -431,6 +452,7 @@ export function runDeepAgentsConfigCommand(
}
};
const managedSymlinkTargetText = readRegularFile(managedSymlinkTarget);
+ const managedParentTargetText = readRegularFile(managedParentTargetConfig);
const managedTargetReadAccessed =
managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
@@ -459,6 +481,8 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
+ managedParentIsSymlink,
+ managedParentTargetText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
managedTargetReadAccessed,
From 068ab29e2763e048e4d2419aa23797bfb77629ac Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 02:18:06 -0700
Subject: [PATCH 25/49] test(mcp): read fixture targets from descriptors
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
test/helpers/mcp-bridge-adapter-deepagents-fixture.ts | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 7fcdaa16fa7..57dfa7a40e1 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -439,7 +439,6 @@ export function runDeepAgentsConfigCommand(
const readRegularFile = (target: string): string | null => {
let descriptor: number | null = null;
try {
- if (!fs.lstatSync(target).isFile()) return null;
descriptor = fs.openSync(
target,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
@@ -451,8 +450,12 @@ export function runDeepAgentsConfigCommand(
if (descriptor !== null) fs.closeSync(descriptor);
}
};
- const managedSymlinkTargetText = readRegularFile(managedSymlinkTarget);
- const managedParentTargetText = readRegularFile(managedParentTargetConfig);
+ const managedSymlinkTargetText =
+ managedTargetReadPath === managedSymlinkTarget ? null : readRegularFile(managedSymlinkTarget);
+ const managedParentTargetText =
+ managedTargetReadPath === managedParentTargetConfig
+ ? null
+ : readRegularFile(managedParentTargetConfig);
const managedTargetReadAccessed =
managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
From a30831ee8812be9dd01819ed63202d49f4abffc7 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 02:47:30 -0700
Subject: [PATCH 26/49] test(mcp): race projection from independent process
Signed-off-by: Prekshi Vyas
---
...idge-adapter-deepagents-projection.test.ts | 242 +++--------
.../mcp-bridge-adapter-deepagents-fixture.ts | 388 ++++++++++--------
2 files changed, 276 insertions(+), 354 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 76b9435c0cf..cef8df8e8dc 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -4,6 +4,7 @@
import { describe, expect, it } from "vitest";
import {
baseEntry,
+ isSafeManagedProjectionRaceResult,
runDeepAgentsConfigCommand,
} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
import type { McpBridgeEntry } from "../../state/registry";
@@ -15,6 +16,7 @@ import { DEEPAGENTS_MCP_MAX_SERVERS } from "./mcp-bridge-adapter-deepagents-proj
import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const emptyProjection = { mcpServers: {} };
+const emptyProjectionText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
const duplicateProjection = '{"mcpServers":{},"mcpServers":{"shadow":{}}}\n';
const attackerProjection = '{"mcpServers":{"attacker":{"type":"stdio"}}}\n';
const managedCommandTimeoutMs = 30_000;
@@ -145,184 +147,53 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.managedTargetReadAccessed).toBe(false);
});
- it.each([
- {
- diagnostic: "symbolic link",
- expectedConfigIsFifo: false,
- expectedConfigIsSocket: false,
- expectedConfigIsSymlink: true,
- expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
- replacementType: "symlink" as const,
- },
- {
- diagnostic: "FIFO",
- expectedConfigIsFifo: true,
- expectedConfigIsSocket: false,
- expectedConfigIsSymlink: false,
- expectedTargetText: null,
- replacementType: "fifo" as const,
- },
- {
- diagnostic: "non-regular file",
- expectedConfigIsFifo: false,
- expectedConfigIsSocket: true,
- expectedConfigIsSymlink: false,
- expectedTargetText: null,
- replacementType: "socket" as const,
- },
- ])(
- "rejects a $replacementType replacement before projection open (#10754)",
- ({
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSocket,
- expectedConfigIsSymlink,
- expectedTargetText,
- replacementType,
- }) => {
- const result = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapOnManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
- );
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain(
- `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
- );
- expect(result.configIsFifo).toBe(expectedConfigIsFifo);
- expect(result.configIsSocket).toBe(expectedConfigIsSocket);
- expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
- expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
- },
- );
-
- it.each([
- {
- diagnostic: "symbolic link",
- expectedConfigIsFifo: false,
- expectedConfigIsSymlink: true,
- expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
- replacementType: "symlink" as const,
- },
- {
- diagnostic: "FIFO",
- expectedConfigIsFifo: true,
- expectedConfigIsSymlink: false,
- expectedTargetText: null,
- replacementType: "fifo" as const,
- },
- ])(
- "rejects a $replacementType replacement after projection open (#10754)",
- ({
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSymlink,
- expectedTargetText,
- replacementType,
- }) => {
- const result = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { swapAfterManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
- );
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain(
- `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
- );
- expect(result.configIsFifo).toBe(expectedConfigIsFifo);
- expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
- expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
- },
- );
-
- it("rejects a symbolic-link replacement while reading the projection (#10754)", () => {
+ it("rejects an existing socket projection path (#10754)", () => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
- "v2",
undefined,
- 0o600,
- { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(result.configIsSymlink).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
- });
-
- it("preserves conclusive ELOOP classification across a follow-up path swap (#10754)", () => {
- const result = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
"v2",
undefined,
0o600,
- {
- swapAfterManagedEloop: true,
- symlink: true,
- timeoutMs: managedCommandTimeoutMs,
- },
+ { socket: true, timeoutMs: managedCommandTimeoutMs },
);
expect(result.status).toBe(2);
expect(result.stdout.trim()).toBe("");
expect(result.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ "Unsafe managed Deep Agents MCP projection path: non-regular file",
);
- expect(result.configIsSymlink).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ expect(result.configIsSocket).toBe(true);
});
it.each([
{
- appearedType: "symlink" as const,
- diagnostic: "symbolic link",
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
- expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ expectedTargetText: emptyProjectionText,
+ raceProjection: "symlink" as const,
},
{
- appearedType: "fifo" as const,
- diagnostic: "FIFO",
expectedConfigIsFifo: true,
expectedConfigIsSymlink: false,
expectedTargetText: null,
+ raceProjection: "fifo" as const,
},
])(
- "classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({
- appearedType,
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSymlink,
- expectedTargetText,
- }) => {
+ "rejects or safely loses a concurrent $raceProjection projection race (#10754)",
+ { timeout: 60_000 },
+ ({ expectedConfigIsFifo, expectedConfigIsSymlink, expectedTargetText, raceProjection }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
"v2",
undefined,
0o600,
- { swapAfterMissingManagedOpen: appearedType, timeoutMs: managedCommandTimeoutMs },
+ { raceProjection, timeoutMs: managedCommandTimeoutMs },
);
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain(
- `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
- );
- expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.managedRaceIterations).toBeGreaterThan(0);
+ expect(isSafeManagedProjectionRaceResult(result)).toBe(true);
expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
},
);
@@ -418,50 +289,65 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
});
- it("preserves a projection that appears during absent publication", () => {
- const absentResult = runDeepAgentsConfigCommand(
- registrationCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { swapBeforeManagedLink: attackerProjection, timeoutMs: managedCommandTimeoutMs },
- );
- expect(absentResult.status).toBe(2);
- expect(absentResult.stderr).toContain("appeared during publication");
- expect(absentResult.configText).toBe(attackerProjection);
- });
+ it(
+ "preserves a projection that appears during absent publication (#10754)",
+ { timeout: 60_000 },
+ () => {
+ const absentResult = runDeepAgentsConfigCommand(
+ registrationCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { raceAbsentPublication: attackerProjection, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(absentResult.managedRaceIterations).toBeGreaterThan(0);
+ expect(absentResult.status).toBe(2);
+ expect(absentResult.configIsSymlink).toBe(true);
+ expect(absentResult.managedSymlinkTargetText).toBe(attackerProjection);
+ },
+ );
- it("preserves a replacement during a descriptor rewrite", () => {
+ it("preserves a replacement during a descriptor rewrite (#10754)", { timeout: 60_000 }, () => {
const existingResult = runDeepAgentsConfigCommand(
registrationCommand,
emptyProjection,
"v2",
undefined,
0o600,
- { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
+ { raceProjection: "symlink", timeoutMs: managedCommandTimeoutMs },
);
- expect(existingResult.status).toBe(2);
- expect(existingResult.stderr).toContain("links, or path identity");
- expect(existingResult.configText).toBe(attackerProjection);
- });
-
- it("keeps forced removal identity-bound during a descriptor rewrite (#10754)", () => {
- const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
- const raced = runDeepAgentsConfigCommand(
- forcedCommand,
- { ui: { theme: "dark" } },
- "v2",
- undefined,
- 0o600,
- { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
+ expect(existingResult.managedRaceIterations).toBeGreaterThan(0);
+ expect([0, 2]).toContain(existingResult.status);
+ expect(existingResult.configIsSymlink).toBe(true);
+ expect(existingResult.managedSymlinkTargetText).toBe(
+ `${JSON.stringify(emptyProjection, null, 2)}\n`,
);
- expect(raced.status).toBe(2);
- expect(raced.stderr).toContain("Refusing unsafe managed MCP v2 repair");
- expect(raced.stderr).not.toContain("Traceback");
- expect(raced.configText).toBe(attackerProjection);
});
+ it(
+ "keeps forced removal identity-bound during a descriptor rewrite (#10754)",
+ { timeout: 60_000 },
+ () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
+ const raced = runDeepAgentsConfigCommand(
+ forcedCommand,
+ { ui: { theme: "dark" } },
+ "v2",
+ undefined,
+ 0o600,
+ { raceProjection: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(raced.managedRaceIterations).toBeGreaterThan(0);
+ expect([0, 2]).toContain(raced.status);
+ expect(raced.stderr).not.toContain("Traceback");
+ expect(raced.configIsSymlink).toBe(true);
+ expect(raced.managedSymlinkTargetText).toBe(
+ `${JSON.stringify({ ui: { theme: "dark" } }, null, 2)}\n`,
+ );
+ },
+ );
+
it("preserves a projection symlink during forced removal", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedSymlink = runDeepAgentsConfigCommand(
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 7fcdaa16fa7..52661bb50a1 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -36,6 +36,7 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigText: string | null;
managedParentIsSymlink: boolean;
managedParentTargetText: string | null;
+ managedRaceIterations: number;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
managedTargetReadAccessed: boolean;
@@ -47,65 +48,19 @@ export interface DeepAgentsManagedFixtureOptions {
fifo?: boolean;
mode?: number;
parentSymlink?: boolean;
- swapAfterManagedEloop?: boolean;
- swapAfterManagedOpen?: "fifo" | "symlink";
- swapAfterMissingManagedOpen?: "fifo" | "symlink";
- swapBeforeManagedLink?: string;
- swapOnManagedOpen?: "fifo" | "socket" | "symlink";
- swapOnManagedRead?: "fifo" | "symlink";
- swapOnManagedSeek?: string;
+ raceAbsentPublication?: string;
+ raceProjection?: "fifo" | "symlink";
+ socket?: boolean;
symlink?: boolean;
targetReadProbe?: boolean;
timeoutMs?: number;
}
-type ManagedSwap = {
- content?: string;
- kind: "fifo" | "regular" | "socket" | "symlink";
- phase:
- | "after-missing-projection-open"
- | "after-projection-eloop"
- | "after-projection-open"
- | "before-projection-open"
- | "projection-publication-begins"
- | "projection-read-begins"
- | "projection-rewrite-begins";
-};
-
-function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSwap | undefined {
- if (options.swapOnManagedOpen) {
- return { kind: options.swapOnManagedOpen, phase: "before-projection-open" };
- }
- if (options.swapAfterManagedOpen) {
- return { kind: options.swapAfterManagedOpen, phase: "after-projection-open" };
- }
- if (options.swapAfterMissingManagedOpen) {
- return {
- kind: options.swapAfterMissingManagedOpen,
- phase: "after-missing-projection-open",
- };
- }
- if (options.swapOnManagedRead) {
- return { kind: options.swapOnManagedRead, phase: "projection-read-begins" };
- }
- if (options.swapAfterManagedEloop) {
- return { kind: "symlink", phase: "after-projection-eloop" };
- }
- if (options.swapBeforeManagedLink !== undefined) {
- return {
- content: options.swapBeforeManagedLink,
- kind: "regular",
- phase: "projection-publication-begins",
- };
- }
- if (options.swapOnManagedSeek !== undefined) {
- return {
- content: options.swapOnManagedSeek,
- kind: "regular",
- phase: "projection-rewrite-begins",
- };
- }
- return undefined;
+export function isSafeManagedProjectionRaceResult(result: DeepAgentsConfigCommandResult): boolean {
+ return (
+ (result.status === 0 && result.stdout.trim() === "absent" && result.stderr.trim() === "") ||
+ (result.status === 2 && result.stdout.trim() === "")
+ );
}
function createFifo(target: string, mode = 0o600): void {
@@ -128,6 +83,154 @@ function waitForPath(target: string, timeoutMs: number): boolean {
return fs.existsSync(target);
}
+type ManagedFixtureProcess = {
+ child: ReturnType;
+ finishedPath: string;
+ resultPath: string;
+ stopPath: string;
+};
+
+function startManagedFixtureProcess(
+ fixtureRoot: string,
+ label: string,
+ script: string,
+ args: string[],
+): ManagedFixtureProcess {
+ const readyPath = path.join(fixtureRoot, `${label}-ready`);
+ const stopPath = path.join(fixtureRoot, `${label}-stop`);
+ const finishedPath = path.join(fixtureRoot, `${label}-finished`);
+ const resultPath = path.join(fixtureRoot, `${label}-result`);
+ const child = spawn(
+ "python3",
+ ["-I", "-c", script, ...args, readyPath, stopPath, finishedPath, resultPath],
+ { stdio: "ignore" },
+ );
+ if (!waitForPath(readyPath, 2000)) {
+ child.kill("SIGTERM");
+ throw new Error(`${label} did not become ready`);
+ }
+ pause(20);
+ return { child, finishedPath, resultPath, stopPath };
+}
+
+function stopManagedFixtureProcess(process: ManagedFixtureProcess): number {
+ if (!fs.existsSync(process.stopPath)) fs.writeFileSync(process.stopPath, "", { flag: "wx" });
+ if (!waitForPath(process.finishedPath, 2000)) {
+ process.child.kill("SIGTERM");
+ throw new Error("managed fixture process did not stop");
+ }
+ process.child.kill("SIGTERM");
+ const result = Number.parseInt(fs.readFileSync(process.resultPath, "utf-8"), 10);
+ return Number.isFinite(result) ? result : 0;
+}
+
+function startManagedPathRace(
+ fixtureRoot: string,
+ managedPath: string,
+ managedTarget: string,
+ kind: "fifo" | "symlink",
+ safeContent: string,
+): ManagedFixtureProcess {
+ const script = [
+ "import base64, os, sys, time",
+ "managed_path, managed_target, race_kind, encoded = sys.argv[1:5]",
+ "ready, stop, finished, result = sys.argv[5:9]",
+ "safe_content = base64.b64decode(encoded)",
+ "safe_temp = managed_path + '.race-safe'",
+ "unsafe_temp = managed_path + '.race-unsafe'",
+ "def remove_temp(target):",
+ " try:",
+ " os.unlink(target)",
+ " except FileNotFoundError:",
+ " pass",
+ "def replace_safe():",
+ " remove_temp(safe_temp)",
+ " descriptor = os.open(safe_temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)",
+ " try:",
+ " offset = 0",
+ " while offset < len(safe_content):",
+ " offset += os.write(descriptor, safe_content[offset:])",
+ " os.fsync(descriptor)",
+ " finally:",
+ " os.close(descriptor)",
+ " os.replace(safe_temp, managed_path)",
+ "def replace_unsafe():",
+ " remove_temp(unsafe_temp)",
+ " if race_kind == 'symlink':",
+ " os.symlink(managed_target, unsafe_temp)",
+ " else:",
+ " os.mkfifo(unsafe_temp, 0o600)",
+ " os.replace(unsafe_temp, managed_path)",
+ "iterations = 0",
+ "replace_unsafe()",
+ "open(ready, 'x').close()",
+ "while not os.path.exists(stop):",
+ " replace_safe()",
+ " iterations += 1",
+ " replace_unsafe()",
+ " iterations += 1",
+ " time.sleep(0.0001)",
+ "replace_unsafe()",
+ "open(result, 'w', encoding='utf-8').write(str(iterations))",
+ "open(finished, 'x').close()",
+ ].join("\n");
+ return startManagedFixtureProcess(fixtureRoot, "managed-path-race", script, [
+ managedPath,
+ managedTarget,
+ kind,
+ Buffer.from(safeContent).toString("base64"),
+ ]);
+}
+
+function startAbsentPublicationRace(
+ fixtureRoot: string,
+ managedPath: string,
+ managedTarget: string,
+): ManagedFixtureProcess {
+ const script = [
+ "import os, sys, time",
+ "managed_path, managed_target = sys.argv[1:3]",
+ "ready, stop, finished, result = sys.argv[3:7]",
+ "parent = os.path.dirname(managed_path)",
+ "triggered = 0",
+ "open(ready, 'x').close()",
+ "while not os.path.exists(stop) and not triggered:",
+ " if os.path.isdir(parent):",
+ " try:",
+ " os.symlink(managed_target, managed_path)",
+ " triggered = 1",
+ " except FileExistsError:",
+ " pass",
+ " time.sleep(0.0001)",
+ "while not os.path.exists(stop):",
+ " time.sleep(0.001)",
+ "open(result, 'w', encoding='utf-8').write(str(triggered))",
+ "open(finished, 'x').close()",
+ ].join("\n");
+ return startManagedFixtureProcess(fixtureRoot, "managed-publication-race", script, [
+ managedPath,
+ managedTarget,
+ ]);
+}
+
+function startManagedSocket(fixtureRoot: string, managedPath: string): ManagedFixtureProcess {
+ const script = [
+ "import os, socket, sys, time",
+ "managed_path = sys.argv[1]",
+ "ready, stop, finished, result = sys.argv[2:6]",
+ "os.makedirs(os.path.dirname(managed_path), exist_ok=True)",
+ "managed_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)",
+ "managed_socket.bind(managed_path)",
+ "open(ready, 'x').close()",
+ "while not os.path.exists(stop):",
+ " time.sleep(0.001)",
+ "managed_socket.close()",
+ "open(result, 'w', encoding='utf-8').write('1')",
+ "open(finished, 'x').close()",
+ ].join("\n");
+ return startManagedFixtureProcess(fixtureRoot, "managed-socket", script, [managedPath]);
+}
+
function startTargetReadProbe(target: string, ready: string, accessed: string) {
const script = [
"import os, sys",
@@ -151,12 +254,10 @@ function createDeepAgentsFixturePythonExecutable(
fixtureRoot: string,
configPath: string,
legacyConfigPath: string,
- managedSymlinkTarget: string,
runtimeKind: "v2" | "legacy" | "unknown",
- managedSwap?: ManagedSwap,
): { executablePath: string; scriptPath: string } {
- const executablePath = path.join(fixtureRoot, "managed-swap-python");
- const scriptPath = path.join(fixtureRoot, "managed-swap-wrapper.py");
+ const executablePath = path.join(fixtureRoot, "fixture-python");
+ const scriptPath = path.join(fixtureRoot, "fixture-wrapper.py");
const runtimeConfigPath =
runtimeKind === "v2"
? configPath
@@ -172,117 +273,12 @@ function createDeepAgentsFixturePythonExecutable(
{ mode: 0o600 },
);
const wrapper = [
- "import errno",
- "import os",
- "import socket",
"import sys",
`fixture_root = ${JSON.stringify(fixtureRoot)}`,
- `managed_path = ${JSON.stringify(configPath)}`,
- `managed_target = ${JSON.stringify(managedSymlinkTarget)}`,
- `managed_swap_kind = ${JSON.stringify(managedSwap?.kind ?? "")}`,
- `managed_swap_phase = ${JSON.stringify(managedSwap?.phase ?? "")}`,
- `managed_swap_content = ${JSON.stringify(managedSwap?.content ?? "")}`,
- "managed_eloop_stage = 0",
- "managed_socket = None",
- "managed_swapped = False",
- "managed_descriptor = None",
- "managed_entry_name = os.path.basename(managed_path)",
- "managed_real_link = os.link",
- "managed_real_lseek = os.lseek",
- "managed_real_open = os.open",
- "managed_real_read = os.read",
- "def replace_managed_path():",
- " global managed_socket, managed_swapped",
- " if managed_swapped:",
- " return",
- " managed_swapped = True",
- " try:",
- " os.unlink(managed_path)",
- " except FileNotFoundError:",
- " pass",
- " if managed_swap_kind == 'symlink':",
- " os.symlink(managed_target, managed_path)",
- " elif managed_swap_kind == 'fifo':",
- " os.mkfifo(managed_path, 0o600)",
- " elif managed_swap_kind == 'socket':",
- " managed_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)",
- " managed_socket.bind(managed_path)",
- " else:",
- " with open(managed_path, 'w', encoding='utf-8') as replacement:",
- " replacement.write(managed_swap_content)",
- " os.chmod(managed_path, 0o600)",
- "def replace_managed_symlink_with_regular():",
- " global managed_eloop_stage",
- " os.unlink(managed_path)",
- " with open(managed_target, 'rb') as source, open(managed_path, 'wb') as replacement:",
- " replacement.write(source.read())",
- " os.chmod(managed_path, 0o600)",
- " managed_eloop_stage = 1",
- "def restore_managed_symlink():",
- " global managed_eloop_stage",
- " os.unlink(managed_path)",
- " os.symlink(managed_target, managed_path)",
- " managed_eloop_stage = 2",
- "def is_managed_entry(value, directory_descriptor):",
- " try:",
- " return directory_descriptor is not None and os.fspath(value) == managed_entry_name",
- " except TypeError:",
- " return False",
- "def managed_boundary_open(value, flags, mode=0o777, *, dir_fd=None):",
- " global managed_descriptor",
- " projection_entry = is_managed_entry(value, dir_fd)",
- " if projection_entry and managed_swap_phase == 'before-projection-open':",
- " replace_managed_path()",
- " try:",
- " if dir_fd is None:",
- " descriptor = managed_real_open(value, flags, mode)",
- " else:",
- " descriptor = managed_real_open(value, flags, mode, dir_fd=dir_fd)",
- " except OSError as exc:",
- " if projection_entry and managed_swap_phase == 'after-missing-projection-open' and isinstance(exc, FileNotFoundError):",
- " replace_managed_path()",
- " elif projection_entry and managed_swap_phase == 'after-projection-eloop' and exc.errno == errno.ELOOP:",
- " replace_managed_symlink_with_regular()",
- " raise",
- " if projection_entry:",
- " managed_descriptor = descriptor",
- " if managed_swap_phase == 'after-projection-open':",
- " replace_managed_path()",
- " return descriptor",
- "def managed_boundary_read(descriptor, length):",
- " if managed_swap_phase == 'projection-read-begins' and descriptor == managed_descriptor:",
- " replace_managed_path()",
- " return managed_real_read(descriptor, length)",
- "def managed_boundary_link(source, destination, *args, **kwargs):",
- " if managed_swap_phase == 'projection-publication-begins' and is_managed_entry(destination, kwargs.get('dst_dir_fd')):",
- " replace_managed_path()",
- " return managed_real_link(source, destination, *args, **kwargs)",
- "def managed_boundary_lseek(descriptor, position, how):",
- " if managed_swap_phase == 'projection-rewrite-begins' and descriptor == managed_descriptor:",
- " replace_managed_path()",
- " return managed_real_lseek(descriptor, position, how)",
- "def install_managed_boundaries():",
- " if not managed_swap_phase:",
- " return",
- " os.link = managed_boundary_link",
- " os.lseek = managed_boundary_lseek",
- " os.open = managed_boundary_open",
- " os.read = managed_boundary_read",
- "def restore_managed_boundaries():",
- " os.link = managed_real_link",
- " os.lseek = managed_real_lseek",
- " os.open = managed_real_open",
- " os.read = managed_real_read",
"source = sys.stdin.read()",
"namespace = {'__name__': '__main__'}",
"sys.path.insert(0, fixture_root)",
- "install_managed_boundaries()",
- "try:",
- " exec(compile(source, '', 'exec'), namespace)",
- "finally:",
- " restore_managed_boundaries()",
- " if managed_swap_phase == 'after-projection-eloop' and managed_eloop_stage == 1:",
- " restore_managed_symlink()",
+ "exec(compile(source, '', 'exec'), namespace)",
"",
].join("\n");
fs.writeFileSync(scriptPath, wrapper, { mode: 0o600 });
@@ -303,8 +299,15 @@ export function runDeepAgentsConfigCommand(
managedOptions: DeepAgentsManagedFixtureOptions = {},
runtimeEnvironment: NodeJS.ProcessEnv = {},
): DeepAgentsConfigCommandResult {
- const managedSwap = resolveManagedSwap(managedOptions);
- const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
+ const activeFixtureProcesses = [
+ managedOptions.raceAbsentPublication !== undefined,
+ managedOptions.raceProjection !== undefined,
+ managedOptions.socket === true,
+ ].filter(Boolean).length;
+ if (activeFixtureProcesses > 1) {
+ throw new Error("managed fixture process options are mutually exclusive");
+ }
+ const fixtureRoot = managedOptions.socket ? "/tmp" : os.tmpdir();
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
const managedParentPath = path.dirname(configPath);
@@ -351,31 +354,36 @@ export function runDeepAgentsConfigCommand(
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
createFifo(configPath, managedOptions.mode);
+ } else if (managedOptions.socket) {
+ // The socket holder creates the path after the rest of the fixture is ready.
} else {
if (managedTargetReadPath === managedSymlinkTarget) {
createFifo(managedSymlinkTarget, managedOptions.mode);
} else if (
!managedOptions.danglingSymlink &&
- managedSwap?.phase !== "after-missing-projection-open"
+ managedOptions.raceAbsentPublication === undefined
) {
initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
if (initialConfig !== undefined)
fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
}
- if (managedSwap?.phase === "after-missing-projection-open") {
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
- }
if (managedSymlink) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.symlinkSync(managedSymlinkTarget, configPath);
}
}
- if (managedSwap?.kind === "symlink") {
+ if (managedOptions.raceProjection === "symlink") {
initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
}
+ if (managedOptions.raceAbsentPublication !== undefined) {
+ initializeConfig(managedSymlinkTarget, managedOptions.raceAbsentPublication);
+ }
initializeConfig(legacyConfigPath, initialLegacyConfig);
if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
let targetReadProbe: ReturnType | null = null;
+ let managedRace: ManagedFixtureProcess | null = null;
+ let socketFixture: ManagedFixtureProcess | null = null;
+ let managedRaceIterations = 0;
try {
if (managedTargetReadPath !== null) {
targetReadProbe = startTargetReadProbe(
@@ -384,22 +392,39 @@ export function runDeepAgentsConfigCommand(
managedTargetReadAccess,
);
}
+ if (managedOptions.socket) {
+ socketFixture = startManagedSocket(tmp, configPath);
+ } else if (managedOptions.raceProjection) {
+ if (initialConfig === undefined) throw new Error("raceProjection requires initialConfig");
+ const safeContent =
+ typeof initialConfig === "string"
+ ? initialConfig
+ : `${JSON.stringify(initialConfig, null, 2)}\n`;
+ managedRace = startManagedPathRace(
+ tmp,
+ configPath,
+ managedSymlinkTarget,
+ managedOptions.raceProjection,
+ safeContent,
+ );
+ } else if (managedOptions.raceAbsentPublication !== undefined) {
+ managedRace = startAbsentPublicationRace(tmp, configPath, managedSymlinkTarget);
+ }
const fixturePython = createDeepAgentsFixturePythonExecutable(
tmp,
configPath,
legacyConfigPath,
- managedSymlinkTarget,
runtimeKind,
- managedSwap,
);
const fixtureCommand = command
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
.replaceAll("/opt/venv/bin/python3", fixturePython.executablePath);
const canonicalEnvironment = Object.fromEntries(
- [...command.matchAll(/openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]*)/gu)].map(
- ([, name]) => [name!, `openshell:resolve:env:${name!}`],
- ),
+ [...command.matchAll(/openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]*)/gu)].map(([, name]) => [
+ name!,
+ `openshell:resolve:env:${name!}`,
+ ]),
);
const fixtureCommandPath = path.join(tmp, "managed-command.sh");
fs.writeFileSync(fixtureCommandPath, fixtureCommand, { mode: 0o600 });
@@ -413,6 +438,14 @@ export function runDeepAgentsConfigCommand(
},
timeout: managedOptions.timeoutMs ?? 5000,
});
+ if (managedRace !== null) {
+ managedRaceIterations = stopManagedFixtureProcess(managedRace);
+ managedRace = null;
+ }
+ if (socketFixture !== null) {
+ stopManagedFixtureProcess(socketFixture);
+ socketFixture = null;
+ }
let configStat: fs.Stats | null = null;
try {
configStat = fs.lstatSync(configPath);
@@ -483,11 +516,14 @@ export function runDeepAgentsConfigCommand(
legacyConfigText,
managedParentIsSymlink,
managedParentTargetText,
+ managedRaceIterations,
managedSymlinkTargetExists,
managedSymlinkTargetText,
managedTargetReadAccessed,
};
} finally {
+ if (managedRace !== null) managedRace.child.kill("SIGTERM");
+ if (socketFixture !== null) socketFixture.child.kill("SIGTERM");
targetReadProbe?.kill("SIGTERM");
fs.rmSync(tmp, { recursive: true, force: true });
}
From 269c1adb1d1d0874278be722b12d5180f4de76eb Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 03:19:17 -0700
Subject: [PATCH 27/49] fix(mcp): tighten projection review boundaries
Signed-off-by: Prekshi Vyas
---
...idge-adapter-deepagents-projection.test.ts | 30 +++++++++++++++++--
...cp-bridge-adapter-deepagents-projection.ts | 14 ++++-----
.../mcp-bridge-adapter-deepagents-fixture.ts | 7 -----
3 files changed, 34 insertions(+), 17 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index cef8df8e8dc..96552973c78 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -4,7 +4,6 @@
import { describe, expect, it } from "vitest";
import {
baseEntry,
- isSafeManagedProjectionRaceResult,
runDeepAgentsConfigCommand,
} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
import type { McpBridgeEntry } from "../../state/registry";
@@ -26,6 +25,16 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
+ it("renders status without mutation-only projection helpers", () => {
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+
+ expect(statusCommand).toContain("def read_managed_projection");
+ expect(statusCommand).not.toContain("def load_managed_projection_for_update");
+ expect(registrationCommand).toContain("def load_managed_projection_for_update");
+ expect(removalCommand).toContain("def load_managed_projection_for_update");
+ expect(rollbackCommand).toContain("def load_managed_projection_for_update");
+ });
+
it(
"applies the shared server cap before normal and rollback v2 publication",
{ timeout: 60_000 },
@@ -168,19 +177,27 @@ describe("Deep Agents managed MCP projection safety", () => {
{
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
+ expectedSafetyDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
expectedTargetText: emptyProjectionText,
raceProjection: "symlink" as const,
},
{
expectedConfigIsFifo: true,
expectedConfigIsSymlink: false,
+ expectedSafetyDiagnostic: "Unsafe managed Deep Agents MCP projection path: FIFO",
expectedTargetText: null,
raceProjection: "fifo" as const,
},
])(
"rejects or safely loses a concurrent $raceProjection projection race (#10754)",
{ timeout: 60_000 },
- ({ expectedConfigIsFifo, expectedConfigIsSymlink, expectedTargetText, raceProjection }) => {
+ ({
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedSafetyDiagnostic,
+ expectedTargetText,
+ raceProjection,
+ }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -191,7 +208,14 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(result.managedRaceIterations).toBeGreaterThan(0);
- expect(isSafeManagedProjectionRaceResult(result)).toBe(true);
+ expect([
+ { status: 0, stderr: "", stdout: "absent" },
+ { status: 2, stderr: expectedSafetyDiagnostic, stdout: "" },
+ ]).toContainEqual({
+ status: result.status,
+ stderr: result.stderr.trim(),
+ stdout: result.stdout.trim(),
+ });
expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
expect(result.configIsFifo).toBe(expectedConfigIsFifo);
expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index c21db5ed120..cd0378cd0b8 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -193,13 +193,6 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" os.close(current)",
" except OSError:",
" pass",
- "def load_managed_projection_for_update(path):",
- " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
- " try:",
- " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
- " raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
" raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
@@ -209,6 +202,13 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
+ "def load_managed_projection_for_update(path):",
+ " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
+ " try:",
+ " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
+ " except Exception:",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " raise",
"def managed_projection_bytes(value):",
" payload = (json.dumps(value, indent=2, sort_keys=True) + '\\n').encode('utf-8')",
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index c976ea486f1..00e1777cae1 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -56,13 +56,6 @@ export interface DeepAgentsManagedFixtureOptions {
timeoutMs?: number;
}
-export function isSafeManagedProjectionRaceResult(result: DeepAgentsConfigCommandResult): boolean {
- return (
- (result.status === 0 && result.stdout.trim() === "absent" && result.stderr.trim() === "") ||
- (result.status === 2 && result.stdout.trim() === "")
- );
-}
-
function createFifo(target: string, mode = 0o600): void {
fs.mkdirSync(path.dirname(target), { recursive: true });
const fifo = spawnSync("mkfifo", [target], { encoding: "utf-8", timeout: 5000 });
From a6ad937bc0d74893073c1a9ece13242dcd1721c2 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 03:29:18 -0700
Subject: [PATCH 28/49] test(mcp): harden fixture result read
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
.../mcp-bridge-adapter-deepagents-fixture.ts | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 00e1777cae1..2095a8cd6a0 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -113,7 +113,19 @@ function stopManagedFixtureProcess(process: ManagedFixtureProcess): number {
throw new Error("managed fixture process did not stop");
}
process.child.kill("SIGTERM");
- const result = Number.parseInt(fs.readFileSync(process.resultPath, "utf-8"), 10);
+ const descriptor = fs.openSync(
+ process.resultPath,
+ fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
+ );
+ let result: number;
+ try {
+ if (!fs.fstatSync(descriptor).isFile()) {
+ throw new Error("managed fixture process result is not a regular file");
+ }
+ result = Number.parseInt(fs.readFileSync(descriptor, "utf-8"), 10);
+ } finally {
+ fs.closeSync(descriptor);
+ }
return Number.isFinite(result) ? result : 0;
}
From 0aa37e9190ea43aeeddd152e72842e1cfcc4ed07 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 03:31:03 -0700
Subject: [PATCH 29/49] test(mcp): remove projection helper shape assertion
Signed-off-by: Prekshi Vyas
---
.../mcp-bridge-adapter-deepagents-projection.test.ts | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 96552973c78..47ff7f9657d 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -25,16 +25,6 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it("renders status without mutation-only projection helpers", () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
-
- expect(statusCommand).toContain("def read_managed_projection");
- expect(statusCommand).not.toContain("def load_managed_projection_for_update");
- expect(registrationCommand).toContain("def load_managed_projection_for_update");
- expect(removalCommand).toContain("def load_managed_projection_for_update");
- expect(rollbackCommand).toContain("def load_managed_projection_for_update");
- });
-
it(
"applies the shared server cap before normal and rollback v2 publication",
{ timeout: 60_000 },
From 286ef7229969ab087859e2e9f5d3d48f40c77830 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 03:56:38 -0700
Subject: [PATCH 30/49] test(mcp): create fixture stop marker atomically
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
test/helpers/mcp-bridge-adapter-deepagents-fixture.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 2095a8cd6a0..41d1294e8d9 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -107,7 +107,11 @@ function startManagedFixtureProcess(
}
function stopManagedFixtureProcess(process: ManagedFixtureProcess): number {
- if (!fs.existsSync(process.stopPath)) fs.writeFileSync(process.stopPath, "", { flag: "wx" });
+ try {
+ fs.writeFileSync(process.stopPath, "", { flag: "wx" });
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
+ }
if (!waitForPath(process.finishedPath, 2000)) {
process.child.kill("SIGTERM");
throw new Error("managed fixture process did not stop");
From 266da41540cd0a5439784ef3e124ef0c823773b6 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 03:59:37 -0700
Subject: [PATCH 31/49] test(e2e): isolate blueprint command fixtures
Signed-off-by: Prekshi Vyas
---
test/e2e-test.sh | 62 ++++++++++++++++++++++++++++++++++--------------
1 file changed, 44 insertions(+), 18 deletions(-)
diff --git a/test/e2e-test.sh b/test/e2e-test.sh
index 5a11a76ca11..32ecaa19bf4 100755
--- a/test/e2e-test.sh
+++ b/test/e2e-test.sh
@@ -111,19 +111,39 @@ fi
# -------------------------------------------------------
info "4. Verify blueprint runner plan command"
# -------------------------------------------------------
-cd /opt/nemoclaw-blueprint
-# Runner will fail at openshell prereq check (expected in test container).
-# Use 'ncp' profile (empty endpoint skips SSRF DNS lookup in sandbox).
-# Catch only the expected error — anything else propagates as a real failure.
-NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint node --input-type=module -e "
- const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
- try {
- await main(['plan', '--profile', 'ncp', '--dry-run']);
- } catch (err) {
- if (!err.message.includes('openshell CLI not found')) throw err;
- console.log('EXPECTED_ERROR: ' + err.message);
- }
-" 2>&1 | tee /tmp/plan-output.txt
+prepare_command_blueprint() {
+ local destination="$1"
+ cp -R /opt/nemoclaw-blueprint/. "$destination"
+ node --input-type=module -e "
+ import { createRequire } from 'node:module';
+ import { readFileSync, writeFileSync } from 'node:fs';
+ const require = createRequire('/opt/nemoclaw/');
+ const YAML = require('yaml');
+ const path = process.argv[1];
+ const bp = YAML.parse(readFileSync(path, 'utf-8'));
+ bp.components.policy.additions.nim_service.endpoints[0].host = '93.184.216.34';
+ writeFileSync(path, YAML.stringify(bp));
+ " "$destination/blueprint.yaml"
+}
+
+(
+ PLAN_BLUEPRINT_PATH=$(mktemp -d)
+ trap 'rm -rf "$PLAN_BLUEPRINT_PATH"' EXIT
+ prepare_command_blueprint "$PLAN_BLUEPRINT_PATH"
+ cd "$PLAN_BLUEPRINT_PATH"
+ # Steps 3 and 3b validate the shipped blueprint. This command smoke test uses
+ # a copy with a stable public IP fixture so DNS and the local service hostname
+ # cannot preempt the expected OpenShell prerequisite boundary.
+ NEMOCLAW_BLUEPRINT_PATH="$PLAN_BLUEPRINT_PATH" node --input-type=module -e "
+ const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
+ try {
+ await main(['plan', '--profile', 'ncp', '--dry-run']);
+ } catch (err) {
+ if (!err.message.includes('openshell CLI not found')) throw err;
+ console.log('EXPECTED_ERROR: ' + err.message);
+ }
+ "
+) 2>&1 | tee /tmp/plan-output.txt
if grep -q "RUN_ID:" /tmp/plan-output.txt; then
pass "Blueprint plan generates run ID"
else
@@ -148,13 +168,15 @@ info "4b. Verify blueprint runner apply smoke test"
# return the same metadata + YAML shape as OpenShell 0.0.72; an empty successful
# response is intentionally rejected by the runner.
FAKE_OPENSHELL_BIN=$(mktemp -d)
+APPLY_BLUEPRINT_PATH=$(mktemp -d)
APPLY_OUTPUT=$(mktemp)
APPLY_CALLS="$FAKE_OPENSHELL_BIN/calls"
cleanup_apply_fixture() {
- rm -rf "$FAKE_OPENSHELL_BIN"
+ rm -rf "$FAKE_OPENSHELL_BIN" "$APPLY_BLUEPRINT_PATH"
rm -f "$APPLY_OUTPUT" "$APPLY_CALLS"
}
trap cleanup_apply_fixture EXIT
+prepare_command_blueprint "$APPLY_BLUEPRINT_PATH"
cat >"$FAKE_OPENSHELL_BIN/openshell" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
@@ -188,7 +210,7 @@ if [ "${1:-} ${2:-}" = "policy get" ]; then
fi
if [ "${1:-} ${2:-}" = "policy get" ] && [[ " $* " == *" --output json "* ]]; then
sandbox="${@: -1}"
- policy_file=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
+ policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
policy_hash=sha256:fixture-policy
policy_version=1
if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
@@ -218,8 +240,12 @@ case "$*" in
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
exit 64
fi
+ policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
+ if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
+ policy_file="${BASH_SOURCE[0]%/*}/effective-policy.yaml"
+ fi
printf '%s\n' 'Policy for sandbox fixture' '---'
- cat /opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
+ cat "$policy_file"
;;
"policy get "*)
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
@@ -229,8 +255,8 @@ esac
SH
chmod 0755 "$FAKE_OPENSHELL_BIN/openshell"
PATH="$FAKE_OPENSHELL_BIN:$PATH" \
- NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint \
- OPENSHELL_SANDBOX_POLICY=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml \
+ NEMOCLAW_BLUEPRINT_PATH="$APPLY_BLUEPRINT_PATH" \
+ OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/policies/openclaw-sandbox.yaml" \
node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
await main(['apply', '--profile', 'ncp']);
From 83d233072cc52b8d2bb7c7649e5808e9cd7b2d37 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 04:15:15 -0700
Subject: [PATCH 32/49] test(e2e): use synthetic blueprint command fixture
Signed-off-by: Prekshi Vyas
---
test/e2e-test.sh | 54 +++++++++++++++++++++++++++++++++++++++---------
1 file changed, 44 insertions(+), 10 deletions(-)
diff --git a/test/e2e-test.sh b/test/e2e-test.sh
index 32ecaa19bf4..9a27498a085 100755
--- a/test/e2e-test.sh
+++ b/test/e2e-test.sh
@@ -113,17 +113,51 @@ info "4. Verify blueprint runner plan command"
# -------------------------------------------------------
prepare_command_blueprint() {
local destination="$1"
- cp -R /opt/nemoclaw-blueprint/. "$destination"
node --input-type=module -e "
import { createRequire } from 'node:module';
- import { readFileSync, writeFileSync } from 'node:fs';
+ import { writeFileSync } from 'node:fs';
+ import { join } from 'node:path';
const require = createRequire('/opt/nemoclaw/');
const YAML = require('yaml');
- const path = process.argv[1];
- const bp = YAML.parse(readFileSync(path, 'utf-8'));
- bp.components.policy.additions.nim_service.endpoints[0].host = '93.184.216.34';
- writeFileSync(path, YAML.stringify(bp));
- " "$destination/blueprint.yaml"
+ const destination = process.argv[1];
+ const blueprint = {
+ version: '1.0',
+ components: {
+ sandbox: {
+ image: 'openclaw',
+ name: 'openclaw',
+ forward_ports: [18789],
+ },
+ inference: {
+ profiles: {
+ ncp: {
+ provider_type: 'nvidia',
+ provider_name: 'nvidia-ncp',
+ endpoint: '',
+ model: 'nvidia/nemotron-3-super-120b-a12b',
+ },
+ },
+ },
+ policy: {
+ additions: {
+ nim_service: {
+ name: 'nim_service',
+ endpoints: [{ host: '93.184.216.34', port: 8000, access: 'full' }],
+ },
+ },
+ },
+ },
+ };
+ writeFileSync(join(destination, 'blueprint.yaml'), YAML.stringify(blueprint));
+ writeFileSync(
+ join(destination, 'sandbox-policy.yaml'),
+ YAML.stringify({ version: 1, network_policies: {} }),
+ );
+ writeFileSync(
+ join(destination, 'private-networks.yaml'),
+ YAML.stringify({ ipv4: [], ipv6: [], names: [] }),
+ );
+ " "$destination"
}
(
@@ -132,8 +166,8 @@ prepare_command_blueprint() {
prepare_command_blueprint "$PLAN_BLUEPRINT_PATH"
cd "$PLAN_BLUEPRINT_PATH"
# Steps 3 and 3b validate the shipped blueprint. This command smoke test uses
- # a copy with a stable public IP fixture so DNS and the local service hostname
- # cannot preempt the expected OpenShell prerequisite boundary.
+ # an independent fixture with a stable public IP so DNS cannot preempt the
+ # expected OpenShell prerequisite boundary.
NEMOCLAW_BLUEPRINT_PATH="$PLAN_BLUEPRINT_PATH" node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
try {
@@ -256,7 +290,7 @@ SH
chmod 0755 "$FAKE_OPENSHELL_BIN/openshell"
PATH="$FAKE_OPENSHELL_BIN:$PATH" \
NEMOCLAW_BLUEPRINT_PATH="$APPLY_BLUEPRINT_PATH" \
- OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/policies/openclaw-sandbox.yaml" \
+ OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/sandbox-policy.yaml" \
node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
await main(['apply', '--profile', 'ncp']);
From b63ef9c0162ed96c89eac3430371691513ebbc8d Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 04:42:32 -0700
Subject: [PATCH 33/49] fix(mcp): pin legacy config parent descriptors
---
...adapter-deepagents-legacy-teardown.test.ts | 27 +++
.../mcp-bridge-adapter-deepagents-legacy.ts | 39 ++---
...idge-adapter-deepagents-projection.test.ts | 19 ++-
...bridge-adapter-deepagents-rollback.test.ts | 22 +++
.../mcp-bridge-adapter-deepagents-teardown.ts | 48 ++----
.../mcp-bridge/deepagents-legacy-config.ts | 160 ++++++++++++++----
.../mcp-bridge-adapter-deepagents-fixture.ts | 38 ++++-
7 files changed, 257 insertions(+), 96 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
index 1a9bcfa3735..9c315445314 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
@@ -80,4 +80,31 @@ describe("Deep Agents MCP config adapter legacy teardown", () => {
ui: { theme: "dark" },
});
});
+
+ it("refuses removal through a symlinked legacy parent", () => {
+ const legacyConfig = {
+ mcpServers: {
+ github: {
+ type: "http",
+ url: baseEntry.url,
+ headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
+ },
+ },
+ };
+ const original = `${JSON.stringify(legacyConfig, null, 2)}\n`;
+
+ const removal = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRemoveCommand(baseEntry, false, true),
+ undefined,
+ "legacy",
+ legacyConfig,
+ 0o600,
+ { legacyParentSymlink: true },
+ );
+
+ expect(removal.status).toBe(2);
+ expect(removal.stderr).toContain("legacy MCP config parent is unsafe: symbolic link");
+ expect(removal.legacyParentIsSymlink).toBe(true);
+ expect(removal.legacyParentTargetText).toBe(original);
+ });
});
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index bcce0026c77..ae22f2e3c1e 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -40,7 +40,7 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys, tempfile",
+ "import json, os, pathlib, stat, sys",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`managed_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
`legacy_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_LEGACY_MCP_CONFIG_PATH)})`,
@@ -74,15 +74,19 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"managed_parent_identity = None",
"managed_parent_descriptor = None",
"legacy_identity = None",
+ "legacy_descriptor = None",
+ "legacy_parent_identity = None",
+ "legacy_parent_descriptor = None",
"def fail_rollback(message):",
" close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
" data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
- " elif os.path.lexists(config_path):",
- " data, legacy_identity = read_legacy_config(config_path)",
+ " else:",
+ " data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity = load_legacy_config_for_update(config_path, True)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
" fail_rollback(f'Invalid managed MCP rollback state at {config_path}: {exc}')",
"if not isinstance(data, dict):",
@@ -111,35 +115,16 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
"if not is_v2:",
- " config_path.parent.mkdir(parents=True, exist_ok=True)",
- " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
- " os.fchmod(tmp_fd, 0o600)",
- " with os.fdopen(tmp_fd, 'w', encoding='utf-8') as tmp_file:",
- " json.dump(data, tmp_file, indent=2, sort_keys=True)",
- " tmp_file.write('\\n')",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
- " assert_legacy_source_stable(config_path, legacy_identity)",
- " if legacy_identity is None:",
- " os.link(tmp_name, config_path, follow_symlinks=False)",
- " os.unlink(tmp_name)",
- " else:",
- " os.replace(tmp_name, config_path)",
- " finally:",
- " try:",
- " os.unlink(tmp_name)",
- " except FileNotFoundError:",
- " pass",
+ " persisted = write_legacy_config(config_path, data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
+ " except (OSError, ValueError) as exc:",
+ " fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
"else:",
" try:",
" write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " persisted = read_managed_projection(config_path)[0]",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
- "try:",
- " persisted = read_managed_projection(config_path)[0] if is_v2 else read_legacy_config(config_path)[0]",
- "except (OSError, UnicodeDecodeError, ValueError) as exc:",
- " fail_rollback(f'Could not verify managed MCP rollback state at {config_path}: {exc}')",
"if is_v2:",
" persisted_servers = persisted.get('mcpServers') if isinstance(persisted, dict) else None",
" restored = isinstance(persisted_servers, dict) and set(persisted_servers) == set(payload['expectedServers']) and all(managed_http_server_matches(persisted_servers.get(name), expected, True) for name, expected in payload['expectedServers'].items())",
@@ -148,6 +133,8 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" restored = isinstance(persisted_servers, dict) and persisted_servers.get(payload['server']) == payload['expected']",
"if not restored:",
" fail_rollback(f'Managed MCP rollback verification failed at {config_path}')",
+ "close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ "close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
"print('NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1')",
"PY",
].join("\n");
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 47ff7f9657d..994121103f8 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -198,14 +198,23 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(result.managedRaceIterations).toBeGreaterThan(0);
- expect([
- { status: 0, stderr: "", stdout: "absent" },
- { status: 2, stderr: expectedSafetyDiagnostic, stdout: "" },
- ]).toContainEqual({
+ const actualOutcome = {
status: result.status,
stderr: result.stderr.trim(),
stdout: result.stdout.trim(),
- });
+ };
+ const safelyLostRace =
+ actualOutcome.status === 0 &&
+ actualOutcome.stderr === "" &&
+ actualOutcome.stdout === "absent";
+ const safelyRejectedRace =
+ actualOutcome.status === 2 &&
+ actualOutcome.stdout === "" &&
+ (actualOutcome.stderr === expectedSafetyDiagnostic ||
+ /^Could not inspect managed Deep Agents MCP state at .+: managed MCP projection has unsafe ownership, mode, type, links, or path identity$/u.test(
+ actualOutcome.stderr,
+ ));
+ expect(safelyLostRace || safelyRejectedRace, JSON.stringify(actualOutcome)).toBe(true);
expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
expect(result.configIsFifo).toBe(expectedConfigIsFifo);
expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
index 0cedb3f7642..59724441013 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
@@ -38,6 +38,28 @@ describe("Deep Agents MCP config adapter rollback", () => {
});
});
+ it("refuses rollback through a symlinked legacy parent", () => {
+ const legacyConfig = {
+ mcpServers: { local: { type: "stdio", command: "user-owned" } },
+ ui: { theme: "dark" },
+ };
+ const original = `${JSON.stringify(legacyConfig, null, 2)}\n`;
+
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true),
+ undefined,
+ "legacy",
+ legacyConfig,
+ 0o600,
+ { legacyParentSymlink: true },
+ );
+
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain("legacy MCP config parent is unsafe: symbolic link");
+ expect(rollback.legacyParentIsSymlink).toBe(true);
+ expect(rollback.legacyParentTargetText).toBe(original);
+ });
+
it("keeps v2 teardown and rollback isolated from the legacy user file", () => {
const managedServer = {
type: "http",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index 94135102f84..ff758362065 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -34,7 +34,7 @@ export function buildDeepAgentsMcpRemoveCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys, tempfile",
+ "import json, os, pathlib, stat, sys",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`managed_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
`legacy_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_LEGACY_MCP_CONFIG_PATH)})`,
@@ -64,12 +64,17 @@ export function buildDeepAgentsMcpRemoveCommand(
"managed_parent_identity = None",
"managed_parent_descriptor = None",
"legacy_identity = None",
+ "legacy_descriptor = None",
+ "legacy_parent_identity = None",
+ "legacy_parent_descriptor = None",
"def finish(outcome):",
" close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
" close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
+ " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
@@ -77,26 +82,6 @@ export function buildDeepAgentsMcpRemoveCommand(
" write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
- "def write_legacy_data(value):",
- " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
- " try:",
- " os.fchmod(tmp_fd, 0o600)",
- " with os.fdopen(tmp_fd, 'w', encoding='utf-8') as tmp_file:",
- " json.dump(value, tmp_file, indent=2, sort_keys=True)",
- " tmp_file.write('\\n')",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
- " assert_legacy_source_stable(config_path, legacy_identity)",
- " if legacy_identity is None:",
- " os.link(tmp_name, config_path, follow_symlinks=False)",
- " os.unlink(tmp_name)",
- " else:",
- " os.replace(tmp_name, config_path)",
- " finally:",
- " try:",
- " os.unlink(tmp_name)",
- " except FileNotFoundError:",
- " pass",
"if is_v2:",
" try:",
" raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
@@ -112,12 +97,14 @@ export function buildDeepAgentsMcpRemoveCommand(
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
- " if not os.path.lexists(config_path):",
- " finish('absent')",
" try:",
- " data, legacy_identity = read_legacy_config(config_path)",
- " except (OSError, UnicodeDecodeError, ValueError):",
+ " data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity = load_legacy_config_for_update(config_path)",
+ " except (OSError, UnicodeDecodeError, ValueError) as exc:",
+ " if str(exc).startswith('legacy MCP config parent is unsafe'):",
+ " fail_teardown(f'Refusing unsafe legacy MCP teardown at {config_path}: {exc}')",
" finish('unowned')",
+ " if legacy_descriptor is None:",
+ " finish('absent')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
" repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
@@ -163,17 +150,16 @@ export function buildDeepAgentsMcpRemoveCommand(
" write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
- " write_legacy_data(data)",
- " persisted = read_legacy_config(config_path)[0]",
+ " persisted = write_legacy_config(config_path, data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
" except (OSError, UnicodeDecodeError, ValueError) as exc:",
" fail_teardown(f'MCP teardown mutation failed at {config_path}: {exc}')",
" if persisted != data:",
" fail_teardown(f'MCP teardown verification failed at {config_path}')",
"else:",
- " assert_legacy_source_stable(config_path, legacy_identity)",
- " config_path.unlink()",
- " if os.path.lexists(config_path):",
- " fail_teardown(f'Managed MCP teardown verification failed at {config_path}')",
+ " try:",
+ " remove_legacy_config(config_path, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
+ " except (OSError, ValueError) as exc:",
+ " fail_teardown(f'MCP teardown mutation failed at {config_path}: {exc}')",
"finish('removed')",
"PY",
].join("\n");
diff --git a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
index 248f4caa262..c1120eb33f6 100644
--- a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
+++ b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
@@ -7,42 +7,138 @@ export const DEEPAGENTS_LEGACY_CONFIG_HELPERS = [
"LEGACY_MCP_MAX_BYTES = 262144",
"def legacy_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def read_legacy_config(path):",
+ "def validate_legacy_descriptor_path(entry_name, parent_descriptor, descriptor):",
+ " opened = os.fstat(descriptor)",
+ " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " safe = (stat.S_ISREG(opened.st_mode) and opened.st_uid == os.getuid() and stat.S_IMODE(opened.st_mode) == 0o600 and opened.st_nlink == 1 and (opened.st_dev, opened.st_ino) == (linked.st_dev, linked.st_ino))",
+ " if not safe:",
+ " raise ValueError('legacy MCP config has unsafe ownership, mode, type, or links')",
+ " return legacy_fingerprint(opened)",
+ "def read_legacy_descriptor(entry_name, parent_descriptor, descriptor):",
+ " before = os.fstat(descriptor)",
+ " validate_legacy_descriptor_path(entry_name, parent_descriptor, descriptor)",
+ " if before.st_size <= 0 or before.st_size > LEGACY_MCP_MAX_BYTES:",
+ " raise ValueError('legacy MCP config has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " stable = (legacy_fingerprint(before) == legacy_fingerprint(after) and legacy_fingerprint(after) == legacy_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('legacy MCP config changed while reading')",
+ " return strict_json_loads(b''.join(chunks).decode('utf-8')), legacy_fingerprint(after)",
+ "def close_legacy_config_descriptors(descriptor, parent_descriptor):",
+ " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ "def load_legacy_config_for_update(path, create_parent=False):",
+ " unsafe_parent_prefix = 'legacy MCP config parent is unsafe'",
+ " try:",
+ " parent_descriptor, parent_identity, _ = open_managed_parent(path, create_parent, unsafe_parent_prefix)",
+ " except FileNotFoundError:",
+ " return {}, None, None, None, None",
+ " entry_name = path.name",
" flags = os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
- " descriptor = os.open(path, flags)",
" try:",
- " before = os.fstat(descriptor)",
- " linked = os.stat(path, follow_symlinks=False)",
- " safe = (stat.S_ISREG(before.st_mode) and before.st_uid == os.getuid() and stat.S_IMODE(before.st_mode) == 0o600 and before.st_nlink == 1 and (before.st_dev, before.st_ino) == (linked.st_dev, linked.st_ino))",
- " if not safe:",
- " raise ValueError('legacy MCP config has unsafe ownership, mode, type, or links')",
- " if before.st_size <= 0 or before.st_size > LEGACY_MCP_MAX_BYTES:",
- " raise ValueError('legacy MCP config has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(path, follow_symlinks=False)",
- " stable = (legacy_fingerprint(before) == legacy_fingerprint(after) and legacy_fingerprint(after) == legacy_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('legacy MCP config changed while reading')",
+ " try:",
+ " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(entry_name, parent_descriptor, None)",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_parent_prefix)",
+ " return {}, None, None, parent_descriptor, parent_identity",
+ " try:",
+ " data, identity = read_legacy_descriptor(entry_name, parent_descriptor, descriptor)",
+ " assert_managed_parent_stable(path, parent_identity, unsafe_parent_prefix)",
+ " return data, identity, descriptor, parent_descriptor, parent_identity",
+ " except Exception:",
+ " os.close(descriptor)",
+ " raise",
+ " except Exception:",
+ " os.close(parent_descriptor)",
+ " raise",
+ "def read_legacy_config(path):",
+ " data, identity, descriptor, parent_descriptor, _ = load_legacy_config_for_update(path)",
+ " try:",
+ " if descriptor is None:",
+ " raise FileNotFoundError(path)",
+ " return data, identity",
" finally:",
- " os.close(descriptor)",
- " raw = b''.join(chunks).decode('utf-8')",
- " data = strict_json_loads(raw)",
- " return data, legacy_fingerprint(before)",
- "def assert_legacy_source_stable(path, identity):",
+ " close_legacy_config_descriptors(descriptor, parent_descriptor)",
+ "def assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity):",
+ " if parent_descriptor is None or parent_identity is None:",
+ " raise ValueError('legacy MCP config parent descriptor is unavailable')",
+ " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
+ " assert_managed_source_stable(path.name, parent_descriptor, identity)",
" if identity is None:",
- " if os.path.lexists(path):",
- " raise ValueError('legacy MCP config appeared during mutation')",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected legacy MCP config descriptor')",
" return",
- " current = os.stat(path, follow_symlinks=False)",
- " safe = (stat.S_ISREG(current.st_mode) and current.st_uid == os.getuid() and stat.S_IMODE(current.st_mode) == 0o600 and current.st_nlink == 1 and legacy_fingerprint(current) == identity)",
- " if not safe:",
+ " if descriptor is None or validate_legacy_descriptor_path(path.name, parent_descriptor, descriptor) != identity:",
" raise ValueError('legacy MCP config changed before mutation')",
+ "def legacy_config_bytes(value):",
+ " payload = (json.dumps(value, indent=2, sort_keys=True) + '\\n').encode('utf-8')",
+ " if not payload or len(payload) > LEGACY_MCP_MAX_BYTES:",
+ " raise ValueError('legacy MCP config has invalid rendered size')",
+ " return payload",
+ "def create_legacy_config_temp(parent_descriptor):",
+ " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
+ " for _ in range(16):",
+ " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
+ " try:",
+ " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
+ " except FileExistsError:",
+ " pass",
+ " raise FileExistsError('could not allocate legacy MCP config temporary file')",
+ "def write_legacy_config(path, value, identity, descriptor, parent_descriptor, parent_identity):",
+ " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
+ " payload = legacy_config_bytes(value)",
+ " tmp_fd, tmp_name = create_legacy_config_temp(parent_descriptor)",
+ " try:",
+ " offset = 0",
+ " while offset < len(payload):",
+ " written = os.write(tmp_fd, payload[offset:])",
+ " if written <= 0:",
+ " raise OSError('legacy MCP config write made no progress')",
+ " offset += written",
+ " os.fsync(tmp_fd)",
+ " os.close(tmp_fd)",
+ " tmp_fd = None",
+ " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
+ " if identity is None:",
+ " try:",
+ " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('legacy MCP config appeared during publication') from exc",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " else:",
+ " os.replace(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor)",
+ " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
+ " verification = os.open(path.name, os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW, dir_fd=parent_descriptor)",
+ " try:",
+ " persisted, _ = read_legacy_descriptor(path.name, parent_descriptor, verification)",
+ " finally:",
+ " os.close(verification)",
+ " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
+ " return persisted",
+ " finally:",
+ " if tmp_fd is not None:",
+ " os.close(tmp_fd)",
+ " try:",
+ " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " except FileNotFoundError:",
+ " pass",
+ "def remove_legacy_config(path, identity, descriptor, parent_descriptor, parent_identity):",
+ " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
+ " os.unlink(path.name, dir_fd=parent_descriptor)",
+ " try:",
+ " os.stat(path.name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " except FileNotFoundError:",
+ " pass",
+ " else:",
+ " raise ValueError('legacy MCP config removal verification failed')",
+ " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
];
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 41d1294e8d9..1f43ee8c7ad 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -34,6 +34,8 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
+ legacyParentIsSymlink: boolean;
+ legacyParentTargetText: string | null;
managedParentIsSymlink: boolean;
managedParentTargetText: string | null;
managedRaceIterations: number;
@@ -46,6 +48,7 @@ export interface DeepAgentsManagedFixtureOptions {
danglingSymlink?: boolean;
directory?: boolean;
fifo?: boolean;
+ legacyParentSymlink?: boolean;
mode?: number;
parentSymlink?: boolean;
raceAbsentPublication?: string;
@@ -326,6 +329,9 @@ export function runDeepAgentsConfigCommand(
const managedTargetReadReady = path.join(tmp, "managed-target-read-ready");
const managedTargetReadAccess = path.join(tmp, "managed-target-read-accessed");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
+ const legacyParentPath = path.dirname(legacyConfigPath);
+ const legacyParentTarget = path.join(tmp, "legacy-parent-target");
+ const legacyParentTargetConfig = path.join(legacyParentTarget, path.basename(legacyConfigPath));
const initializeConfig = (
target: string,
value: Record | string | undefined,
@@ -340,6 +346,20 @@ export function runDeepAgentsConfigCommand(
);
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
+ if (
+ managedOptions.legacyParentSymlink &&
+ (initialConfig !== undefined ||
+ managedOptions.danglingSymlink ||
+ managedOptions.directory ||
+ managedOptions.fifo ||
+ managedOptions.parentSymlink ||
+ managedOptions.raceAbsentPublication !== undefined ||
+ managedOptions.raceProjection !== undefined ||
+ managedOptions.socket ||
+ managedOptions.symlink)
+ ) {
+ throw new Error("legacyParentSymlink cannot share the managed projection parent fixture");
+ }
const managedTargetReadPath = managedOptions.targetReadProbe
? managedOptions.parentSymlink
? managedParentTargetConfig
@@ -387,8 +407,13 @@ export function runDeepAgentsConfigCommand(
if (managedOptions.raceAbsentPublication !== undefined) {
initializeConfig(managedSymlinkTarget, managedOptions.raceAbsentPublication);
}
- initializeConfig(legacyConfigPath, initialLegacyConfig);
- if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
+ if (managedOptions.legacyParentSymlink) {
+ fs.mkdirSync(legacyParentTarget, { recursive: true });
+ initializeConfig(legacyParentTargetConfig, initialLegacyConfig, initialLegacyMode);
+ fs.symlinkSync(legacyParentTarget, legacyParentPath);
+ } else {
+ initializeConfig(legacyConfigPath, initialLegacyConfig, initialLegacyMode);
+ }
let targetReadProbe: ReturnType | null = null;
let managedRace: ManagedFixtureProcess | null = null;
let socketFixture: ManagedFixtureProcess | null = null;
@@ -469,6 +494,12 @@ export function runDeepAgentsConfigCommand(
// A missing projection parent cannot redirect the managed path.
}
const legacyConfigExists = fs.existsSync(legacyConfigPath);
+ let legacyParentIsSymlink = false;
+ try {
+ legacyParentIsSymlink = fs.lstatSync(legacyParentPath).isSymbolicLink();
+ } catch {
+ // A missing legacy parent cannot redirect the legacy config path.
+ }
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
const configIsSymlink = configStat?.isSymbolicLink() === true;
@@ -501,6 +532,7 @@ export function runDeepAgentsConfigCommand(
const managedTargetReadAccessed =
managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
+ const legacyParentTargetText = readRegularFile(legacyParentTargetConfig);
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
try {
@@ -526,6 +558,8 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
+ legacyParentIsSymlink,
+ legacyParentTargetText,
managedParentIsSymlink,
managedParentTargetText,
managedRaceIterations,
From dbf5c9fd68d07fb32c99d6cdc9e97d483eaa7ea7 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 05:06:47 -0700
Subject: [PATCH 34/49] test(policy): mock typed read failure
---
test/runtime/policy/policies.test.ts | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/test/runtime/policy/policies.test.ts b/test/runtime/policy/policies.test.ts
index 283d2c912a8..3a5c1132e1b 100644
--- a/test/runtime/policy/policies.test.ts
+++ b/test/runtime/policy/policies.test.ts
@@ -616,10 +616,16 @@ exit 1
it("applyPreset does not create temp dirs when bounded policy observation loses OpenShell", () => {
const policyTempPrefix = path.join(os.tmpdir(), "nemoclaw-policy-");
- const resolveSpy = vi
- .spyOn(resolveOpenshellModule, "resolveOpenshell")
- .mockReturnValueOnce(fakeOpenshell)
- .mockReturnValue(null);
+ const policyReadSpy = vi
+ .spyOn(policyReaderModule.syncCliOpenShellSandboxPolicyReader, "readSandboxPolicy")
+ .mockReturnValue({
+ ok: false,
+ error: {
+ kind: "command",
+ reason: "failed",
+ message: "OpenShell became unavailable during the bounded policy read.",
+ },
+ });
const mkdtempSpy = vi.spyOn(fs, "mkdtempSync");
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -639,7 +645,7 @@ exit 1
mkdtempSpy.mock.calls.filter(([prefix]) => String(prefix).startsWith(policyTempPrefix)),
).toEqual([]);
} finally {
- resolveSpy.mockRestore();
+ policyReadSpy.mockRestore();
mkdtempSpy.mockRestore();
errSpy.mockRestore();
logSpy.mockRestore();
From 07d8cb43fb2ef6c239f7b9193cc7842273ea110e Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 05:30:12 -0700
Subject: [PATCH 35/49] refactor(mcp): share Deep Agents runtime selection
---
.../mcp-bridge-adapter-deepagents-legacy.ts | 15 +---
...-adapter-deepagents-runtime-guards.test.ts | 84 ++++++++++++-------
.../mcp-bridge-adapter-deepagents-teardown.ts | 15 +---
.../sandbox/mcp-bridge-adapter-status.ts | 15 +---
.../mcp-bridge/deepagents-legacy-config.ts | 16 ++++
5 files changed, 80 insertions(+), 65 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index ae22f2e3c1e..659c8ac2996 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -10,6 +10,7 @@ import {
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
+ DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
MANAGED_HTTP_SERVER_MATCH_HELPERS,
@@ -47,19 +48,9 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
+ ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "auto"`,
- "if runtime_kind == 'auto':",
- " runtime_kind = 'unknown'",
- " try:",
- " from deepagents_code import _nemoclaw_managed as managed",
- " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
- " if runtime_path == str(managed_path):",
- " runtime_kind = 'v2'",
- " elif runtime_path == str(legacy_path):",
- " runtime_kind = 'legacy'",
- " except Exception:",
- " pass",
+ "runtime_kind = select_deepagents_runtime_kind('auto', managed_path, legacy_path)",
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime; refusing rollback', file=sys.stderr)",
" raise SystemExit(2)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
index 73ad1997741..49958524dc3 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
@@ -10,38 +10,64 @@ import {
buildDeepAgentsMcpRegisterCommand,
buildDeepAgentsMcpRemoveCommand,
} from "./mcp-bridge-adapter-deepagents";
+import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
describe("Deep Agents MCP config adapter runtime guards", () => {
- it.each(
- Array.from(
- [
- buildDeepAgentsMcpRemoveCommand(baseEntry, true, true),
- buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true),
- ],
- (value) => [value],
- ),
- )(
- "fails closed without touching either config when the runtime generation is unknown [case %#]",
- (command) => {
- const v2Config = {
- mcpServers: {
- github: {
- type: "http",
- url: baseEntry.url,
- headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
- },
- },
- };
- const legacyConfig = {
- mcpServers: { local: { type: "stdio", command: "user-owned" } },
- ui: { theme: "dark" },
- };
+ const managedServer = {
+ type: "http",
+ url: baseEntry.url,
+ headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
+ };
+ const userServer = { type: "stdio", command: "user-owned" };
+ const registeredV2Config = { mcpServers: { github: managedServer } };
+ const registeredLegacyConfig = {
+ mcpServers: { github: managedServer, local: userServer },
+ ui: { theme: "dark" },
+ };
+ const driftedLegacyConfig = {
+ mcpServers: { github: { type: "http", url: "https://user.example/mcp" } },
+ };
+ const emptyV2Config = { mcpServers: {} };
+ const emptyLegacyConfig = {
+ mcpServers: { local: userServer },
+ ui: { theme: "dark" },
+ };
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true, true);
+ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true);
+ const runtimeSelectionCases = [
+ ["status", "v2", statusCommand, registeredV2Config, driftedLegacyConfig, [0, "registered", "", true, true]],
+ ["status", "legacy", statusCommand, registeredV2Config, driftedLegacyConfig, [0, "mismatch", "", true, true]],
+ ["status", "unknown", statusCommand, registeredV2Config, driftedLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime", true, true]],
+ ["adaptive teardown", "v2", removalCommand, registeredV2Config, registeredLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=removed", "", false, true]],
+ ["adaptive teardown", "legacy", removalCommand, registeredV2Config, registeredLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=removed", "", true, false]],
+ ["adaptive teardown", "unknown", removalCommand, registeredV2Config, registeredLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime; refusing teardown", true, true]],
+ ["rollback", "v2", rollbackCommand, emptyV2Config, emptyLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1", "", true, false]],
+ ["rollback", "legacy", rollbackCommand, emptyV2Config, emptyLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1", "", false, true]],
+ ["rollback", "unknown", rollbackCommand, emptyV2Config, emptyLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime; refusing rollback", false, false]],
+ ] as const;
+ const hasGithubServer = (config: Record | null): boolean => {
+ const servers = config?.mcpServers;
+ return typeof servers === "object" && servers !== null && Object.hasOwn(servers, "github");
+ };
+
+ it.each(runtimeSelectionCases)(
+ "uses the shared runtime marker for %s: %s",
+ (_operation, runtimeKind, command, initialConfig, initialLegacyConfig, expected) => {
+ const result = runDeepAgentsConfigCommand(
+ command,
+ initialConfig,
+ runtimeKind,
+ initialLegacyConfig,
+ );
- const result = runDeepAgentsConfigCommand(command, v2Config, "unknown", legacyConfig);
- expect(result.status).toBe(2);
- expect(result.stderr).toContain("Could not identify the managed Deep Agents MCP runtime");
- expect(result.config).toEqual(v2Config);
- expect(result.legacyConfig).toEqual(legacyConfig);
+ expect([
+ result.status,
+ result.stdout.trim(),
+ result.stderr.trim(),
+ hasGithubServer(result.config),
+ hasGithubServer(result.legacyConfig),
+ ]).toEqual(expected);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index ff758362065..d62099bdfb7 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -6,6 +6,7 @@ import { runDeepAgentsAdapterCommand } from "./mcp-bridge-adapter-deepagents-com
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
+ DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
@@ -41,19 +42,9 @@ export function buildDeepAgentsMcpRemoveCommand(
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
+ ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "${adaptiveTeardown ? "auto" : "v2"}"`,
- "if runtime_kind == 'auto':",
- " runtime_kind = 'unknown'",
- " try:",
- " from deepagents_code import _nemoclaw_managed as managed",
- " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
- " if runtime_path == str(managed_path):",
- " runtime_kind = 'v2'",
- " elif runtime_path == str(legacy_path):",
- " runtime_kind = 'legacy'",
- " except Exception:",
- " pass",
+ `runtime_kind = select_deepagents_runtime_kind("${adaptiveTeardown ? "auto" : "v2"}", managed_path, legacy_path)`,
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime; refusing teardown', file=sys.stderr)",
" raise SystemExit(2)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index 3bca5c8c498..a6c9ceb281c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -6,6 +6,7 @@ import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readin
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
+ DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
@@ -242,19 +243,9 @@ export function buildDeepAgentsMcpStatusCommand(
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
+ ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = "auto"`,
- "if runtime_kind == 'auto':",
- " runtime_kind = 'unknown'",
- " try:",
- " from deepagents_code import _nemoclaw_managed as managed",
- " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
- " if runtime_path == str(managed_path):",
- " runtime_kind = 'v2'",
- " elif runtime_path == str(legacy_path):",
- " runtime_kind = 'legacy'",
- " except Exception:",
- " pass",
+ "runtime_kind = select_deepagents_runtime_kind('auto', managed_path, legacy_path)",
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime', file=sys.stderr)",
" raise SystemExit(2)",
diff --git a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
index c1120eb33f6..ab13b4eee74 100644
--- a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
+++ b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
@@ -3,6 +3,22 @@
export const DEEPAGENTS_LEGACY_MCP_CONFIG_PATH = "/sandbox/.deepagents/.mcp.json";
+export const DEEPAGENTS_RUNTIME_SELECTION_HELPERS = [
+ "def select_deepagents_runtime_kind(requested_kind, managed_path, legacy_path):",
+ " if requested_kind != 'auto':",
+ " return requested_kind",
+ " try:",
+ " from deepagents_code import _nemoclaw_managed as managed",
+ " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
+ " if runtime_path == str(managed_path):",
+ " return 'v2'",
+ " if runtime_path == str(legacy_path):",
+ " return 'legacy'",
+ " except Exception:",
+ " pass",
+ " return 'unknown'",
+];
+
export const DEEPAGENTS_LEGACY_CONFIG_HELPERS = [
"LEGACY_MCP_MAX_BYTES = 262144",
"def legacy_fingerprint(metadata):",
From db783abb8844193dc20932c96d7c37ea46df17c3 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 05:57:18 -0700
Subject: [PATCH 36/49] fix(mcp): narrow projection status safety
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 5 +-
...adapter-deepagents-legacy-teardown.test.ts | 27 -
.../mcp-bridge-adapter-deepagents-legacy.ts | 62 ++-
...idge-adapter-deepagents-projection.test.ts | 324 ++++++------
...cp-bridge-adapter-deepagents-projection.ts | 294 ++++-------
...-bridge-adapter-deepagents-registration.ts | 10 +-
...bridge-adapter-deepagents-rollback.test.ts | 22 -
...-adapter-deepagents-runtime-guards.test.ts | 84 ++-
.../mcp-bridge-adapter-deepagents-teardown.ts | 85 ++--
.../sandbox/mcp-bridge-adapter-status.ts | 15 +-
.../mcp-bridge-status-resolution.test.ts | 36 +-
.../mcp-bridge/deepagents-legacy-config.ts | 176 ++-----
test/e2e-test.sh | 96 +---
.../mcp-bridge-adapter-deepagents-fixture.ts | 480 +++++++-----------
test/runtime/policy/policies.test.ts | 16 +-
15 files changed, 692 insertions(+), 1040 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 4a579130499..8a564cd373d 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,9 +42,8 @@ For a trusted private server, status resolves the endpoint without changing mana
Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
-The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, `non-regular file`, or `non-directory parent`.
-Other Deep Agents inspection failures can also exit with status `2`.
-Use the standard error diagnostic to distinguish an unsafe path from another inspection failure.
+The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
+Exit status `2` is specific to this unsafe path-type result.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
index 9c315445314..1a9bcfa3735 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy-teardown.test.ts
@@ -80,31 +80,4 @@ describe("Deep Agents MCP config adapter legacy teardown", () => {
ui: { theme: "dark" },
});
});
-
- it("refuses removal through a symlinked legacy parent", () => {
- const legacyConfig = {
- mcpServers: {
- github: {
- type: "http",
- url: baseEntry.url,
- headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
- },
- },
- };
- const original = `${JSON.stringify(legacyConfig, null, 2)}\n`;
-
- const removal = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRemoveCommand(baseEntry, false, true),
- undefined,
- "legacy",
- legacyConfig,
- 0o600,
- { legacyParentSymlink: true },
- );
-
- expect(removal.status).toBe(2);
- expect(removal.stderr).toContain("legacy MCP config parent is unsafe: symbolic link");
- expect(removal.legacyParentIsSymlink).toBe(true);
- expect(removal.legacyParentTargetText).toBe(original);
- });
});
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
index 659c8ac2996..50251a1de33 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-legacy.ts
@@ -10,7 +10,6 @@ import {
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
- DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
MANAGED_HTTP_SERVER_MATCH_HELPERS,
@@ -41,16 +40,26 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys",
+ "import json, os, pathlib, stat, sys, tempfile",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`managed_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
`legacy_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_LEGACY_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
- ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- "runtime_kind = select_deepagents_runtime_kind('auto', managed_path, legacy_path)",
+ `runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ "if runtime_kind == 'auto':",
+ " runtime_kind = 'unknown'",
+ " try:",
+ " from deepagents_code import _nemoclaw_managed as managed",
+ " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
+ " if runtime_path == str(managed_path):",
+ " runtime_kind = 'v2'",
+ " elif runtime_path == str(legacy_path):",
+ " runtime_kind = 'legacy'",
+ " except Exception:",
+ " pass",
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime; refusing rollback', file=sys.stderr)",
" raise SystemExit(2)",
@@ -62,22 +71,16 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
"data = {}",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
- "legacy_descriptor = None",
- "legacy_parent_identity = None",
- "legacy_parent_descriptor = None",
"def fail_rollback(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
- " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
" if is_v2:",
- " data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = load_managed_projection_for_update(config_path)",
- " else:",
- " data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity = load_legacy_config_for_update(config_path, True)",
+ " data, managed_identity, managed_descriptor = load_managed_projection_for_update(config_path)",
+ " elif os.path.lexists(config_path):",
+ " data, legacy_identity = read_legacy_config(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
" fail_rollback(f'Invalid managed MCP rollback state at {config_path}: {exc}')",
"if not isinstance(data, dict):",
@@ -105,17 +108,36 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" if payload['server'] in servers and current != payload['expected']:",
" fail_rollback(f'Refusing to overwrite user-owned legacy MCP server at {config_path}')",
" servers[payload['server']] = payload['expected']",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"if not is_v2:",
+ " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
" try:",
- " persisted = write_legacy_config(config_path, data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
- " except (OSError, ValueError) as exc:",
- " fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
+ " os.fchmod(tmp_fd, 0o600)",
+ " with os.fdopen(tmp_fd, 'w', encoding='utf-8') as tmp_file:",
+ " json.dump(data, tmp_file, indent=2, sort_keys=True)",
+ " tmp_file.write('\\n')",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " assert_legacy_source_stable(config_path, legacy_identity)",
+ " if legacy_identity is None:",
+ " os.link(tmp_name, config_path, follow_symlinks=False)",
+ " os.unlink(tmp_name)",
+ " else:",
+ " os.replace(tmp_name, config_path)",
+ " finally:",
+ " try:",
+ " os.unlink(tmp_name)",
+ " except FileNotFoundError:",
+ " pass",
"else:",
" try:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
- " persisted = read_managed_projection(config_path)[0]",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" except (OSError, ValueError) as exc:",
" fail_rollback(f'Could not publish managed MCP rollback state at {config_path}: {exc}')",
+ "try:",
+ " persisted = read_managed_projection(config_path)[0] if is_v2 else read_legacy_config(config_path)[0]",
+ "except (OSError, UnicodeDecodeError, ValueError) as exc:",
+ " fail_rollback(f'Could not verify managed MCP rollback state at {config_path}: {exc}')",
"if is_v2:",
" persisted_servers = persisted.get('mcpServers') if isinstance(persisted, dict) else None",
" restored = isinstance(persisted_servers, dict) and set(persisted_servers) == set(payload['expectedServers']) and all(managed_http_server_matches(persisted_servers.get(name), expected, True) for name, expected in payload['expectedServers'].items())",
@@ -124,8 +146,6 @@ export function buildDeepAgentsMcpRollbackRegisterCommand(
" restored = isinstance(persisted_servers, dict) and persisted_servers.get(payload['server']) == payload['expected']",
"if not restored:",
" fail_rollback(f'Managed MCP rollback verification failed at {config_path}')",
- "close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
- "close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
"print('NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1')",
"PY",
].join("\n");
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 994121103f8..f0a4ad5d8c5 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -15,7 +15,6 @@ import { DEEPAGENTS_MCP_MAX_SERVERS } from "./mcp-bridge-adapter-deepagents-proj
import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const emptyProjection = { mcpServers: {} };
-const emptyProjectionText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
const duplicateProjection = '{"mcpServers":{},"mcpServers":{"shadow":{}}}\n';
const attackerProjection = '{"mcpServers":{"attacker":{"type":"stdio"}}}\n';
const managedCommandTimeoutMs = 30_000;
@@ -25,36 +24,27 @@ const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [base
const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry);
describe("Deep Agents managed MCP projection safety", () => {
- it(
- "applies the shared server cap before normal and rollback v2 publication",
- { timeout: 60_000 },
- () => {
- const entries = Array.from(
- { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
- (_, index): McpBridgeEntry => ({
- ...baseEntry,
- server: `server${String(index)}`,
- env: [`SERVER_${String(index)}_TOKEN`],
- }),
- );
+ it("applies the shared server cap before normal and rollback v2 publication", () => {
+ const entries = Array.from(
+ { length: DEEPAGENTS_MCP_MAX_SERVERS + 1 },
+ (_, index): McpBridgeEntry => ({
+ ...baseEntry,
+ server: `server${String(index)}`,
+ env: [`SERVER_${String(index)}_TOKEN`],
+ }),
+ );
- expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
- `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
- undefined,
- "v2",
- undefined,
- 0o600,
- { timeoutMs: managedCommandTimeoutMs },
- );
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain(
- `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
- );
- },
- );
+ expect(() => buildDeepAgentsMcpRegisterCommand(entries[0], false, entries)).toThrow(
+ `at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ const rollback = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpRegisterCommand(entries[0], true, entries, true),
+ );
+ expect(rollback.status).toBe(2);
+ expect(rollback.stderr).toContain(
+ `supports at most ${String(DEEPAGENTS_MCP_MAX_SERVERS)} servers`,
+ );
+ });
it.each([
{
@@ -146,47 +136,40 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.managedTargetReadAccessed).toBe(false);
});
- it("rejects an existing socket projection path (#10754)", () => {
- const result = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- undefined,
- "v2",
- undefined,
- 0o600,
- { socket: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: non-regular file",
- );
- expect(result.configIsSocket).toBe(true);
- });
-
it.each([
{
+ diagnostic: "symbolic link",
expectedConfigIsFifo: false,
+ expectedConfigIsSocket: false,
expectedConfigIsSymlink: true,
- expectedSafetyDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
- expectedTargetText: emptyProjectionText,
- raceProjection: "symlink" as const,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ replacementType: "symlink" as const,
},
{
+ diagnostic: "FIFO",
expectedConfigIsFifo: true,
+ expectedConfigIsSocket: false,
expectedConfigIsSymlink: false,
- expectedSafetyDiagnostic: "Unsafe managed Deep Agents MCP projection path: FIFO",
expectedTargetText: null,
- raceProjection: "fifo" as const,
+ replacementType: "fifo" as const,
+ },
+ {
+ diagnostic: "non-regular file",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSocket: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ replacementType: "socket" as const,
},
])(
- "rejects or safely loses a concurrent $raceProjection projection race (#10754)",
- { timeout: 60_000 },
+ "rejects a $replacementType replacement before projection open (#10754)",
({
+ diagnostic,
expectedConfigIsFifo,
+ expectedConfigIsSocket,
expectedConfigIsSymlink,
- expectedSafetyDiagnostic,
expectedTargetText,
- raceProjection,
+ replacementType,
}) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
@@ -194,69 +177,139 @@ describe("Deep Agents managed MCP projection safety", () => {
"v2",
undefined,
0o600,
- { raceProjection, timeoutMs: managedCommandTimeoutMs },
+ { swapOnManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
);
-
- expect(result.managedRaceIterations).toBeGreaterThan(0);
- const actualOutcome = {
- status: result.status,
- stderr: result.stderr.trim(),
- stdout: result.stdout.trim(),
- };
- const safelyLostRace =
- actualOutcome.status === 0 &&
- actualOutcome.stderr === "" &&
- actualOutcome.stdout === "absent";
- const safelyRejectedRace =
- actualOutcome.status === 2 &&
- actualOutcome.stdout === "" &&
- (actualOutcome.stderr === expectedSafetyDiagnostic ||
- /^Could not inspect managed Deep Agents MCP state at .+: managed MCP projection has unsafe ownership, mode, type, links, or path identity$/u.test(
- actualOutcome.stderr,
- ));
- expect(safelyLostRace || safelyRejectedRace, JSON.stringify(actualOutcome)).toBe(true);
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSocket).toBe(expectedConfigIsSocket);
expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ it.each([
+ {
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ replacementType: "symlink" as const,
+ },
+ {
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ replacementType: "fifo" as const,
+ },
+ ])(
+ "rejects a $replacementType replacement after projection open (#10754)",
+ ({
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ replacementType,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { swapAfterManagedOpen: replacementType, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
},
);
- it("rejects a symlinked projection parent without reading its target (#10754)", () => {
- const status = runDeepAgentsConfigCommand(
+ it("rejects a symbolic-link replacement while reading the projection (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
undefined,
+ 0o600,
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(result.configIsSymlink).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ });
+
+ it("preserves conclusive ELOOP classification across a follow-up path swap (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
"v2",
undefined,
0o600,
- { parentSymlink: true, targetReadProbe: true, timeoutMs: managedCommandTimeoutMs },
+ {
+ swapAfterManagedEloop: true,
+ symlink: true,
+ timeoutMs: managedCommandTimeoutMs,
+ },
);
- expect(status.status).toBe(2);
- expect(status.stdout.trim()).toBe("");
- expect(status.stderr).toContain(
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
"Unsafe managed Deep Agents MCP projection path: symbolic link",
);
- expect(status.managedParentIsSymlink).toBe(true);
- expect(status.managedTargetReadAccessed).toBe(false);
+ expect(result.configIsSymlink).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
});
it.each([
- ["registration", registrationCommand],
- ["forced removal", buildDeepAgentsMcpRemoveCommand(baseEntry, true)],
+ {
+ appearedType: "symlink" as const,
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ expectedTargetText: `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ },
+ {
+ appearedType: "fifo" as const,
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ expectedTargetText: null,
+ },
])(
- "rejects a symlinked projection parent during %s without mutating its target (#10754)",
- (_operation, command) => {
- const initialText = `${JSON.stringify(emptyProjection, null, 2)}\n`;
- const mutation = runDeepAgentsConfigCommand(
- command,
+ "classifies a $diagnostic that appears after a missing-path open (#10754)",
+ ({ appearedType, diagnostic }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
"v2",
undefined,
0o600,
- { parentSymlink: true },
+ { swapAfterMissingManagedOpen: appearedType, timeoutMs: managedCommandTimeoutMs },
+ );
+
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
+ expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(
+ appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
);
- expect(mutation.status).toBe(2);
- expect(mutation.managedParentIsSymlink).toBe(true);
- expect(mutation.managedParentTargetText).toBe(initialText);
},
);
@@ -312,64 +365,49 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(symlink.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
});
- it(
- "preserves a projection that appears during absent publication (#10754)",
- { timeout: 60_000 },
- () => {
- const absentResult = runDeepAgentsConfigCommand(
- registrationCommand,
- undefined,
- "v2",
- undefined,
- 0o600,
- { raceAbsentPublication: attackerProjection, timeoutMs: managedCommandTimeoutMs },
- );
- expect(absentResult.managedRaceIterations).toBeGreaterThan(0);
- expect(absentResult.status).toBe(2);
- expect(absentResult.configIsSymlink).toBe(true);
- expect(absentResult.managedSymlinkTargetText).toBe(attackerProjection);
- },
- );
+ it("preserves a projection that appears during absent publication", () => {
+ const absentResult = runDeepAgentsConfigCommand(
+ registrationCommand,
+ undefined,
+ "v2",
+ undefined,
+ 0o600,
+ { swapBeforeManagedLink: attackerProjection, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(absentResult.status).toBe(2);
+ expect(absentResult.stderr).toContain("appeared during publication");
+ expect(absentResult.configText).toBe(attackerProjection);
+ });
- it("preserves a replacement during a descriptor rewrite (#10754)", { timeout: 60_000 }, () => {
+ it("preserves a replacement during a descriptor rewrite", () => {
const existingResult = runDeepAgentsConfigCommand(
registrationCommand,
emptyProjection,
"v2",
undefined,
0o600,
- { raceProjection: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(existingResult.managedRaceIterations).toBeGreaterThan(0);
- expect([0, 2]).toContain(existingResult.status);
- expect(existingResult.configIsSymlink).toBe(true);
- expect(existingResult.managedSymlinkTargetText).toBe(
- `${JSON.stringify(emptyProjection, null, 2)}\n`,
+ { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
);
+ expect(existingResult.status).toBe(2);
+ expect(existingResult.stderr).toContain("links, or path identity");
+ expect(existingResult.configText).toBe(attackerProjection);
});
- it(
- "keeps forced removal identity-bound during a descriptor rewrite (#10754)",
- { timeout: 60_000 },
- () => {
- const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
- const raced = runDeepAgentsConfigCommand(
- forcedCommand,
- { ui: { theme: "dark" } },
- "v2",
- undefined,
- 0o600,
- { raceProjection: "symlink", timeoutMs: managedCommandTimeoutMs },
- );
- expect(raced.managedRaceIterations).toBeGreaterThan(0);
- expect([0, 2]).toContain(raced.status);
- expect(raced.stderr).not.toContain("Traceback");
- expect(raced.configIsSymlink).toBe(true);
- expect(raced.managedSymlinkTargetText).toBe(
- `${JSON.stringify({ ui: { theme: "dark" } }, null, 2)}\n`,
- );
- },
- );
+ it("keeps forced removal identity-bound during a descriptor rewrite (#10754)", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
+ const raced = runDeepAgentsConfigCommand(
+ forcedCommand,
+ { ui: { theme: "dark" } },
+ "v2",
+ undefined,
+ 0o600,
+ { swapOnManagedSeek: attackerProjection, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(raced.status).toBe(2);
+ expect(raced.stderr).toContain("Refusing unsafe managed MCP v2 repair");
+ expect(raced.stderr).not.toContain("Traceback");
+ expect(raced.configText).toBe(attackerProjection);
+ });
it("preserves a projection symlink during forced removal", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index cd0378cd0b8..d484ec70a7c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,76 +28,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
- "def managed_parent_path_kind(mode):",
- " if stat.S_ISLNK(mode):",
- " return 'symbolic link'",
- " if not stat.S_ISDIR(mode):",
- " return 'non-directory parent'",
- " return None",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def managed_parent_fingerprint(metadata):",
- " return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid)",
- "def raise_managed_parent_error(parent_descriptor, component, exc, unsafe_path_prefix=None):",
- " unsafe_path_kind = None",
- " try:",
- " unsafe_path_kind = managed_parent_path_kind(os.stat(component, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
- " if unsafe_path_kind and unsafe_path_prefix:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " if unsafe_path_kind:",
- " raise ValueError(f'managed MCP projection parent is a {unsafe_path_kind}') from exc",
- " raise exc",
- "def open_managed_parent(path, create=False, unsafe_path_prefix=None):",
- " if not path.is_absolute() or not path.name or path.name in ('.', '..'):",
- " raise ValueError('managed MCP projection path must be absolute')",
- " components = path.parts[1:-1]",
- " descriptor = os.open('/', os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)",
- " created = False",
- " try:",
- " for index, component in enumerate(components):",
- " flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except FileNotFoundError:",
- " if not create or index != len(components) - 1:",
- " raise",
- " try:",
- " os.mkdir(component, 0o700, dir_fd=descriptor)",
- " created = True",
- " except FileExistsError:",
- " pass",
- " try:",
- " next_descriptor = os.open(component, flags, dir_fd=descriptor)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " except OSError as exc:",
- " raise_managed_parent_error(descriptor, component, exc, unsafe_path_prefix)",
- " os.close(descriptor)",
- " descriptor = next_descriptor",
- " metadata = os.fstat(descriptor)",
- " if not stat.S_ISDIR(metadata.st_mode):",
- " raise ValueError('managed MCP projection parent is not a directory')",
- " return descriptor, managed_parent_fingerprint(metadata), created",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
- "def assert_managed_parent_stable(path, identity, unsafe_path_prefix=None):",
- " try:",
- " descriptor, current, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " current = None",
- " descriptor = None",
+ "def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
" try:",
- " if current != identity:",
- " raise ValueError('managed MCP projection parent changed during operation')",
- " finally:",
- " if descriptor is not None:",
- " os.close(descriptor)",
- "def assert_managed_source_stable(entry_name, parent_descriptor, identity, unsafe_path_prefix=None):",
- " try:",
- " current_metadata = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " current_metadata = os.stat(path, follow_symlinks=False)",
" except FileNotFoundError:",
" current_metadata = None",
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
@@ -110,9 +45,9 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" return",
" if current != identity:",
" raise ValueError('managed MCP projection changed before mutation')",
- "def validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix=None):",
+ "def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
- " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
+ " linked = os.stat(path, follow_symlinks=False)",
" if unsafe_path_prefix:",
" unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
" if unsafe_path_kind:",
@@ -122,103 +57,90 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
" return managed_fingerprint(opened)",
"def open_managed_projection(path, writable=False, unsafe_path_prefix=None):",
- " try:",
- " parent_descriptor, parent_identity, _ = open_managed_parent(path, unsafe_path_prefix=unsafe_path_prefix)",
- " except FileNotFoundError:",
- " return b'', None, None, None, None",
- " entry_name = path.name",
" access = os.O_RDWR if writable else os.O_RDONLY",
" flags = access | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
" try:",
- " try:",
- " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " assert_managed_source_stable(entry_name, parent_descriptor, None, unsafe_path_prefix)",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b'', None, None, parent_descriptor, parent_identity",
- " except OSError as exc:",
- " unsafe_path_kind = None",
- " if unsafe_path_prefix:",
- " if exc.errno == errno.ELOOP:",
- " unsafe_path_kind = 'symbolic link'",
- " else:",
- " try:",
- " unsafe_path_kind = managed_projection_path_kind(os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False).st_mode)",
- " except OSError:",
- " pass",
+ " descriptor = os.open(path, flags)",
+ " except FileNotFoundError:",
+ " assert_managed_source_stable(path, None, unsafe_path_prefix)",
+ " return b'', None, None",
+ " except OSError as exc:",
+ " unsafe_path_kind = None",
+ " if unsafe_path_prefix:",
+ " if exc.errno == errno.ELOOP:",
+ " unsafe_path_kind = 'symbolic link'",
+ " else:",
+ " try:",
+ " unsafe_path_kind = managed_projection_path_kind(os.stat(path, follow_symlinks=False).st_mode)",
+ " except OSError:",
+ " pass",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
+ " raise",
+ " try:",
+ " before = os.fstat(descriptor)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
" if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}') from exc",
- " raise",
- " try:",
- " before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " validate_managed_descriptor_path(entry_name, parent_descriptor, descriptor, unsafe_path_prefix)",
- " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
- " raise ValueError('managed MCP projection has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
- " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
- " assert_managed_parent_stable(path, parent_identity, unsafe_path_prefix)",
- " return b''.join(chunks), managed_fingerprint(after), descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
+ " if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
+ " raise ValueError('managed MCP projection has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(path, follow_symlinks=False)",
+ " if unsafe_path_prefix:",
+ " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('managed MCP projection changed while reading')",
+ " return b''.join(chunks), managed_fingerprint(after), descriptor",
" except Exception:",
- " os.close(parent_descriptor)",
+ " os.close(descriptor)",
" raise",
"def decode_managed_projection(raw):",
" return strict_json_loads(raw.decode('utf-8')) if raw else {}",
- "def close_managed_projection_descriptors(descriptor, parent_descriptor):",
- " for current in (descriptor, parent_descriptor):",
- " if current is None:",
- " continue",
- " try:",
- " os.close(current)",
- " except OSError:",
- " pass",
+ "def close_managed_projection_descriptor(descriptor):",
+ " if descriptor is None:",
+ " return",
+ " try:",
+ " os.close(descriptor)",
+ " except OSError:",
+ " pass",
+ "def load_managed_projection_for_update(path):",
+ " raw, identity, descriptor = open_managed_projection(path, True)",
+ " try:",
+ " return decode_managed_projection(raw), identity, descriptor",
+ " except Exception:",
+ " close_managed_projection_descriptor(descriptor)",
+ " raise",
"def read_managed_projection(path, unsafe_path_prefix=None):",
- " raw, identity, descriptor, parent_descriptor, _ = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
+ " raw, identity, descriptor = open_managed_projection(path, unsafe_path_prefix=unsafe_path_prefix)",
" try:",
" return decode_managed_projection(raw), identity",
" finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
- "def load_managed_projection_for_update(path):",
- " raw, identity, descriptor, parent_descriptor, parent_identity = open_managed_projection(path, True)",
- " try:",
- " return decode_managed_projection(raw), identity, descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
- " raise",
"def managed_projection_bytes(value):",
" payload = (json.dumps(value, indent=2, sort_keys=True) + '\\n').encode('utf-8')",
" if not payload or len(payload) > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid rendered size')",
" return payload",
- "def rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " if identity is None or descriptor is None or parent_descriptor is None or parent_identity is None:",
+ "def rewrite_managed_projection(path, value, identity, descriptor):",
+ " if identity is None or descriptor is None:",
" raise ValueError('managed MCP projection descriptor is unavailable')",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, identity)",
+ " assert_managed_source_stable(path, identity)",
" payload = managed_projection_bytes(value)",
" os.lseek(descriptor, 0, os.SEEK_SET)",
" os.ftruncate(descriptor, 0)",
@@ -233,62 +155,40 @@ export const DEEPAGENTS_MANAGED_PROJECTION_MUTATION_HELPERS = [
" persisted = os.read(descriptor, len(payload) + 1)",
" if persisted != payload or os.fstat(descriptor).st_size != len(payload):",
" raise ValueError('managed MCP projection verification failed')",
- " validate_managed_descriptor_path(path.name, parent_descriptor, descriptor)",
- " assert_managed_parent_stable(path, parent_identity)",
- "def create_managed_projection_temp(parent_descriptor):",
- " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
- " for _ in range(16):",
- " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
+ " validate_managed_descriptor_path(path, descriptor)",
+ "def publish_absent_managed_projection(path, value):",
+ " assert_managed_source_stable(path, None)",
+ " payload = managed_projection_bytes(value)",
+ " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=path.parent)",
+ " try:",
+ " os.fchmod(tmp_fd, 0o600)",
+ " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
+ " tmp_file.write(payload)",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " try:",
+ " os.link(tmp_name, path, follow_symlinks=False)",
+ " except FileExistsError as exc:",
+ " raise ValueError('managed MCP projection appeared during publication') from exc",
+ " os.unlink(tmp_name)",
+ " finally:",
" try:",
- " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
- " except FileExistsError:",
+ " os.unlink(tmp_name)",
+ " except FileNotFoundError:",
" pass",
- " raise FileExistsError('could not allocate managed MCP projection temporary file')",
- "def publish_absent_managed_projection(path, value, parent_descriptor, parent_identity):",
- " owns_parent_descriptor = parent_descriptor is None",
- " if parent_descriptor is None:",
- " parent_descriptor, parent_identity, created = open_managed_parent(path, True)",
- " if not created:",
- " os.close(parent_descriptor)",
- " raise ValueError('managed MCP projection parent appeared during publication')",
- " try:",
- " if not owns_parent_descriptor:",
- " assert_managed_parent_stable(path, parent_identity)",
- " assert_managed_source_stable(path.name, parent_descriptor, None)",
- " payload = managed_projection_bytes(value)",
- " tmp_fd, tmp_name = create_managed_projection_temp(parent_descriptor)",
+ " persisted, _ = read_managed_projection(path)",
+ " if persisted != value:",
+ " raise ValueError('managed MCP projection verification failed')",
+ "def write_managed_projection(path, value, identity, descriptor):",
+ " if identity is None:",
+ " if descriptor is not None:",
+ " raise ValueError('unexpected managed MCP projection descriptor')",
+ " publish_absent_managed_projection(path, value)",
+ " else:",
" try:",
- " with os.fdopen(tmp_fd, 'wb') as tmp_file:",
- " tmp_file.write(payload)",
- " tmp_file.flush()",
- " os.fsync(tmp_file.fileno())",
- " try:",
- " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('managed MCP projection appeared during publication') from exc",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
+ " rewrite_managed_projection(path, value, identity, descriptor)",
" finally:",
- " try:",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " pass",
- " assert_managed_parent_stable(path, parent_identity)",
- " persisted, _ = read_managed_projection(path)",
- " if persisted != value:",
- " raise ValueError('managed MCP projection verification failed')",
- " finally:",
- " if owns_parent_descriptor:",
- " os.close(parent_descriptor)",
- "def write_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " try:",
- " if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected managed MCP projection descriptor')",
- " publish_absent_managed_projection(path, value, parent_descriptor, parent_identity)",
- " else:",
- " rewrite_managed_projection(path, value, identity, descriptor, parent_descriptor, parent_identity)",
- " finally:",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
+ " close_managed_projection_descriptor(descriptor)",
];
export const DEEPAGENTS_MANAGED_PROJECTION_HELPERS = [
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
index 5657a097489..0b9365de1b0 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts
@@ -52,20 +52,19 @@ export function buildDeepAgentsMcpRegisterCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys",
+ "import json, os, pathlib, stat, sys, tempfile",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`config_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
"source_descriptor = None",
- "source_parent_descriptor = None",
"def fail_registration(message):",
- " close_managed_projection_descriptors(source_descriptor, source_parent_descriptor)",
+ " close_managed_projection_descriptor(source_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
"try:",
- " data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity = load_managed_projection_for_update(config_path)",
+ " data, source_identity, source_descriptor = load_managed_projection_for_update(config_path)",
"except (OSError, UnicodeDecodeError, ValueError) as exc:",
` fail_registration(f'Invalid ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"if not isinstance(data, dict):",
@@ -96,8 +95,9 @@ export function buildDeepAgentsMcpRegisterCommand(
" else:",
" next_servers[name] = expected",
"data = {'mcpServers': next_servers}",
+ "config_path.parent.mkdir(parents=True, exist_ok=True)",
"try:",
- " write_managed_projection(config_path, data, source_identity, source_descriptor, source_parent_descriptor, source_parent_identity)",
+ " write_managed_projection(config_path, data, source_identity, source_descriptor)",
"except (OSError, ValueError) as exc:",
` fail_registration(f'Could not publish ${DEEPAGENTS_MCP_CONFIG_PATH}: {exc}')`,
"PY",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
index 59724441013..0cedb3f7642 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-rollback.test.ts
@@ -38,28 +38,6 @@ describe("Deep Agents MCP config adapter rollback", () => {
});
});
- it("refuses rollback through a symlinked legacy parent", () => {
- const legacyConfig = {
- mcpServers: { local: { type: "stdio", command: "user-owned" } },
- ui: { theme: "dark" },
- };
- const original = `${JSON.stringify(legacyConfig, null, 2)}\n`;
-
- const rollback = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true),
- undefined,
- "legacy",
- legacyConfig,
- 0o600,
- { legacyParentSymlink: true },
- );
-
- expect(rollback.status).toBe(2);
- expect(rollback.stderr).toContain("legacy MCP config parent is unsafe: symbolic link");
- expect(rollback.legacyParentIsSymlink).toBe(true);
- expect(rollback.legacyParentTargetText).toBe(original);
- });
-
it("keeps v2 teardown and rollback isolated from the legacy user file", () => {
const managedServer = {
type: "http",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
index 49958524dc3..73ad1997741 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-runtime-guards.test.ts
@@ -10,64 +10,38 @@ import {
buildDeepAgentsMcpRegisterCommand,
buildDeepAgentsMcpRemoveCommand,
} from "./mcp-bridge-adapter-deepagents";
-import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
describe("Deep Agents MCP config adapter runtime guards", () => {
- const managedServer = {
- type: "http",
- url: baseEntry.url,
- headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
- };
- const userServer = { type: "stdio", command: "user-owned" };
- const registeredV2Config = { mcpServers: { github: managedServer } };
- const registeredLegacyConfig = {
- mcpServers: { github: managedServer, local: userServer },
- ui: { theme: "dark" },
- };
- const driftedLegacyConfig = {
- mcpServers: { github: { type: "http", url: "https://user.example/mcp" } },
- };
- const emptyV2Config = { mcpServers: {} };
- const emptyLegacyConfig = {
- mcpServers: { local: userServer },
- ui: { theme: "dark" },
- };
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const removalCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true, true);
- const rollbackCommand = buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true);
- const runtimeSelectionCases = [
- ["status", "v2", statusCommand, registeredV2Config, driftedLegacyConfig, [0, "registered", "", true, true]],
- ["status", "legacy", statusCommand, registeredV2Config, driftedLegacyConfig, [0, "mismatch", "", true, true]],
- ["status", "unknown", statusCommand, registeredV2Config, driftedLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime", true, true]],
- ["adaptive teardown", "v2", removalCommand, registeredV2Config, registeredLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=removed", "", false, true]],
- ["adaptive teardown", "legacy", removalCommand, registeredV2Config, registeredLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=removed", "", true, false]],
- ["adaptive teardown", "unknown", removalCommand, registeredV2Config, registeredLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime; refusing teardown", true, true]],
- ["rollback", "v2", rollbackCommand, emptyV2Config, emptyLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1", "", true, false]],
- ["rollback", "legacy", rollbackCommand, emptyV2Config, emptyLegacyConfig, [0, "NEMOCLAW_DEEPAGENTS_MCP_ROLLBACK_RESTORED=1", "", false, true]],
- ["rollback", "unknown", rollbackCommand, emptyV2Config, emptyLegacyConfig, [2, "", "Could not identify the managed Deep Agents MCP runtime; refusing rollback", false, false]],
- ] as const;
- const hasGithubServer = (config: Record | null): boolean => {
- const servers = config?.mcpServers;
- return typeof servers === "object" && servers !== null && Object.hasOwn(servers, "github");
- };
-
- it.each(runtimeSelectionCases)(
- "uses the shared runtime marker for %s: %s",
- (_operation, runtimeKind, command, initialConfig, initialLegacyConfig, expected) => {
- const result = runDeepAgentsConfigCommand(
- command,
- initialConfig,
- runtimeKind,
- initialLegacyConfig,
- );
+ it.each(
+ Array.from(
+ [
+ buildDeepAgentsMcpRemoveCommand(baseEntry, true, true),
+ buildDeepAgentsMcpRegisterCommand(baseEntry, true, [baseEntry], true),
+ ],
+ (value) => [value],
+ ),
+ )(
+ "fails closed without touching either config when the runtime generation is unknown [case %#]",
+ (command) => {
+ const v2Config = {
+ mcpServers: {
+ github: {
+ type: "http",
+ url: baseEntry.url,
+ headers: { Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
+ },
+ },
+ };
+ const legacyConfig = {
+ mcpServers: { local: { type: "stdio", command: "user-owned" } },
+ ui: { theme: "dark" },
+ };
- expect([
- result.status,
- result.stdout.trim(),
- result.stderr.trim(),
- hasGithubServer(result.config),
- hasGithubServer(result.legacyConfig),
- ]).toEqual(expected);
+ const result = runDeepAgentsConfigCommand(command, v2Config, "unknown", legacyConfig);
+ expect(result.status).toBe(2);
+ expect(result.stderr).toContain("Could not identify the managed Deep Agents MCP runtime");
+ expect(result.config).toEqual(v2Config);
+ expect(result.legacyConfig).toEqual(legacyConfig);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
index d62099bdfb7..c27d89dc2f3 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-teardown.ts
@@ -6,7 +6,6 @@ import { runDeepAgentsAdapterCommand } from "./mcp-bridge-adapter-deepagents-com
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
- DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
@@ -35,16 +34,26 @@ export function buildDeepAgentsMcpRemoveCommand(
};
return [
"/opt/venv/bin/python3 -I - <<'PY'",
- "import json, os, pathlib, stat, sys",
+ "import json, os, pathlib, stat, sys, tempfile",
`payload = json.loads(${pythonJsonLiteral(payload)})`,
`managed_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_MCP_CONFIG_PATH)})`,
`legacy_path = pathlib.Path(${JSON.stringify(DEEPAGENTS_LEGACY_MCP_CONFIG_PATH)})`,
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
- ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- `runtime_kind = select_deepagents_runtime_kind("${adaptiveTeardown ? "auto" : "v2"}", managed_path, legacy_path)`,
+ `runtime_kind = "${adaptiveTeardown ? "auto" : "v2"}" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ "if runtime_kind == 'auto':",
+ " runtime_kind = 'unknown'",
+ " try:",
+ " from deepagents_code import _nemoclaw_managed as managed",
+ " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
+ " if runtime_path == str(managed_path):",
+ " runtime_kind = 'v2'",
+ " elif runtime_path == str(legacy_path):",
+ " runtime_kind = 'legacy'",
+ " except Exception:",
+ " pass",
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime; refusing teardown', file=sys.stderr)",
" raise SystemExit(2)",
@@ -52,30 +61,43 @@ export function buildDeepAgentsMcpRemoveCommand(
"config_path = managed_path if is_v2 else legacy_path",
"managed_identity = None",
"managed_descriptor = None",
- "managed_parent_identity = None",
- "managed_parent_descriptor = None",
"legacy_identity = None",
- "legacy_descriptor = None",
- "legacy_parent_identity = None",
- "legacy_parent_descriptor = None",
"def finish(outcome):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
- " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print('NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=' + outcome)",
" raise SystemExit(0)",
"def fail_teardown(message):",
- " close_managed_projection_descriptors(managed_descriptor, managed_parent_descriptor)",
- " close_legacy_config_descriptors(legacy_descriptor, legacy_parent_descriptor)",
+ " close_managed_projection_descriptor(managed_descriptor)",
" print(message, file=sys.stderr)",
" raise SystemExit(2)",
- "def repair_v2_projection(identity, descriptor, parent_descriptor, parent_identity):",
+ "def repair_v2_projection(identity, descriptor):",
" try:",
- " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor, parent_descriptor, parent_identity)",
+ " write_managed_projection(config_path, {'mcpServers': {}}, identity, descriptor)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Refusing unsafe managed MCP v2 repair at {config_path}: {exc}')",
+ "def write_legacy_data(value):",
+ " tmp_fd, tmp_name = tempfile.mkstemp(prefix='.nemoclaw-mcp.', dir=config_path.parent)",
+ " try:",
+ " os.fchmod(tmp_fd, 0o600)",
+ " with os.fdopen(tmp_fd, 'w', encoding='utf-8') as tmp_file:",
+ " json.dump(value, tmp_file, indent=2, sort_keys=True)",
+ " tmp_file.write('\\n')",
+ " tmp_file.flush()",
+ " os.fsync(tmp_file.fileno())",
+ " assert_legacy_source_stable(config_path, legacy_identity)",
+ " if legacy_identity is None:",
+ " os.link(tmp_name, config_path, follow_symlinks=False)",
+ " os.unlink(tmp_name)",
+ " else:",
+ " os.replace(tmp_name, config_path)",
+ " finally:",
+ " try:",
+ " os.unlink(tmp_name)",
+ " except FileNotFoundError:",
+ " pass",
"if is_v2:",
" try:",
- " raw, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity = open_managed_projection(config_path, True)",
+ " raw, managed_identity, managed_descriptor = open_managed_projection(config_path, True)",
" except (OSError, ValueError) as exc:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
" if managed_descriptor is None:",
@@ -84,21 +106,19 @@ export function buildDeepAgentsMcpRemoveCommand(
" data = decode_managed_projection(raw)",
" except (UnicodeDecodeError, ValueError) as exc:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: {exc}')",
"else:",
+ " if not os.path.lexists(config_path):",
+ " finish('absent')",
" try:",
- " data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity = load_legacy_config_for_update(config_path)",
- " except (OSError, UnicodeDecodeError, ValueError) as exc:",
- " if str(exc).startswith('legacy MCP config parent is unsafe'):",
- " fail_teardown(f'Refusing unsafe legacy MCP teardown at {config_path}: {exc}')",
+ " data, legacy_identity = read_legacy_config(config_path)",
+ " except (OSError, UnicodeDecodeError, ValueError):",
" finish('unowned')",
- " if legacy_descriptor is None:",
- " finish('absent')",
"if not isinstance(data, dict):",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: expected object')",
@@ -108,7 +128,7 @@ export function buildDeepAgentsMcpRemoveCommand(
" if not is_v2 and 'mcpServers' not in data:",
" finish('absent')",
" if is_v2 and payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" if is_v2:",
" fail_teardown(f'Invalid managed MCP v2 server map at {config_path}')",
@@ -118,7 +138,7 @@ export function buildDeepAgentsMcpRemoveCommand(
"if is_v2:",
" if data and set(data) != {'mcpServers'}:",
" if payload['force']:",
- " repair_v2_projection(managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " repair_v2_projection(managed_identity, managed_descriptor)",
" finish('removed')",
" fail_teardown(f'Invalid managed MCP v2 projection at {config_path}: only mcpServers is allowed')",
" if present and not payload['force'] and not managed_http_server_matches(current, payload['expected'], True):",
@@ -138,19 +158,20 @@ export function buildDeepAgentsMcpRemoveCommand(
"if data:",
" try:",
" if is_v2:",
- " write_managed_projection(config_path, data, managed_identity, managed_descriptor, managed_parent_descriptor, managed_parent_identity)",
+ " write_managed_projection(config_path, data, managed_identity, managed_descriptor)",
" persisted = read_managed_projection(config_path)[0]",
" else:",
- " persisted = write_legacy_config(config_path, data, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
+ " write_legacy_data(data)",
+ " persisted = read_legacy_config(config_path)[0]",
" except (OSError, UnicodeDecodeError, ValueError) as exc:",
" fail_teardown(f'MCP teardown mutation failed at {config_path}: {exc}')",
" if persisted != data:",
" fail_teardown(f'MCP teardown verification failed at {config_path}')",
"else:",
- " try:",
- " remove_legacy_config(config_path, legacy_identity, legacy_descriptor, legacy_parent_descriptor, legacy_parent_identity)",
- " except (OSError, ValueError) as exc:",
- " fail_teardown(f'MCP teardown mutation failed at {config_path}: {exc}')",
+ " assert_legacy_source_stable(config_path, legacy_identity)",
+ " config_path.unlink()",
+ " if os.path.lexists(config_path):",
+ " fail_teardown(f'Managed MCP teardown verification failed at {config_path}')",
"finish('removed')",
"PY",
].join("\n");
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index a6c9ceb281c..e46d13bbcde 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -6,7 +6,6 @@ import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readin
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
- DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
} from "./mcp-bridge/deepagents-legacy-config";
import {
DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
@@ -243,9 +242,19 @@ export function buildDeepAgentsMcpStatusCommand(
...DEEPAGENTS_STRICT_JSON_HELPERS,
...DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
...DEEPAGENTS_LEGACY_CONFIG_HELPERS,
- ...DEEPAGENTS_RUNTIME_SELECTION_HELPERS,
...MANAGED_HTTP_SERVER_MATCH_HELPERS,
- "runtime_kind = select_deepagents_runtime_kind('auto', managed_path, legacy_path)",
+ `runtime_kind = "auto" # NEMOCLAW_DEEPAGENTS_RUNTIME_TEST_ANCHOR`,
+ "if runtime_kind == 'auto':",
+ " runtime_kind = 'unknown'",
+ " try:",
+ " from deepagents_code import _nemoclaw_managed as managed",
+ " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
+ " if runtime_path == str(managed_path):",
+ " runtime_kind = 'v2'",
+ " elif runtime_path == str(legacy_path):",
+ " runtime_kind = 'legacy'",
+ " except Exception:",
+ " pass",
"if runtime_kind not in ('v2', 'legacy'):",
" print('Could not identify the managed Deep Agents MCP runtime', file=sys.stderr)",
" raise SystemExit(2)",
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index e4b521a09fa..3582140ab2c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,6 +7,12 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
+import {
+ baseEntry,
+ runDeepAgentsConfigCommand,
+} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
+
+import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -199,13 +205,31 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "preserves fixed unsafe projection diagnostics across credential observations (#10754)",
- { timeout: 30_000 },
+ "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
+ { timeout: 120_000 },
() => {
- const unsafeDetails = [
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- "Unsafe managed Deep Agents MCP projection path: FIFO",
+ const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
+ const projection = { mcpServers: {} };
+ const unsafeResults = [
+ ...(["symlink", "fifo"] as const).map((appearedType) =>
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterMissingManagedOpen: appearedType,
+ timeoutMs: 30_000,
+ }),
+ ),
+ runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
+ swapAfterManagedEloop: true,
+ symlink: true,
+ timeoutMs: 30_000,
+ }),
];
+ unsafeResults.forEach((result) => {
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
+ expect(result.configIsSymlink || result.configIsFifo).toBe(true);
+ });
+ const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -257,7 +281,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(6);
+ expect(outcomes).toHaveLength(9);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
diff --git a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
index ab13b4eee74..248f4caa262 100644
--- a/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
+++ b/src/lib/actions/sandbox/mcp-bridge/deepagents-legacy-config.ts
@@ -3,158 +3,46 @@
export const DEEPAGENTS_LEGACY_MCP_CONFIG_PATH = "/sandbox/.deepagents/.mcp.json";
-export const DEEPAGENTS_RUNTIME_SELECTION_HELPERS = [
- "def select_deepagents_runtime_kind(requested_kind, managed_path, legacy_path):",
- " if requested_kind != 'auto':",
- " return requested_kind",
- " try:",
- " from deepagents_code import _nemoclaw_managed as managed",
- " runtime_path = str(getattr(managed, '_MCP_CONFIG_FILE', ''))",
- " if runtime_path == str(managed_path):",
- " return 'v2'",
- " if runtime_path == str(legacy_path):",
- " return 'legacy'",
- " except Exception:",
- " pass",
- " return 'unknown'",
-];
-
export const DEEPAGENTS_LEGACY_CONFIG_HELPERS = [
"LEGACY_MCP_MAX_BYTES = 262144",
"def legacy_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
- "def validate_legacy_descriptor_path(entry_name, parent_descriptor, descriptor):",
- " opened = os.fstat(descriptor)",
- " linked = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " safe = (stat.S_ISREG(opened.st_mode) and opened.st_uid == os.getuid() and stat.S_IMODE(opened.st_mode) == 0o600 and opened.st_nlink == 1 and (opened.st_dev, opened.st_ino) == (linked.st_dev, linked.st_ino))",
- " if not safe:",
- " raise ValueError('legacy MCP config has unsafe ownership, mode, type, or links')",
- " return legacy_fingerprint(opened)",
- "def read_legacy_descriptor(entry_name, parent_descriptor, descriptor):",
- " before = os.fstat(descriptor)",
- " validate_legacy_descriptor_path(entry_name, parent_descriptor, descriptor)",
- " if before.st_size <= 0 or before.st_size > LEGACY_MCP_MAX_BYTES:",
- " raise ValueError('legacy MCP config has invalid size')",
- " chunks = []",
- " remaining = before.st_size",
- " while remaining:",
- " chunk = os.read(descriptor, remaining)",
- " if not chunk:",
- " break",
- " chunks.append(chunk)",
- " remaining -= len(chunk)",
- " after = os.fstat(descriptor)",
- " linked_after = os.stat(entry_name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " stable = (legacy_fingerprint(before) == legacy_fingerprint(after) and legacy_fingerprint(after) == legacy_fingerprint(linked_after))",
- " if remaining or not stable:",
- " raise ValueError('legacy MCP config changed while reading')",
- " return strict_json_loads(b''.join(chunks).decode('utf-8')), legacy_fingerprint(after)",
- "def close_legacy_config_descriptors(descriptor, parent_descriptor):",
- " close_managed_projection_descriptors(descriptor, parent_descriptor)",
- "def load_legacy_config_for_update(path, create_parent=False):",
- " unsafe_parent_prefix = 'legacy MCP config parent is unsafe'",
- " try:",
- " parent_descriptor, parent_identity, _ = open_managed_parent(path, create_parent, unsafe_parent_prefix)",
- " except FileNotFoundError:",
- " return {}, None, None, None, None",
- " entry_name = path.name",
- " flags = os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
- " try:",
- " try:",
- " descriptor = os.open(entry_name, flags, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " assert_managed_source_stable(entry_name, parent_descriptor, None)",
- " assert_managed_parent_stable(path, parent_identity, unsafe_parent_prefix)",
- " return {}, None, None, parent_descriptor, parent_identity",
- " try:",
- " data, identity = read_legacy_descriptor(entry_name, parent_descriptor, descriptor)",
- " assert_managed_parent_stable(path, parent_identity, unsafe_parent_prefix)",
- " return data, identity, descriptor, parent_descriptor, parent_identity",
- " except Exception:",
- " os.close(descriptor)",
- " raise",
- " except Exception:",
- " os.close(parent_descriptor)",
- " raise",
"def read_legacy_config(path):",
- " data, identity, descriptor, parent_descriptor, _ = load_legacy_config_for_update(path)",
+ " flags = os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW",
+ " descriptor = os.open(path, flags)",
" try:",
- " if descriptor is None:",
- " raise FileNotFoundError(path)",
- " return data, identity",
+ " before = os.fstat(descriptor)",
+ " linked = os.stat(path, follow_symlinks=False)",
+ " safe = (stat.S_ISREG(before.st_mode) and before.st_uid == os.getuid() and stat.S_IMODE(before.st_mode) == 0o600 and before.st_nlink == 1 and (before.st_dev, before.st_ino) == (linked.st_dev, linked.st_ino))",
+ " if not safe:",
+ " raise ValueError('legacy MCP config has unsafe ownership, mode, type, or links')",
+ " if before.st_size <= 0 or before.st_size > LEGACY_MCP_MAX_BYTES:",
+ " raise ValueError('legacy MCP config has invalid size')",
+ " chunks = []",
+ " remaining = before.st_size",
+ " while remaining:",
+ " chunk = os.read(descriptor, remaining)",
+ " if not chunk:",
+ " break",
+ " chunks.append(chunk)",
+ " remaining -= len(chunk)",
+ " after = os.fstat(descriptor)",
+ " linked_after = os.stat(path, follow_symlinks=False)",
+ " stable = (legacy_fingerprint(before) == legacy_fingerprint(after) and legacy_fingerprint(after) == legacy_fingerprint(linked_after))",
+ " if remaining or not stable:",
+ " raise ValueError('legacy MCP config changed while reading')",
" finally:",
- " close_legacy_config_descriptors(descriptor, parent_descriptor)",
- "def assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity):",
- " if parent_descriptor is None or parent_identity is None:",
- " raise ValueError('legacy MCP config parent descriptor is unavailable')",
- " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
- " assert_managed_source_stable(path.name, parent_descriptor, identity)",
+ " os.close(descriptor)",
+ " raw = b''.join(chunks).decode('utf-8')",
+ " data = strict_json_loads(raw)",
+ " return data, legacy_fingerprint(before)",
+ "def assert_legacy_source_stable(path, identity):",
" if identity is None:",
- " if descriptor is not None:",
- " raise ValueError('unexpected legacy MCP config descriptor')",
+ " if os.path.lexists(path):",
+ " raise ValueError('legacy MCP config appeared during mutation')",
" return",
- " if descriptor is None or validate_legacy_descriptor_path(path.name, parent_descriptor, descriptor) != identity:",
+ " current = os.stat(path, follow_symlinks=False)",
+ " safe = (stat.S_ISREG(current.st_mode) and current.st_uid == os.getuid() and stat.S_IMODE(current.st_mode) == 0o600 and current.st_nlink == 1 and legacy_fingerprint(current) == identity)",
+ " if not safe:",
" raise ValueError('legacy MCP config changed before mutation')",
- "def legacy_config_bytes(value):",
- " payload = (json.dumps(value, indent=2, sort_keys=True) + '\\n').encode('utf-8')",
- " if not payload or len(payload) > LEGACY_MCP_MAX_BYTES:",
- " raise ValueError('legacy MCP config has invalid rendered size')",
- " return payload",
- "def create_legacy_config_temp(parent_descriptor):",
- " flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW",
- " for _ in range(16):",
- " name = '.nemoclaw-mcp.' + os.urandom(16).hex()",
- " try:",
- " return os.open(name, flags, 0o600, dir_fd=parent_descriptor), name",
- " except FileExistsError:",
- " pass",
- " raise FileExistsError('could not allocate legacy MCP config temporary file')",
- "def write_legacy_config(path, value, identity, descriptor, parent_descriptor, parent_identity):",
- " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
- " payload = legacy_config_bytes(value)",
- " tmp_fd, tmp_name = create_legacy_config_temp(parent_descriptor)",
- " try:",
- " offset = 0",
- " while offset < len(payload):",
- " written = os.write(tmp_fd, payload[offset:])",
- " if written <= 0:",
- " raise OSError('legacy MCP config write made no progress')",
- " offset += written",
- " os.fsync(tmp_fd)",
- " os.close(tmp_fd)",
- " tmp_fd = None",
- " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
- " if identity is None:",
- " try:",
- " os.link(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor, follow_symlinks=False)",
- " except FileExistsError as exc:",
- " raise ValueError('legacy MCP config appeared during publication') from exc",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
- " else:",
- " os.replace(tmp_name, path.name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor)",
- " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
- " verification = os.open(path.name, os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK | os.O_NOFOLLOW, dir_fd=parent_descriptor)",
- " try:",
- " persisted, _ = read_legacy_descriptor(path.name, parent_descriptor, verification)",
- " finally:",
- " os.close(verification)",
- " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
- " return persisted",
- " finally:",
- " if tmp_fd is not None:",
- " os.close(tmp_fd)",
- " try:",
- " os.unlink(tmp_name, dir_fd=parent_descriptor)",
- " except FileNotFoundError:",
- " pass",
- "def remove_legacy_config(path, identity, descriptor, parent_descriptor, parent_identity):",
- " assert_legacy_source_stable(path, identity, descriptor, parent_descriptor, parent_identity)",
- " os.unlink(path.name, dir_fd=parent_descriptor)",
- " try:",
- " os.stat(path.name, dir_fd=parent_descriptor, follow_symlinks=False)",
- " except FileNotFoundError:",
- " pass",
- " else:",
- " raise ValueError('legacy MCP config removal verification failed')",
- " assert_managed_parent_stable(path, parent_identity, 'legacy MCP config parent is unsafe')",
];
diff --git a/test/e2e-test.sh b/test/e2e-test.sh
index 9a27498a085..5a11a76ca11 100755
--- a/test/e2e-test.sh
+++ b/test/e2e-test.sh
@@ -111,73 +111,19 @@ fi
# -------------------------------------------------------
info "4. Verify blueprint runner plan command"
# -------------------------------------------------------
-prepare_command_blueprint() {
- local destination="$1"
- node --input-type=module -e "
- import { createRequire } from 'node:module';
- import { writeFileSync } from 'node:fs';
- import { join } from 'node:path';
- const require = createRequire('/opt/nemoclaw/');
- const YAML = require('yaml');
- const destination = process.argv[1];
- const blueprint = {
- version: '1.0',
- components: {
- sandbox: {
- image: 'openclaw',
- name: 'openclaw',
- forward_ports: [18789],
- },
- inference: {
- profiles: {
- ncp: {
- provider_type: 'nvidia',
- provider_name: 'nvidia-ncp',
- endpoint: '',
- model: 'nvidia/nemotron-3-super-120b-a12b',
- },
- },
- },
- policy: {
- additions: {
- nim_service: {
- name: 'nim_service',
- endpoints: [{ host: '93.184.216.34', port: 8000, access: 'full' }],
- },
- },
- },
- },
- };
- writeFileSync(join(destination, 'blueprint.yaml'), YAML.stringify(blueprint));
- writeFileSync(
- join(destination, 'sandbox-policy.yaml'),
- YAML.stringify({ version: 1, network_policies: {} }),
- );
- writeFileSync(
- join(destination, 'private-networks.yaml'),
- YAML.stringify({ ipv4: [], ipv6: [], names: [] }),
- );
- " "$destination"
-}
-
-(
- PLAN_BLUEPRINT_PATH=$(mktemp -d)
- trap 'rm -rf "$PLAN_BLUEPRINT_PATH"' EXIT
- prepare_command_blueprint "$PLAN_BLUEPRINT_PATH"
- cd "$PLAN_BLUEPRINT_PATH"
- # Steps 3 and 3b validate the shipped blueprint. This command smoke test uses
- # an independent fixture with a stable public IP so DNS cannot preempt the
- # expected OpenShell prerequisite boundary.
- NEMOCLAW_BLUEPRINT_PATH="$PLAN_BLUEPRINT_PATH" node --input-type=module -e "
- const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
- try {
- await main(['plan', '--profile', 'ncp', '--dry-run']);
- } catch (err) {
- if (!err.message.includes('openshell CLI not found')) throw err;
- console.log('EXPECTED_ERROR: ' + err.message);
- }
- "
-) 2>&1 | tee /tmp/plan-output.txt
+cd /opt/nemoclaw-blueprint
+# Runner will fail at openshell prereq check (expected in test container).
+# Use 'ncp' profile (empty endpoint skips SSRF DNS lookup in sandbox).
+# Catch only the expected error — anything else propagates as a real failure.
+NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint node --input-type=module -e "
+ const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
+ try {
+ await main(['plan', '--profile', 'ncp', '--dry-run']);
+ } catch (err) {
+ if (!err.message.includes('openshell CLI not found')) throw err;
+ console.log('EXPECTED_ERROR: ' + err.message);
+ }
+" 2>&1 | tee /tmp/plan-output.txt
if grep -q "RUN_ID:" /tmp/plan-output.txt; then
pass "Blueprint plan generates run ID"
else
@@ -202,15 +148,13 @@ info "4b. Verify blueprint runner apply smoke test"
# return the same metadata + YAML shape as OpenShell 0.0.72; an empty successful
# response is intentionally rejected by the runner.
FAKE_OPENSHELL_BIN=$(mktemp -d)
-APPLY_BLUEPRINT_PATH=$(mktemp -d)
APPLY_OUTPUT=$(mktemp)
APPLY_CALLS="$FAKE_OPENSHELL_BIN/calls"
cleanup_apply_fixture() {
- rm -rf "$FAKE_OPENSHELL_BIN" "$APPLY_BLUEPRINT_PATH"
+ rm -rf "$FAKE_OPENSHELL_BIN"
rm -f "$APPLY_OUTPUT" "$APPLY_CALLS"
}
trap cleanup_apply_fixture EXIT
-prepare_command_blueprint "$APPLY_BLUEPRINT_PATH"
cat >"$FAKE_OPENSHELL_BIN/openshell" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
@@ -244,7 +188,7 @@ if [ "${1:-} ${2:-}" = "policy get" ]; then
fi
if [ "${1:-} ${2:-}" = "policy get" ] && [[ " $* " == *" --output json "* ]]; then
sandbox="${@: -1}"
- policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
+ policy_file=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
policy_hash=sha256:fixture-policy
policy_version=1
if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
@@ -274,12 +218,8 @@ case "$*" in
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
exit 64
fi
- policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
- if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
- policy_file="${BASH_SOURCE[0]%/*}/effective-policy.yaml"
- fi
printf '%s\n' 'Policy for sandbox fixture' '---'
- cat "$policy_file"
+ cat /opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
;;
"policy get "*)
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
@@ -289,8 +229,8 @@ esac
SH
chmod 0755 "$FAKE_OPENSHELL_BIN/openshell"
PATH="$FAKE_OPENSHELL_BIN:$PATH" \
- NEMOCLAW_BLUEPRINT_PATH="$APPLY_BLUEPRINT_PATH" \
- OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/sandbox-policy.yaml" \
+ NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint \
+ OPENSHELL_SANDBOX_POLICY=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml \
node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
await main(['apply', '--profile', 'ncp']);
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 1f43ee8c7ad..743f48ef5f9 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -34,11 +34,6 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigExists: boolean;
legacyConfig: Record | null;
legacyConfigText: string | null;
- legacyParentIsSymlink: boolean;
- legacyParentTargetText: string | null;
- managedParentIsSymlink: boolean;
- managedParentTargetText: string | null;
- managedRaceIterations: number;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
managedTargetReadAccessed: boolean;
@@ -48,17 +43,68 @@ export interface DeepAgentsManagedFixtureOptions {
danglingSymlink?: boolean;
directory?: boolean;
fifo?: boolean;
- legacyParentSymlink?: boolean;
mode?: number;
- parentSymlink?: boolean;
- raceAbsentPublication?: string;
- raceProjection?: "fifo" | "symlink";
- socket?: boolean;
+ swapAfterManagedEloop?: boolean;
+ swapAfterManagedOpen?: "fifo" | "symlink";
+ swapAfterMissingManagedOpen?: "fifo" | "symlink";
+ swapBeforeManagedLink?: string;
+ swapOnManagedOpen?: "fifo" | "socket" | "symlink";
+ swapOnManagedRead?: "fifo" | "symlink";
+ swapOnManagedSeek?: string;
symlink?: boolean;
targetReadProbe?: boolean;
timeoutMs?: number;
}
+type ManagedSwap = {
+ content?: string;
+ kind: "fifo" | "regular" | "socket" | "symlink";
+ phase:
+ | "after-missing-projection-open"
+ | "after-projection-eloop"
+ | "after-projection-open"
+ | "before-projection-open"
+ | "projection-publication-begins"
+ | "projection-read-begins"
+ | "projection-rewrite-begins";
+};
+
+function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSwap | undefined {
+ if (options.swapOnManagedOpen) {
+ return { kind: options.swapOnManagedOpen, phase: "before-projection-open" };
+ }
+ if (options.swapAfterManagedOpen) {
+ return { kind: options.swapAfterManagedOpen, phase: "after-projection-open" };
+ }
+ if (options.swapAfterMissingManagedOpen) {
+ return {
+ kind: options.swapAfterMissingManagedOpen,
+ phase: "after-missing-projection-open",
+ };
+ }
+ if (options.swapOnManagedRead) {
+ return { kind: options.swapOnManagedRead, phase: "projection-read-begins" };
+ }
+ if (options.swapAfterManagedEloop) {
+ return { kind: "symlink", phase: "after-projection-eloop" };
+ }
+ if (options.swapBeforeManagedLink !== undefined) {
+ return {
+ content: options.swapBeforeManagedLink,
+ kind: "regular",
+ phase: "projection-publication-begins",
+ };
+ }
+ if (options.swapOnManagedSeek !== undefined) {
+ return {
+ content: options.swapOnManagedSeek,
+ kind: "regular",
+ phase: "projection-rewrite-begins",
+ };
+ }
+ return undefined;
+}
+
function createFifo(target: string, mode = 0o600): void {
fs.mkdirSync(path.dirname(target), { recursive: true });
const fifo = spawnSync("mkfifo", [target], { encoding: "utf-8", timeout: 5000 });
@@ -79,170 +125,6 @@ function waitForPath(target: string, timeoutMs: number): boolean {
return fs.existsSync(target);
}
-type ManagedFixtureProcess = {
- child: ReturnType;
- finishedPath: string;
- resultPath: string;
- stopPath: string;
-};
-
-function startManagedFixtureProcess(
- fixtureRoot: string,
- label: string,
- script: string,
- args: string[],
-): ManagedFixtureProcess {
- const readyPath = path.join(fixtureRoot, `${label}-ready`);
- const stopPath = path.join(fixtureRoot, `${label}-stop`);
- const finishedPath = path.join(fixtureRoot, `${label}-finished`);
- const resultPath = path.join(fixtureRoot, `${label}-result`);
- const child = spawn(
- "python3",
- ["-I", "-c", script, ...args, readyPath, stopPath, finishedPath, resultPath],
- { stdio: "ignore" },
- );
- if (!waitForPath(readyPath, 2000)) {
- child.kill("SIGTERM");
- throw new Error(`${label} did not become ready`);
- }
- pause(20);
- return { child, finishedPath, resultPath, stopPath };
-}
-
-function stopManagedFixtureProcess(process: ManagedFixtureProcess): number {
- try {
- fs.writeFileSync(process.stopPath, "", { flag: "wx" });
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
- }
- if (!waitForPath(process.finishedPath, 2000)) {
- process.child.kill("SIGTERM");
- throw new Error("managed fixture process did not stop");
- }
- process.child.kill("SIGTERM");
- const descriptor = fs.openSync(
- process.resultPath,
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
- );
- let result: number;
- try {
- if (!fs.fstatSync(descriptor).isFile()) {
- throw new Error("managed fixture process result is not a regular file");
- }
- result = Number.parseInt(fs.readFileSync(descriptor, "utf-8"), 10);
- } finally {
- fs.closeSync(descriptor);
- }
- return Number.isFinite(result) ? result : 0;
-}
-
-function startManagedPathRace(
- fixtureRoot: string,
- managedPath: string,
- managedTarget: string,
- kind: "fifo" | "symlink",
- safeContent: string,
-): ManagedFixtureProcess {
- const script = [
- "import base64, os, sys, time",
- "managed_path, managed_target, race_kind, encoded = sys.argv[1:5]",
- "ready, stop, finished, result = sys.argv[5:9]",
- "safe_content = base64.b64decode(encoded)",
- "safe_temp = managed_path + '.race-safe'",
- "unsafe_temp = managed_path + '.race-unsafe'",
- "def remove_temp(target):",
- " try:",
- " os.unlink(target)",
- " except FileNotFoundError:",
- " pass",
- "def replace_safe():",
- " remove_temp(safe_temp)",
- " descriptor = os.open(safe_temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)",
- " try:",
- " offset = 0",
- " while offset < len(safe_content):",
- " offset += os.write(descriptor, safe_content[offset:])",
- " os.fsync(descriptor)",
- " finally:",
- " os.close(descriptor)",
- " os.replace(safe_temp, managed_path)",
- "def replace_unsafe():",
- " remove_temp(unsafe_temp)",
- " if race_kind == 'symlink':",
- " os.symlink(managed_target, unsafe_temp)",
- " else:",
- " os.mkfifo(unsafe_temp, 0o600)",
- " os.replace(unsafe_temp, managed_path)",
- "iterations = 0",
- "replace_unsafe()",
- "open(ready, 'x').close()",
- "while not os.path.exists(stop):",
- " replace_safe()",
- " iterations += 1",
- " replace_unsafe()",
- " iterations += 1",
- " time.sleep(0.0001)",
- "replace_unsafe()",
- "open(result, 'w', encoding='utf-8').write(str(iterations))",
- "open(finished, 'x').close()",
- ].join("\n");
- return startManagedFixtureProcess(fixtureRoot, "managed-path-race", script, [
- managedPath,
- managedTarget,
- kind,
- Buffer.from(safeContent).toString("base64"),
- ]);
-}
-
-function startAbsentPublicationRace(
- fixtureRoot: string,
- managedPath: string,
- managedTarget: string,
-): ManagedFixtureProcess {
- const script = [
- "import os, sys, time",
- "managed_path, managed_target = sys.argv[1:3]",
- "ready, stop, finished, result = sys.argv[3:7]",
- "parent = os.path.dirname(managed_path)",
- "triggered = 0",
- "open(ready, 'x').close()",
- "while not os.path.exists(stop) and not triggered:",
- " if os.path.isdir(parent):",
- " try:",
- " os.symlink(managed_target, managed_path)",
- " triggered = 1",
- " except FileExistsError:",
- " pass",
- " time.sleep(0.0001)",
- "while not os.path.exists(stop):",
- " time.sleep(0.001)",
- "open(result, 'w', encoding='utf-8').write(str(triggered))",
- "open(finished, 'x').close()",
- ].join("\n");
- return startManagedFixtureProcess(fixtureRoot, "managed-publication-race", script, [
- managedPath,
- managedTarget,
- ]);
-}
-
-function startManagedSocket(fixtureRoot: string, managedPath: string): ManagedFixtureProcess {
- const script = [
- "import os, socket, sys, time",
- "managed_path = sys.argv[1]",
- "ready, stop, finished, result = sys.argv[2:6]",
- "os.makedirs(os.path.dirname(managed_path), exist_ok=True)",
- "managed_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)",
- "managed_socket.bind(managed_path)",
- "open(ready, 'x').close()",
- "while not os.path.exists(stop):",
- " time.sleep(0.001)",
- "managed_socket.close()",
- "open(result, 'w', encoding='utf-8').write('1')",
- "open(finished, 'x').close()",
- ].join("\n");
- return startManagedFixtureProcess(fixtureRoot, "managed-socket", script, [managedPath]);
-}
-
function startTargetReadProbe(target: string, ready: string, accessed: string) {
const script = [
"import os, sys",
@@ -266,10 +148,12 @@ function createDeepAgentsFixturePythonExecutable(
fixtureRoot: string,
configPath: string,
legacyConfigPath: string,
+ managedSymlinkTarget: string,
runtimeKind: "v2" | "legacy" | "unknown",
+ managedSwap?: ManagedSwap,
): { executablePath: string; scriptPath: string } {
- const executablePath = path.join(fixtureRoot, "fixture-python");
- const scriptPath = path.join(fixtureRoot, "fixture-wrapper.py");
+ const executablePath = path.join(fixtureRoot, "managed-swap-python");
+ const scriptPath = path.join(fixtureRoot, "managed-swap-wrapper.py");
const runtimeConfigPath =
runtimeKind === "v2"
? configPath
@@ -285,12 +169,118 @@ function createDeepAgentsFixturePythonExecutable(
{ mode: 0o600 },
);
const wrapper = [
+ "import errno",
+ "import os",
+ "import socket",
"import sys",
`fixture_root = ${JSON.stringify(fixtureRoot)}`,
+ `managed_path = ${JSON.stringify(configPath)}`,
+ `managed_target = ${JSON.stringify(managedSymlinkTarget)}`,
+ `managed_swap_kind = ${JSON.stringify(managedSwap?.kind ?? "")}`,
+ `managed_swap_phase = ${JSON.stringify(managedSwap?.phase ?? "")}`,
+ `managed_swap_content = ${JSON.stringify(managedSwap?.content ?? "")}`,
+ "managed_eloop_stage = 0",
+ "managed_socket = None",
+ "managed_swapped = False",
+ "managed_descriptor = None",
+ "managed_entry_name = os.path.basename(managed_path)",
+ "managed_real_link = os.link",
+ "managed_real_lseek = os.lseek",
+ "managed_real_open = os.open",
+ "managed_real_read = os.read",
+ "def replace_managed_path():",
+ " global managed_socket, managed_swapped",
+ " if managed_swapped:",
+ " return",
+ " managed_swapped = True",
+ " try:",
+ " os.unlink(managed_path)",
+ " except FileNotFoundError:",
+ " pass",
+ " if managed_swap_kind == 'symlink':",
+ " os.symlink(managed_target, managed_path)",
+ " elif managed_swap_kind == 'fifo':",
+ " os.mkfifo(managed_path, 0o600)",
+ " elif managed_swap_kind == 'socket':",
+ " managed_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)",
+ " managed_socket.bind(managed_path)",
+ " else:",
+ " with open(managed_path, 'w', encoding='utf-8') as replacement:",
+ " replacement.write(managed_swap_content)",
+ " os.chmod(managed_path, 0o600)",
+ "def replace_managed_symlink_with_regular():",
+ " global managed_eloop_stage",
+ " os.unlink(managed_path)",
+ " with open(managed_target, 'rb') as source, open(managed_path, 'wb') as replacement:",
+ " replacement.write(source.read())",
+ " os.chmod(managed_path, 0o600)",
+ " managed_eloop_stage = 1",
+ "def restore_managed_symlink():",
+ " global managed_eloop_stage",
+ " os.unlink(managed_path)",
+ " os.symlink(managed_target, managed_path)",
+ " managed_eloop_stage = 2",
+ "def is_managed_entry(value, directory_descriptor):",
+ " try:",
+ " candidate = os.fspath(value)",
+ " return candidate == managed_path if directory_descriptor is None else candidate == managed_entry_name",
+ " except TypeError:",
+ " return False",
+ "def managed_boundary_open(value, flags, mode=0o777, *, dir_fd=None):",
+ " global managed_descriptor",
+ " projection_entry = is_managed_entry(value, dir_fd)",
+ " if projection_entry and managed_swap_phase == 'before-projection-open':",
+ " replace_managed_path()",
+ " try:",
+ " if dir_fd is None:",
+ " descriptor = managed_real_open(value, flags, mode)",
+ " else:",
+ " descriptor = managed_real_open(value, flags, mode, dir_fd=dir_fd)",
+ " except OSError as exc:",
+ " if projection_entry and managed_swap_phase == 'after-missing-projection-open' and isinstance(exc, FileNotFoundError):",
+ " replace_managed_path()",
+ " elif projection_entry and managed_swap_phase == 'after-projection-eloop' and exc.errno == errno.ELOOP:",
+ " replace_managed_symlink_with_regular()",
+ " raise",
+ " if projection_entry:",
+ " managed_descriptor = descriptor",
+ " if managed_swap_phase == 'after-projection-open':",
+ " replace_managed_path()",
+ " return descriptor",
+ "def managed_boundary_read(descriptor, length):",
+ " if managed_swap_phase == 'projection-read-begins' and descriptor == managed_descriptor:",
+ " replace_managed_path()",
+ " return managed_real_read(descriptor, length)",
+ "def managed_boundary_link(source, destination, *args, **kwargs):",
+ " if managed_swap_phase == 'projection-publication-begins' and is_managed_entry(destination, kwargs.get('dst_dir_fd')):",
+ " replace_managed_path()",
+ " return managed_real_link(source, destination, *args, **kwargs)",
+ "def managed_boundary_lseek(descriptor, position, how):",
+ " if managed_swap_phase == 'projection-rewrite-begins' and descriptor == managed_descriptor:",
+ " replace_managed_path()",
+ " return managed_real_lseek(descriptor, position, how)",
+ "def install_managed_boundaries():",
+ " if not managed_swap_phase:",
+ " return",
+ " os.link = managed_boundary_link",
+ " os.lseek = managed_boundary_lseek",
+ " os.open = managed_boundary_open",
+ " os.read = managed_boundary_read",
+ "def restore_managed_boundaries():",
+ " os.link = managed_real_link",
+ " os.lseek = managed_real_lseek",
+ " os.open = managed_real_open",
+ " os.read = managed_real_read",
"source = sys.stdin.read()",
"namespace = {'__name__': '__main__'}",
"sys.path.insert(0, fixture_root)",
- "exec(compile(source, '', 'exec'), namespace)",
+ "install_managed_boundaries()",
+ "try:",
+ " exec(compile(source, '', 'exec'), namespace)",
+ "finally:",
+ " restore_managed_boundaries()",
+ " if managed_swap_phase == 'after-projection-eloop' and managed_eloop_stage == 1:",
+ " restore_managed_symlink()",
"",
].join("\n");
fs.writeFileSync(scriptPath, wrapper, { mode: 0o600 });
@@ -311,27 +301,14 @@ export function runDeepAgentsConfigCommand(
managedOptions: DeepAgentsManagedFixtureOptions = {},
runtimeEnvironment: NodeJS.ProcessEnv = {},
): DeepAgentsConfigCommandResult {
- const activeFixtureProcesses = [
- managedOptions.raceAbsentPublication !== undefined,
- managedOptions.raceProjection !== undefined,
- managedOptions.socket === true,
- ].filter(Boolean).length;
- if (activeFixtureProcesses > 1) {
- throw new Error("managed fixture process options are mutually exclusive");
- }
- const fixtureRoot = managedOptions.socket ? "/tmp" : os.tmpdir();
- const tmp = fs.realpathSync(fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-")));
+ const managedSwap = resolveManagedSwap(managedOptions);
+ const fixtureRoot = managedSwap?.kind === "socket" ? "/tmp" : os.tmpdir();
+ const tmp = fs.mkdtempSync(path.join(fixtureRoot, "nemoclaw-deepagents-mcp-"));
const configPath = path.join(tmp, ".deepagents", ".nemoclaw-mcp.json");
- const managedParentPath = path.dirname(configPath);
- const managedParentTarget = path.join(tmp, "managed-parent-target");
- const managedParentTargetConfig = path.join(managedParentTarget, path.basename(configPath));
const managedSymlinkTarget = path.join(tmp, "managed-projection-target.json");
const managedTargetReadReady = path.join(tmp, "managed-target-read-ready");
const managedTargetReadAccess = path.join(tmp, "managed-target-read-accessed");
const legacyConfigPath = path.join(tmp, ".deepagents", ".mcp.json");
- const legacyParentPath = path.dirname(legacyConfigPath);
- const legacyParentTarget = path.join(tmp, "legacy-parent-target");
- const legacyParentTargetConfig = path.join(legacyParentTarget, path.basename(legacyConfigPath));
const initializeConfig = (
target: string,
value: Record | string | undefined,
@@ -346,78 +323,44 @@ export function runDeepAgentsConfigCommand(
);
};
const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
- if (
- managedOptions.legacyParentSymlink &&
- (initialConfig !== undefined ||
- managedOptions.danglingSymlink ||
- managedOptions.directory ||
- managedOptions.fifo ||
- managedOptions.parentSymlink ||
- managedOptions.raceAbsentPublication !== undefined ||
- managedOptions.raceProjection !== undefined ||
- managedOptions.socket ||
- managedOptions.symlink)
- ) {
- throw new Error("legacyParentSymlink cannot share the managed projection parent fixture");
- }
const managedTargetReadPath = managedOptions.targetReadProbe
- ? managedOptions.parentSymlink
- ? managedParentTargetConfig
- : managedSymlink
- ? managedSymlinkTarget
- : null
+ ? managedSymlink
+ ? managedSymlinkTarget
+ : null
: null;
if (managedOptions.targetReadProbe && managedTargetReadPath === null) {
- throw new Error("targetReadProbe requires a projection or parent symlink");
+ throw new Error("targetReadProbe requires a projection symlink");
}
const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
- if (managedOptions.parentSymlink) {
- fs.mkdirSync(managedParentTarget, { recursive: true });
- if (managedTargetReadPath === managedParentTargetConfig) {
- createFifo(managedParentTargetConfig, managedOptions.mode);
- } else {
- initializeConfig(managedParentTargetConfig, initialConfig, managedOptions.mode);
- }
- fs.symlinkSync(managedParentTarget, managedParentPath);
- } else if (managedOptions.directory) {
+ if (managedOptions.directory) {
fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
} else if (managedOptions.fifo) {
createFifo(configPath, managedOptions.mode);
- } else if (managedOptions.socket) {
- // The socket holder creates the path after the rest of the fixture is ready.
} else {
if (managedTargetReadPath === managedSymlinkTarget) {
createFifo(managedSymlinkTarget, managedOptions.mode);
} else if (
!managedOptions.danglingSymlink &&
- managedOptions.raceAbsentPublication === undefined
+ managedSwap?.phase !== "after-missing-projection-open"
) {
initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
if (initialConfig !== undefined)
fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
}
+ if (managedSwap?.phase === "after-missing-projection-open") {
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
+ }
if (managedSymlink) {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.symlinkSync(managedSymlinkTarget, configPath);
}
}
- if (managedOptions.raceProjection === "symlink") {
+ if (managedSwap?.kind === "symlink") {
initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
}
- if (managedOptions.raceAbsentPublication !== undefined) {
- initializeConfig(managedSymlinkTarget, managedOptions.raceAbsentPublication);
- }
- if (managedOptions.legacyParentSymlink) {
- fs.mkdirSync(legacyParentTarget, { recursive: true });
- initializeConfig(legacyParentTargetConfig, initialLegacyConfig, initialLegacyMode);
- fs.symlinkSync(legacyParentTarget, legacyParentPath);
- } else {
- initializeConfig(legacyConfigPath, initialLegacyConfig, initialLegacyMode);
- }
+ initializeConfig(legacyConfigPath, initialLegacyConfig);
+ if (initialLegacyConfig !== undefined) fs.chmodSync(legacyConfigPath, initialLegacyMode);
let targetReadProbe: ReturnType | null = null;
- let managedRace: ManagedFixtureProcess | null = null;
- let socketFixture: ManagedFixtureProcess | null = null;
- let managedRaceIterations = 0;
try {
if (managedTargetReadPath !== null) {
targetReadProbe = startTargetReadProbe(
@@ -426,39 +369,22 @@ export function runDeepAgentsConfigCommand(
managedTargetReadAccess,
);
}
- if (managedOptions.socket) {
- socketFixture = startManagedSocket(tmp, configPath);
- } else if (managedOptions.raceProjection) {
- if (initialConfig === undefined) throw new Error("raceProjection requires initialConfig");
- const safeContent =
- typeof initialConfig === "string"
- ? initialConfig
- : `${JSON.stringify(initialConfig, null, 2)}\n`;
- managedRace = startManagedPathRace(
- tmp,
- configPath,
- managedSymlinkTarget,
- managedOptions.raceProjection,
- safeContent,
- );
- } else if (managedOptions.raceAbsentPublication !== undefined) {
- managedRace = startAbsentPublicationRace(tmp, configPath, managedSymlinkTarget);
- }
const fixturePython = createDeepAgentsFixturePythonExecutable(
tmp,
configPath,
legacyConfigPath,
+ managedSymlinkTarget,
runtimeKind,
+ managedSwap,
);
const fixtureCommand = command
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
.replaceAll("/opt/venv/bin/python3", fixturePython.executablePath);
const canonicalEnvironment = Object.fromEntries(
- [...command.matchAll(/openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]*)/gu)].map(([, name]) => [
- name!,
- `openshell:resolve:env:${name!}`,
- ]),
+ [...command.matchAll(/openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]*)/gu)].map(
+ ([, name]) => [name!, `openshell:resolve:env:${name!}`],
+ ),
);
const fixtureCommandPath = path.join(tmp, "managed-command.sh");
fs.writeFileSync(fixtureCommandPath, fixtureCommand, { mode: 0o600 });
@@ -472,14 +398,6 @@ export function runDeepAgentsConfigCommand(
},
timeout: managedOptions.timeoutMs ?? 5000,
});
- if (managedRace !== null) {
- managedRaceIterations = stopManagedFixtureProcess(managedRace);
- managedRace = null;
- }
- if (socketFixture !== null) {
- stopManagedFixtureProcess(socketFixture);
- socketFixture = null;
- }
let configStat: fs.Stats | null = null;
try {
configStat = fs.lstatSync(configPath);
@@ -487,19 +405,7 @@ export function runDeepAgentsConfigCommand(
// A missing projection has no path type to report.
}
const configExists = configStat !== null;
- let managedParentIsSymlink = false;
- try {
- managedParentIsSymlink = fs.lstatSync(managedParentPath).isSymbolicLink();
- } catch {
- // A missing projection parent cannot redirect the managed path.
- }
const legacyConfigExists = fs.existsSync(legacyConfigPath);
- let legacyParentIsSymlink = false;
- try {
- legacyParentIsSymlink = fs.lstatSync(legacyParentPath).isSymbolicLink();
- } catch {
- // A missing legacy parent cannot redirect the legacy config path.
- }
const configIsFifo = configStat?.isFIFO() === true;
const configIsSocket = configStat?.isSocket() === true;
const configIsSymlink = configStat?.isSymbolicLink() === true;
@@ -525,14 +431,9 @@ export function runDeepAgentsConfigCommand(
};
const managedSymlinkTargetText =
managedTargetReadPath === managedSymlinkTarget ? null : readRegularFile(managedSymlinkTarget);
- const managedParentTargetText =
- managedTargetReadPath === managedParentTargetConfig
- ? null
- : readRegularFile(managedParentTargetConfig);
const managedTargetReadAccessed =
managedTargetReadPath !== null && waitForPath(managedTargetReadAccess, 200);
const legacyConfigText = legacyConfigExists ? fs.readFileSync(legacyConfigPath, "utf-8") : null;
- const legacyParentTargetText = readRegularFile(legacyParentTargetConfig);
const parseConfigText = (text: string | null): Record | null => {
if (!text) return null;
try {
@@ -558,18 +459,11 @@ export function runDeepAgentsConfigCommand(
legacyConfigExists,
legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
- legacyParentIsSymlink,
- legacyParentTargetText,
- managedParentIsSymlink,
- managedParentTargetText,
- managedRaceIterations,
managedSymlinkTargetExists,
managedSymlinkTargetText,
managedTargetReadAccessed,
};
} finally {
- if (managedRace !== null) managedRace.child.kill("SIGTERM");
- if (socketFixture !== null) socketFixture.child.kill("SIGTERM");
targetReadProbe?.kill("SIGTERM");
fs.rmSync(tmp, { recursive: true, force: true });
}
diff --git a/test/runtime/policy/policies.test.ts b/test/runtime/policy/policies.test.ts
index 3a5c1132e1b..283d2c912a8 100644
--- a/test/runtime/policy/policies.test.ts
+++ b/test/runtime/policy/policies.test.ts
@@ -616,16 +616,10 @@ exit 1
it("applyPreset does not create temp dirs when bounded policy observation loses OpenShell", () => {
const policyTempPrefix = path.join(os.tmpdir(), "nemoclaw-policy-");
- const policyReadSpy = vi
- .spyOn(policyReaderModule.syncCliOpenShellSandboxPolicyReader, "readSandboxPolicy")
- .mockReturnValue({
- ok: false,
- error: {
- kind: "command",
- reason: "failed",
- message: "OpenShell became unavailable during the bounded policy read.",
- },
- });
+ const resolveSpy = vi
+ .spyOn(resolveOpenshellModule, "resolveOpenshell")
+ .mockReturnValueOnce(fakeOpenshell)
+ .mockReturnValue(null);
const mkdtempSpy = vi.spyOn(fs, "mkdtempSync");
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -645,7 +639,7 @@ exit 1
mkdtempSpy.mock.calls.filter(([prefix]) => String(prefix).startsWith(policyTempPrefix)),
).toEqual([]);
} finally {
- policyReadSpy.mockRestore();
+ resolveSpy.mockRestore();
mkdtempSpy.mockRestore();
errSpy.mockRestore();
logSpy.mockRestore();
From eefd9982856d75feef29ec5d58fe6aafaf9dad2f Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 06:16:17 -0700
Subject: [PATCH 37/49] fix(ci): restore review-head validation fixtures
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 2 +-
test/e2e-test.sh | 96 ++++++++++++++++----
test/runtime/policy/policies.test.ts | 16 +++-
3 files changed, 90 insertions(+), 24 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 8a564cd373d..b74dbb11036 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -43,7 +43,7 @@ Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
-Exit status `2` is specific to this unsafe path-type result.
+For this unsafe path-type result, the command exits with status `2`.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/test/e2e-test.sh b/test/e2e-test.sh
index 5a11a76ca11..9a27498a085 100755
--- a/test/e2e-test.sh
+++ b/test/e2e-test.sh
@@ -111,19 +111,73 @@ fi
# -------------------------------------------------------
info "4. Verify blueprint runner plan command"
# -------------------------------------------------------
-cd /opt/nemoclaw-blueprint
-# Runner will fail at openshell prereq check (expected in test container).
-# Use 'ncp' profile (empty endpoint skips SSRF DNS lookup in sandbox).
-# Catch only the expected error — anything else propagates as a real failure.
-NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint node --input-type=module -e "
- const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
- try {
- await main(['plan', '--profile', 'ncp', '--dry-run']);
- } catch (err) {
- if (!err.message.includes('openshell CLI not found')) throw err;
- console.log('EXPECTED_ERROR: ' + err.message);
- }
-" 2>&1 | tee /tmp/plan-output.txt
+prepare_command_blueprint() {
+ local destination="$1"
+ node --input-type=module -e "
+ import { createRequire } from 'node:module';
+ import { writeFileSync } from 'node:fs';
+ import { join } from 'node:path';
+ const require = createRequire('/opt/nemoclaw/');
+ const YAML = require('yaml');
+ const destination = process.argv[1];
+ const blueprint = {
+ version: '1.0',
+ components: {
+ sandbox: {
+ image: 'openclaw',
+ name: 'openclaw',
+ forward_ports: [18789],
+ },
+ inference: {
+ profiles: {
+ ncp: {
+ provider_type: 'nvidia',
+ provider_name: 'nvidia-ncp',
+ endpoint: '',
+ model: 'nvidia/nemotron-3-super-120b-a12b',
+ },
+ },
+ },
+ policy: {
+ additions: {
+ nim_service: {
+ name: 'nim_service',
+ endpoints: [{ host: '93.184.216.34', port: 8000, access: 'full' }],
+ },
+ },
+ },
+ },
+ };
+ writeFileSync(join(destination, 'blueprint.yaml'), YAML.stringify(blueprint));
+ writeFileSync(
+ join(destination, 'sandbox-policy.yaml'),
+ YAML.stringify({ version: 1, network_policies: {} }),
+ );
+ writeFileSync(
+ join(destination, 'private-networks.yaml'),
+ YAML.stringify({ ipv4: [], ipv6: [], names: [] }),
+ );
+ " "$destination"
+}
+
+(
+ PLAN_BLUEPRINT_PATH=$(mktemp -d)
+ trap 'rm -rf "$PLAN_BLUEPRINT_PATH"' EXIT
+ prepare_command_blueprint "$PLAN_BLUEPRINT_PATH"
+ cd "$PLAN_BLUEPRINT_PATH"
+ # Steps 3 and 3b validate the shipped blueprint. This command smoke test uses
+ # an independent fixture with a stable public IP so DNS cannot preempt the
+ # expected OpenShell prerequisite boundary.
+ NEMOCLAW_BLUEPRINT_PATH="$PLAN_BLUEPRINT_PATH" node --input-type=module -e "
+ const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
+ try {
+ await main(['plan', '--profile', 'ncp', '--dry-run']);
+ } catch (err) {
+ if (!err.message.includes('openshell CLI not found')) throw err;
+ console.log('EXPECTED_ERROR: ' + err.message);
+ }
+ "
+) 2>&1 | tee /tmp/plan-output.txt
if grep -q "RUN_ID:" /tmp/plan-output.txt; then
pass "Blueprint plan generates run ID"
else
@@ -148,13 +202,15 @@ info "4b. Verify blueprint runner apply smoke test"
# return the same metadata + YAML shape as OpenShell 0.0.72; an empty successful
# response is intentionally rejected by the runner.
FAKE_OPENSHELL_BIN=$(mktemp -d)
+APPLY_BLUEPRINT_PATH=$(mktemp -d)
APPLY_OUTPUT=$(mktemp)
APPLY_CALLS="$FAKE_OPENSHELL_BIN/calls"
cleanup_apply_fixture() {
- rm -rf "$FAKE_OPENSHELL_BIN"
+ rm -rf "$FAKE_OPENSHELL_BIN" "$APPLY_BLUEPRINT_PATH"
rm -f "$APPLY_OUTPUT" "$APPLY_CALLS"
}
trap cleanup_apply_fixture EXIT
+prepare_command_blueprint "$APPLY_BLUEPRINT_PATH"
cat >"$FAKE_OPENSHELL_BIN/openshell" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
@@ -188,7 +244,7 @@ if [ "${1:-} ${2:-}" = "policy get" ]; then
fi
if [ "${1:-} ${2:-}" = "policy get" ] && [[ " $* " == *" --output json "* ]]; then
sandbox="${@: -1}"
- policy_file=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
+ policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
policy_hash=sha256:fixture-policy
policy_version=1
if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
@@ -218,8 +274,12 @@ case "$*" in
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
exit 64
fi
+ policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
+ if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
+ policy_file="${BASH_SOURCE[0]%/*}/effective-policy.yaml"
+ fi
printf '%s\n' 'Policy for sandbox fixture' '---'
- cat /opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
+ cat "$policy_file"
;;
"policy get "*)
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
@@ -229,8 +289,8 @@ esac
SH
chmod 0755 "$FAKE_OPENSHELL_BIN/openshell"
PATH="$FAKE_OPENSHELL_BIN:$PATH" \
- NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint \
- OPENSHELL_SANDBOX_POLICY=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml \
+ NEMOCLAW_BLUEPRINT_PATH="$APPLY_BLUEPRINT_PATH" \
+ OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/sandbox-policy.yaml" \
node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
await main(['apply', '--profile', 'ncp']);
diff --git a/test/runtime/policy/policies.test.ts b/test/runtime/policy/policies.test.ts
index 283d2c912a8..3a5c1132e1b 100644
--- a/test/runtime/policy/policies.test.ts
+++ b/test/runtime/policy/policies.test.ts
@@ -616,10 +616,16 @@ exit 1
it("applyPreset does not create temp dirs when bounded policy observation loses OpenShell", () => {
const policyTempPrefix = path.join(os.tmpdir(), "nemoclaw-policy-");
- const resolveSpy = vi
- .spyOn(resolveOpenshellModule, "resolveOpenshell")
- .mockReturnValueOnce(fakeOpenshell)
- .mockReturnValue(null);
+ const policyReadSpy = vi
+ .spyOn(policyReaderModule.syncCliOpenShellSandboxPolicyReader, "readSandboxPolicy")
+ .mockReturnValue({
+ ok: false,
+ error: {
+ kind: "command",
+ reason: "failed",
+ message: "OpenShell became unavailable during the bounded policy read.",
+ },
+ });
const mkdtempSpy = vi.spyOn(fs, "mkdtempSync");
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -639,7 +645,7 @@ exit 1
mkdtempSpy.mock.calls.filter(([prefix]) => String(prefix).startsWith(policyTempPrefix)),
).toEqual([]);
} finally {
- resolveSpy.mockRestore();
+ policyReadSpy.mockRestore();
mkdtempSpy.mockRestore();
errSpy.mockRestore();
logSpy.mockRestore();
From 1cf2440c013c80b3a3133a08555093f2faee5590 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 06:35:40 -0700
Subject: [PATCH 38/49] test(mcp): simplify projection status coverage
---
...idge-adapter-deepagents-projection.test.ts | 15 ++--
.../mcp-bridge-status-resolution.test.ts | 79 +++++++++----------
2 files changed, 48 insertions(+), 46 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index f0a4ad5d8c5..35c105428f6 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -291,7 +291,13 @@ describe("Deep Agents managed MCP projection safety", () => {
},
])(
"classifies a $diagnostic that appears after a missing-path open (#10754)",
- ({ appearedType, diagnostic }) => {
+ ({
+ appearedType,
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ }) => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
@@ -306,10 +312,9 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(result.stderr).toContain(
`Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
);
- expect(appearedType === "symlink" ? result.configIsSymlink : result.configIsFifo).toBe(true);
- expect(result.managedSymlinkTargetText).toBe(
- appearedType === "symlink" ? `${JSON.stringify(emptyProjection, null, 2)}\n` : null,
- );
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
},
);
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 3582140ab2c..44a7f9ad6ed 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -205,31 +205,29 @@ ${body}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
it(
- "returns a type-specific failure for unsafe Deep Agents projection races (#10754)",
+ "preserves an unsafe Deep Agents projection failure across credential observations (#10754)",
{ timeout: 120_000 },
() => {
const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
const projection = { mcpServers: {} };
- const unsafeResults = [
- ...(["symlink", "fifo"] as const).map((appearedType) =>
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterMissingManagedOpen: appearedType,
- timeoutMs: 30_000,
- }),
- ),
- runDeepAgentsConfigCommand(statusCommand, projection, "v2", undefined, 0o600, {
- swapAfterManagedEloop: true,
- symlink: true,
+ const unsafeResult = runDeepAgentsConfigCommand(
+ statusCommand,
+ projection,
+ "v2",
+ undefined,
+ 0o600,
+ {
+ swapAfterMissingManagedOpen: "symlink",
timeoutMs: 30_000,
- }),
- ];
- unsafeResults.forEach((result) => {
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain("Unsafe managed Deep Agents MCP projection path:");
- expect(result.configIsSymlink || result.configIsFifo).toBe(true);
- });
- const unsafeDetails = unsafeResults.map((result) => result.stderr.trim());
+ },
+ );
+ expect(unsafeResult.status).toBe(2);
+ expect(unsafeResult.stdout.trim()).toBe("");
+ expect(unsafeResult.stderr).toContain(
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
+ );
+ expect(unsafeResult.configIsSymlink).toBe(true);
+ const unsafeDetail = unsafeResult.stderr.trim();
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -249,26 +247,25 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
},
});
const outcomes = [];
- for (const detail of ${JSON.stringify(unsafeDetails)}) {
- for (const observation of [null, "absent", "canonical"]) {
- providerCredentialObservation = observation;
- processRecovery.executeSandboxCommand = () => ({
- status: 2,
- stdout: "",
- stderr: detail + "\n",
- });
- logLines.length = 0;
- errorLines.length = 0;
- await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
- outcomes.push({
- detail,
- observation: observation ?? "unavailable",
- exitCode: process.exitCode ?? 0,
- stdout: logLines.join("\n"),
- stderr: errorLines.join("\n"),
- });
- process.exitCode = 0;
- }
+ const detail = ${JSON.stringify(unsafeDetail)};
+ for (const observation of [null, "absent", "canonical"]) {
+ providerCredentialObservation = observation;
+ processRecovery.executeSandboxCommand = () => ({
+ status: 2,
+ stdout: "",
+ stderr: detail + "\n",
+ });
+ logLines.length = 0;
+ errorLines.length = 0;
+ await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
+ outcomes.push({
+ detail,
+ observation: observation ?? "unavailable",
+ exitCode: process.exitCode ?? 0,
+ stdout: logLines.join("\n"),
+ stderr: errorLines.join("\n"),
+ });
+ process.exitCode = 0;
}
writeHarnessResult(JSON.stringify(outcomes));
`,
@@ -281,7 +278,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stderr: string;
}>;
- expect(outcomes).toHaveLength(9);
+ expect(outcomes).toHaveLength(3);
outcomes.forEach((outcome) => {
expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
expect(outcome.exitCode).toBe(2);
From 9f9eeb6751a649a24b7b715c34bb5e3aab3bddb6 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 06:36:10 -0700
Subject: [PATCH 39/49] fix(mcp): keep projection status change scoped
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
test/e2e-test.sh | 96 ++++++----------------------
test/runtime/policy/policies.test.ts | 16 ++---
2 files changed, 23 insertions(+), 89 deletions(-)
diff --git a/test/e2e-test.sh b/test/e2e-test.sh
index 9a27498a085..5a11a76ca11 100755
--- a/test/e2e-test.sh
+++ b/test/e2e-test.sh
@@ -111,73 +111,19 @@ fi
# -------------------------------------------------------
info "4. Verify blueprint runner plan command"
# -------------------------------------------------------
-prepare_command_blueprint() {
- local destination="$1"
- node --input-type=module -e "
- import { createRequire } from 'node:module';
- import { writeFileSync } from 'node:fs';
- import { join } from 'node:path';
- const require = createRequire('/opt/nemoclaw/');
- const YAML = require('yaml');
- const destination = process.argv[1];
- const blueprint = {
- version: '1.0',
- components: {
- sandbox: {
- image: 'openclaw',
- name: 'openclaw',
- forward_ports: [18789],
- },
- inference: {
- profiles: {
- ncp: {
- provider_type: 'nvidia',
- provider_name: 'nvidia-ncp',
- endpoint: '',
- model: 'nvidia/nemotron-3-super-120b-a12b',
- },
- },
- },
- policy: {
- additions: {
- nim_service: {
- name: 'nim_service',
- endpoints: [{ host: '93.184.216.34', port: 8000, access: 'full' }],
- },
- },
- },
- },
- };
- writeFileSync(join(destination, 'blueprint.yaml'), YAML.stringify(blueprint));
- writeFileSync(
- join(destination, 'sandbox-policy.yaml'),
- YAML.stringify({ version: 1, network_policies: {} }),
- );
- writeFileSync(
- join(destination, 'private-networks.yaml'),
- YAML.stringify({ ipv4: [], ipv6: [], names: [] }),
- );
- " "$destination"
-}
-
-(
- PLAN_BLUEPRINT_PATH=$(mktemp -d)
- trap 'rm -rf "$PLAN_BLUEPRINT_PATH"' EXIT
- prepare_command_blueprint "$PLAN_BLUEPRINT_PATH"
- cd "$PLAN_BLUEPRINT_PATH"
- # Steps 3 and 3b validate the shipped blueprint. This command smoke test uses
- # an independent fixture with a stable public IP so DNS cannot preempt the
- # expected OpenShell prerequisite boundary.
- NEMOCLAW_BLUEPRINT_PATH="$PLAN_BLUEPRINT_PATH" node --input-type=module -e "
- const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
- try {
- await main(['plan', '--profile', 'ncp', '--dry-run']);
- } catch (err) {
- if (!err.message.includes('openshell CLI not found')) throw err;
- console.log('EXPECTED_ERROR: ' + err.message);
- }
- "
-) 2>&1 | tee /tmp/plan-output.txt
+cd /opt/nemoclaw-blueprint
+# Runner will fail at openshell prereq check (expected in test container).
+# Use 'ncp' profile (empty endpoint skips SSRF DNS lookup in sandbox).
+# Catch only the expected error — anything else propagates as a real failure.
+NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint node --input-type=module -e "
+ const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
+ try {
+ await main(['plan', '--profile', 'ncp', '--dry-run']);
+ } catch (err) {
+ if (!err.message.includes('openshell CLI not found')) throw err;
+ console.log('EXPECTED_ERROR: ' + err.message);
+ }
+" 2>&1 | tee /tmp/plan-output.txt
if grep -q "RUN_ID:" /tmp/plan-output.txt; then
pass "Blueprint plan generates run ID"
else
@@ -202,15 +148,13 @@ info "4b. Verify blueprint runner apply smoke test"
# return the same metadata + YAML shape as OpenShell 0.0.72; an empty successful
# response is intentionally rejected by the runner.
FAKE_OPENSHELL_BIN=$(mktemp -d)
-APPLY_BLUEPRINT_PATH=$(mktemp -d)
APPLY_OUTPUT=$(mktemp)
APPLY_CALLS="$FAKE_OPENSHELL_BIN/calls"
cleanup_apply_fixture() {
- rm -rf "$FAKE_OPENSHELL_BIN" "$APPLY_BLUEPRINT_PATH"
+ rm -rf "$FAKE_OPENSHELL_BIN"
rm -f "$APPLY_OUTPUT" "$APPLY_CALLS"
}
trap cleanup_apply_fixture EXIT
-prepare_command_blueprint "$APPLY_BLUEPRINT_PATH"
cat >"$FAKE_OPENSHELL_BIN/openshell" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
@@ -244,7 +188,7 @@ if [ "${1:-} ${2:-}" = "policy get" ]; then
fi
if [ "${1:-} ${2:-}" = "policy get" ] && [[ " $* " == *" --output json "* ]]; then
sandbox="${@: -1}"
- policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
+ policy_file=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
policy_hash=sha256:fixture-policy
policy_version=1
if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
@@ -274,12 +218,8 @@ case "$*" in
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
exit 64
fi
- policy_file="${OPENSHELL_SANDBOX_POLICY:?}"
- if [ -f "${BASH_SOURCE[0]%/*}/effective-policy.yaml" ]; then
- policy_file="${BASH_SOURCE[0]%/*}/effective-policy.yaml"
- fi
printf '%s\n' 'Policy for sandbox fixture' '---'
- cat "$policy_file"
+ cat /opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml
;;
"policy get "*)
echo "unexpected policy read: expected policy get -g fixture-gateway --base " >&2
@@ -289,8 +229,8 @@ esac
SH
chmod 0755 "$FAKE_OPENSHELL_BIN/openshell"
PATH="$FAKE_OPENSHELL_BIN:$PATH" \
- NEMOCLAW_BLUEPRINT_PATH="$APPLY_BLUEPRINT_PATH" \
- OPENSHELL_SANDBOX_POLICY="$APPLY_BLUEPRINT_PATH/sandbox-policy.yaml" \
+ NEMOCLAW_BLUEPRINT_PATH=/opt/nemoclaw-blueprint \
+ OPENSHELL_SANDBOX_POLICY=/opt/nemoclaw-blueprint/policies/openclaw-sandbox.yaml \
node --input-type=module -e "
const { main } = await import('/opt/nemoclaw/dist/blueprint/runner.js');
await main(['apply', '--profile', 'ncp']);
diff --git a/test/runtime/policy/policies.test.ts b/test/runtime/policy/policies.test.ts
index 3a5c1132e1b..283d2c912a8 100644
--- a/test/runtime/policy/policies.test.ts
+++ b/test/runtime/policy/policies.test.ts
@@ -616,16 +616,10 @@ exit 1
it("applyPreset does not create temp dirs when bounded policy observation loses OpenShell", () => {
const policyTempPrefix = path.join(os.tmpdir(), "nemoclaw-policy-");
- const policyReadSpy = vi
- .spyOn(policyReaderModule.syncCliOpenShellSandboxPolicyReader, "readSandboxPolicy")
- .mockReturnValue({
- ok: false,
- error: {
- kind: "command",
- reason: "failed",
- message: "OpenShell became unavailable during the bounded policy read.",
- },
- });
+ const resolveSpy = vi
+ .spyOn(resolveOpenshellModule, "resolveOpenshell")
+ .mockReturnValueOnce(fakeOpenshell)
+ .mockReturnValue(null);
const mkdtempSpy = vi.spyOn(fs, "mkdtempSync");
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -645,7 +639,7 @@ exit 1
mkdtempSpy.mock.calls.filter(([prefix]) => String(prefix).startsWith(policyTempPrefix)),
).toEqual([]);
} finally {
- policyReadSpy.mockRestore();
+ resolveSpy.mockRestore();
mkdtempSpy.mockRestore();
errSpy.mockRestore();
logSpy.mockRestore();
From 6572de2341ab48e7e05386d859cb1094ae192a8c Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 06:48:16 -0700
Subject: [PATCH 40/49] test(mcp): exercise unsafe status composition
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 1 -
.../mcp-bridge-status-resolution.test.ts | 136 ++++++++++--------
2 files changed, 74 insertions(+), 63 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index b74dbb11036..2f2a7e57fe9 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -43,7 +43,6 @@ Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `
It does not follow a final symbolic link or read from a FIFO.
If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
-For this unsafe path-type result, the command exits with status `2`.
Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 44a7f9ad6ed..86c29cd8483 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -7,12 +7,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
-import {
- baseEntry,
- runDeepAgentsConfigCommand,
-} from "../../../../test/helpers/mcp-bridge-adapter-deepagents-fixture";
-
-import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
const sourceRequireHook = path.resolve("test/helpers/onboard-script-mocks.cjs");
const sourceNodeOptions = [process.env.NODE_OPTIONS, `--require=${sourceRequireHook}`]
@@ -43,6 +37,7 @@ const gatewayRuntime = require("./src/lib/gateway-runtime-action.js");
const providerCommands = require("./src/lib/adapters/openshell/provider-command.js");
const policies = require("./src/lib/policy/index.js");
const processRecovery = require("./src/lib/actions/sandbox/process-recovery.js");
+const deepAgentsFixture = require("./test/helpers/mcp-bridge-adapter-deepagents-fixture.ts");
gatewayRuntime.recoverNamedGatewayRuntime = async () => ({
recovered: true,
attempted: false,
@@ -93,6 +88,8 @@ providerCommands.runOpenshellProviderCommand = (args) => {
let activePolicyState = "match";
policies.getPresetContentGatewayState = () => activePolicyState;
const executedSandboxCommands = [];
+let deepAgentsManagedOptions = null;
+let lastDeepAgentsFixtureResult = null;
let providerCredentialObservation = "v11";
let credentialObservationCount = 0;
processRecovery.executeSandboxExecCommand = () => {
@@ -107,6 +104,17 @@ processRecovery.executeSandboxExecCommand = () => {
};
processRecovery.executeSandboxCommand = (sandboxName, command) => {
executedSandboxCommands.push(command);
+ if (deepAgentsManagedOptions !== null) {
+ lastDeepAgentsFixtureResult = deepAgentsFixture.runDeepAgentsConfigCommand(
+ command,
+ { mcpServers: {} },
+ "v2",
+ undefined,
+ 0o600,
+ deepAgentsManagedOptions,
+ );
+ return lastDeepAgentsFixtureResult;
+ }
if (command.includes("NEMOCLAW_MCP_PROBE")) {
const resultMarker = command.match(/__NEMOCLAW_SANDBOX_EXEC_STARTED___[0-9a-f]{32}/)?.[0];
if (!resultMarker) throw new Error("credential probe result marker missing");
@@ -204,30 +212,41 @@ ${body}
}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
- it(
- "preserves an unsafe Deep Agents projection failure across credential observations (#10754)",
+ it.each([
+ {
+ caseName: "a symlink appearing after a missing-path open",
+ credentialObservation: null,
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ managedOptions: { swapAfterMissingManagedOpen: "symlink", timeoutMs: 30_000 },
+ },
+ {
+ caseName: "a FIFO appearing after a missing-path open",
+ credentialObservation: "absent",
+ diagnostic: "FIFO",
+ expectedConfigIsFifo: true,
+ expectedConfigIsSymlink: false,
+ managedOptions: { swapAfterMissingManagedOpen: "fifo", timeoutMs: 30_000 },
+ },
+ {
+ caseName: "a symlink restored after ELOOP",
+ credentialObservation: "canonical",
+ diagnostic: "symbolic link",
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: true,
+ managedOptions: { swapAfterManagedEloop: true, symlink: true, timeoutMs: 30_000 },
+ },
+ ])(
+ "propagates $caseName through status with credential observation $credentialObservation (#10754)",
{ timeout: 120_000 },
- () => {
- const statusCommand = buildDeepAgentsMcpStatusCommand(baseEntry);
- const projection = { mcpServers: {} };
- const unsafeResult = runDeepAgentsConfigCommand(
- statusCommand,
- projection,
- "v2",
- undefined,
- 0o600,
- {
- swapAfterMissingManagedOpen: "symlink",
- timeoutMs: 30_000,
- },
- );
- expect(unsafeResult.status).toBe(2);
- expect(unsafeResult.stdout.trim()).toBe("");
- expect(unsafeResult.stderr).toContain(
- "Unsafe managed Deep Agents MCP projection path: symbolic link",
- );
- expect(unsafeResult.configIsSymlink).toBe(true);
- const unsafeDetail = unsafeResult.stderr.trim();
+ ({
+ credentialObservation,
+ diagnostic,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ managedOptions,
+ }) => {
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -246,45 +265,38 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
managedServerNames: ["github"],
},
});
- const outcomes = [];
- const detail = ${JSON.stringify(unsafeDetail)};
- for (const observation of [null, "absent", "canonical"]) {
- providerCredentialObservation = observation;
- processRecovery.executeSandboxCommand = () => ({
- status: 2,
- stdout: "",
- stderr: detail + "\n",
- });
- logLines.length = 0;
- errorLines.length = 0;
- await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
- outcomes.push({
- detail,
- observation: observation ?? "unavailable",
- exitCode: process.exitCode ?? 0,
- stdout: logLines.join("\n"),
- stderr: errorLines.join("\n"),
- });
- process.exitCode = 0;
- }
- writeHarnessResult(JSON.stringify(outcomes));
+ providerCredentialObservation = ${JSON.stringify(credentialObservation)};
+ deepAgentsManagedOptions = ${JSON.stringify(managedOptions)};
+ await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
+ const exitCode = process.exitCode ?? 0;
+ process.exitCode = 0;
+ writeHarnessResult(JSON.stringify({
+ exitCode,
+ stdout: logLines.join("\n"),
+ stderr: errorLines.join("\n"),
+ fixtureStatus: lastDeepAgentsFixtureResult?.status ?? null,
+ configIsFifo: lastDeepAgentsFixtureResult?.configIsFifo ?? false,
+ configIsSymlink: lastDeepAgentsFixtureResult?.configIsSymlink ?? false,
+ }));
`,
);
- const outcomes = JSON.parse(stdout) as Array<{
- detail: string;
- observation: string;
+ const outcome = JSON.parse(stdout) as {
exitCode: number;
stdout: string;
stderr: string;
- }>;
+ fixtureStatus: number | null;
+ configIsFifo: boolean;
+ configIsSymlink: boolean;
+ };
- expect(outcomes).toHaveLength(3);
- outcomes.forEach((outcome) => {
- expect(["unavailable", "absent", "canonical"]).toContain(outcome.observation);
- expect(outcome.exitCode).toBe(2);
- expect(outcome.stdout).toBe("");
- expect(outcome.stderr).toContain(outcome.detail);
- });
+ expect(outcome.fixtureStatus).toBe(2);
+ expect(outcome.exitCode).toBe(2);
+ expect(outcome.stdout).toBe("");
+ expect(outcome.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
+ expect(outcome.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(outcome.configIsSymlink).toBe(expectedConfigIsSymlink);
},
);
From dead0c84e061c79ce706c40bec3fba1d353c099d Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 09:02:52 -0700
Subject: [PATCH 41/49] docs(mcp): clarify projection path diagnostics
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 9 +++++----
.../mcp-bridge-adapter-deepagents-projection.test.ts | 2 +-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 2f2a7e57fe9..d2cb2c55d59 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -39,11 +39,12 @@ For a trusted private server, status resolves the endpoint without changing mana
-Before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`, Deep Agents Code v2 `mcp status` checks the path type.
+On a LangChain Deep Agents Code image with managed MCP capability v2, `mcp status` checks the final path type before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`.
It does not follow a final symbolic link or read from a FIFO.
-If the path is a symbolic link, FIFO, or another non-regular file, the command names the path type, writes no status output, and exits with status `2`.
-The diagnostic starts with `Unsafe managed Deep Agents MCP projection path:` and names a type such as `symbolic link`, `FIFO`, or `non-regular file`.
-Deep Agents Code v1 reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
+If the final entry is unsafe, the command writes no standard output and exits with status `2`.
+The standard error line starts with ` Unsafe managed Deep Agents MCP projection path:`.
+It ends with `symbolic link`, `FIFO`, or `non-regular file`.
+A Deep Agents v1 image reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 35c105428f6..884963b6fa3 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -398,7 +398,7 @@ describe("Deep Agents managed MCP projection safety", () => {
expect(existingResult.configText).toBe(attackerProjection);
});
- it("keeps forced removal identity-bound during a descriptor rewrite (#10754)", () => {
+ it("keeps forced removal identity-bound during a descriptor rewrite", () => {
const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const raced = runDeepAgentsConfigCommand(
forcedCommand,
From 88dcb68afbc624fdbe62a47eda8c117015ffe750 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 09:08:07 -0700
Subject: [PATCH 42/49] docs(mcp): limit unsafe projection claim
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index d2cb2c55d59..d25f8c3ab7b 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -41,7 +41,7 @@ For a trusted private server, status resolves the endpoint without changing mana
On a LangChain Deep Agents Code image with managed MCP capability v2, `mcp status` checks the final path type before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`.
It does not follow a final symbolic link or read from a FIFO.
-If the final entry is unsafe, the command writes no standard output and exits with status `2`.
+If the final entry is a symbolic link, FIFO, or another non-regular object, the command writes no standard output and exits with status `2`.
The standard error line starts with ` Unsafe managed Deep Agents MCP projection path:`.
It ends with `symbolic link`, `FIFO`, or `non-regular file`.
A Deep Agents v1 image reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
From da3d372770213f02b77ef4bcc01e0899d199b1c8 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 09:30:55 -0700
Subject: [PATCH 43/49] docs(mcp): correct unsafe projection prefix
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index d25f8c3ab7b..948842c5429 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,7 +42,7 @@ For a trusted private server, status resolves the endpoint without changing mana
On a LangChain Deep Agents Code image with managed MCP capability v2, `mcp status` checks the final path type before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`.
It does not follow a final symbolic link or read from a FIFO.
If the final entry is a symbolic link, FIFO, or another non-regular object, the command writes no standard output and exits with status `2`.
-The standard error line starts with ` Unsafe managed Deep Agents MCP projection path:`.
+The standard error line starts with `Unsafe managed Deep Agents MCP projection path:`.
It ends with `symbolic link`, `FIFO`, or `non-regular file`.
A Deep Agents v1 image reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
From 53aeb71b28bf2e2bec262b7463ac7655b98a21ee Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 09:51:53 -0700
Subject: [PATCH 44/49] test(mcp): preserve unsafe projection evidence
---
.../sandbox/mcp-bridge-adapter-deepagents-projection.test.ts | 2 ++
test/helpers/mcp-bridge-adapter-deepagents-fixture.ts | 3 +++
2 files changed, 5 insertions(+)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 884963b6fa3..2aa145c7018 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -426,6 +426,7 @@ describe("Deep Agents managed MCP projection safety", () => {
);
expect(forcedSymlink.status).toBe(2);
expect(forcedSymlink.configExists).toBe(true);
+ expect(forcedSymlink.configIsSymlink).toBe(true);
expect(forcedSymlink.managedSymlinkTargetExists).toBe(true);
expect(forcedSymlink.managedSymlinkTargetText).toBe(
`${JSON.stringify(emptyProjection, null, 2)}\n`,
@@ -443,6 +444,7 @@ describe("Deep Agents managed MCP projection safety", () => {
{ mode: 0o644 },
);
expect(forcedUnsafeMode.status).toBe(2);
+ expect(forcedUnsafeMode.configMode).toBe(0o644);
expect(forcedUnsafeMode.config).toEqual(emptyProjection);
});
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index 743f48ef5f9..f7c46dbcfde 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -29,6 +29,7 @@ export interface DeepAgentsConfigCommandResult {
configIsFifo: boolean;
configIsSocket: boolean;
configIsSymlink: boolean;
+ configMode: number | null;
config: Record | null;
configText: string | null;
legacyConfigExists: boolean;
@@ -410,6 +411,7 @@ export function runDeepAgentsConfigCommand(
const configIsSocket = configStat?.isSocket() === true;
const configIsSymlink = configStat?.isSymbolicLink() === true;
const configIsDirectory = configStat?.isDirectory() === true;
+ const configMode = configStat === null ? null : configStat.mode & 0o777;
const configText =
configExists && !configIsFifo && !configIsSocket && !configIsSymlink && !configIsDirectory
? fs.readFileSync(configPath, "utf-8")
@@ -454,6 +456,7 @@ export function runDeepAgentsConfigCommand(
configIsFifo,
configIsSocket,
configIsSymlink,
+ configMode,
config: parseConfigText(configText),
configText,
legacyConfigExists,
From 9c2ed4d29317a26d5700dabc98921bbc7622e0d5 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 10:08:44 -0700
Subject: [PATCH 45/49] docs(mcp): describe diagnostic indentation
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
docs/manage-sandboxes/manage-mcp-servers.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 948842c5429..c018783c33c 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -42,7 +42,7 @@ For a trusted private server, status resolves the endpoint without changing mana
On a LangChain Deep Agents Code image with managed MCP capability v2, `mcp status` checks the final path type before it reads `/sandbox/.deepagents/.nemoclaw-mcp.json`.
It does not follow a final symbolic link or read from a FIFO.
If the final entry is a symbolic link, FIFO, or another non-regular object, the command writes no standard output and exits with status `2`.
-The standard error line starts with `Unsafe managed Deep Agents MCP projection path:`.
+The standard error line starts with two spaces followed by `Unsafe managed Deep Agents MCP projection path:`.
It ends with `symbolic link`, `FIFO`, or `non-regular file`.
A Deep Agents v1 image reads the legacy `.deepagents/.mcp.json` path and does not expose these type-specific diagnostics.
From bd8f1fcb46119c00c4c28e3ccf40f7d7afb2cb70 Mon Sep 17 00:00:00 2001
From: Prekshi Vyas
Date: Wed, 2 Sep 2026 10:17:12 -0700
Subject: [PATCH 46/49] fix(mcp): reject projection removal races
---
...idge-adapter-deepagents-projection.test.ts | 16 +++++++++++
...cp-bridge-adapter-deepagents-projection.ts | 7 ++++-
.../sandbox/mcp-bridge-adapter-status.ts | 3 ++
.../actions/sandbox/mcp-bridge-adapters.ts | 1 +
.../mcp-bridge-status-resolution.test.ts | 28 ++++++++++++++-----
src/lib/actions/sandbox/mcp-bridge-status.ts | 4 ++-
.../mcp-bridge-adapter-deepagents-fixture.ts | 8 +++++-
7 files changed, 57 insertions(+), 10 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index 2aa145c7018..cc50536e7c5 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -234,6 +234,22 @@ describe("Deep Agents managed MCP projection safety", () => {
},
);
+ it("rejects a projection removed after it is opened (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { removeAfterManagedOpen: true, timeoutMs: managedCommandTimeoutMs },
+ );
+ expect(result.status).toBe(2);
+ expect(result.stdout.trim()).toBe("");
+ expect(result.stderr).toContain("Could not inspect managed Deep Agents MCP state");
+ expect(result.stderr).toContain("managed MCP projection changed while reading");
+ expect(result.configExists).toBe(false);
+ });
+
it("rejects a symbolic-link replacement while reading the projection (#10754)", () => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index d484ec70a7c..626d8f399ab 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
export const DEEPAGENTS_MCP_MAX_SERVERS = 64;
+export const DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL =
+ "managed MCP projection changed while reading";
export const DEEPAGENTS_STRICT_JSON_HELPERS = [
"def reject_duplicate_keys(pairs):",
@@ -102,8 +104,11 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
" if remaining or not stable:",
- " raise ValueError('managed MCP projection changed while reading')",
+ ` raise ValueError(${JSON.stringify(DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL)})`,
" return b''.join(chunks), managed_fingerprint(after), descriptor",
+ " except FileNotFoundError as exc:",
+ " os.close(descriptor)",
+ ` raise ValueError(${JSON.stringify(DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL)}) from exc`,
" except Exception:",
" os.close(descriptor)",
" raise",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index e46d13bbcde..f292883c6ae 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -8,10 +8,13 @@ import {
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
} from "./mcp-bridge/deepagents-legacy-config";
import {
+ DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
DEEPAGENTS_STRICT_JSON_HELPERS,
} from "./mcp-bridge-adapter-deepagents-projection";
+export { DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL };
+
// NemoClaw owns this dedicated projection. Deep Agents Code's user/project
// `.mcp.json` discovery is disabled in the managed image so user-authored MCP
// state can never be layered over the validated registry projection.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapters.ts b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
index afc9657ce6e..f58054702ef 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapters.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
@@ -61,6 +61,7 @@ export {
buildHermesMcpStatusCommand,
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
+ DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_MCP_CONFIG_PATH,
DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
mcporterHeadersMatchExpected,
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 86c29cd8483..8eec782d354 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -216,35 +216,48 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
{
caseName: "a symlink appearing after a missing-path open",
credentialObservation: null,
- diagnostic: "symbolic link",
+ expectedConfigExists: true,
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
+ expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
managedOptions: { swapAfterMissingManagedOpen: "symlink", timeoutMs: 30_000 },
},
{
caseName: "a FIFO appearing after a missing-path open",
credentialObservation: "absent",
- diagnostic: "FIFO",
+ expectedConfigExists: true,
expectedConfigIsFifo: true,
expectedConfigIsSymlink: false,
+ expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: FIFO",
managedOptions: { swapAfterMissingManagedOpen: "fifo", timeoutMs: 30_000 },
},
{
caseName: "a symlink restored after ELOOP",
credentialObservation: "canonical",
- diagnostic: "symbolic link",
+ expectedConfigExists: true,
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
+ expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
managedOptions: { swapAfterManagedEloop: true, symlink: true, timeoutMs: 30_000 },
},
+ {
+ caseName: "a projection removed after it is opened",
+ credentialObservation: "canonical",
+ expectedConfigExists: false,
+ expectedConfigIsFifo: false,
+ expectedConfigIsSymlink: false,
+ expectedDiagnostic: "managed MCP projection changed while reading",
+ managedOptions: { removeAfterManagedOpen: true, timeoutMs: 30_000 },
+ },
])(
"propagates $caseName through status with credential observation $credentialObservation (#10754)",
{ timeout: 120_000 },
({
credentialObservation,
- diagnostic,
+ expectedConfigExists,
expectedConfigIsFifo,
expectedConfigIsSymlink,
+ expectedDiagnostic,
managedOptions,
}) => {
const home = createTempHome("nemoclaw-mcp-status-projection-");
@@ -275,6 +288,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stdout: logLines.join("\n"),
stderr: errorLines.join("\n"),
fixtureStatus: lastDeepAgentsFixtureResult?.status ?? null,
+ configExists: lastDeepAgentsFixtureResult?.configExists ?? false,
configIsFifo: lastDeepAgentsFixtureResult?.configIsFifo ?? false,
configIsSymlink: lastDeepAgentsFixtureResult?.configIsSymlink ?? false,
}));
@@ -285,6 +299,7 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stdout: string;
stderr: string;
fixtureStatus: number | null;
+ configExists: boolean;
configIsFifo: boolean;
configIsSymlink: boolean;
};
@@ -292,9 +307,8 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
expect(outcome.fixtureStatus).toBe(2);
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
- expect(outcome.stderr).toContain(
- `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
- );
+ expect(outcome.stderr).toContain(expectedDiagnostic);
+ expect(outcome.configExists).toBe(expectedConfigExists);
expect(outcome.configIsFifo).toBe(expectedConfigIsFifo);
expect(outcome.configIsSymlink).toBe(expectedConfigIsSymlink);
},
diff --git a/src/lib/actions/sandbox/mcp-bridge-status.ts b/src/lib/actions/sandbox/mcp-bridge-status.ts
index 7d04efa0f55..f62cf23f3f4 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status.ts
@@ -8,6 +8,7 @@ import {
buildHermesMcpStatusCommand,
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
+ DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
openClawMcporterRoot,
} from "./mcp-bridge-adapters";
@@ -139,7 +140,8 @@ function getAdapterRegistration(
const normalizedDetail = detail.trim();
if (
adapter === "deepagents-config" &&
- normalizedDetail.startsWith(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX)
+ (normalizedDetail.startsWith(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX) ||
+ normalizedDetail.endsWith(`: ${DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL}`))
) {
throw new McpBridgeError(normalizedDetail, 2);
}
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index f7c46dbcfde..e4cfcc20c9b 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -45,6 +45,7 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
+ removeAfterManagedOpen?: boolean;
swapAfterManagedEloop?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -59,7 +60,7 @@ export interface DeepAgentsManagedFixtureOptions {
type ManagedSwap = {
content?: string;
- kind: "fifo" | "regular" | "socket" | "symlink";
+ kind: "absent" | "fifo" | "regular" | "socket" | "symlink";
phase:
| "after-missing-projection-open"
| "after-projection-eloop"
@@ -77,6 +78,9 @@ function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSw
if (options.swapAfterManagedOpen) {
return { kind: options.swapAfterManagedOpen, phase: "after-projection-open" };
}
+ if (options.removeAfterManagedOpen) {
+ return { kind: "absent", phase: "after-projection-open" };
+ }
if (options.swapAfterMissingManagedOpen) {
return {
kind: options.swapAfterMissingManagedOpen,
@@ -198,6 +202,8 @@ function createDeepAgentsFixturePythonExecutable(
" os.unlink(managed_path)",
" except FileNotFoundError:",
" pass",
+ " if managed_swap_kind == 'absent':",
+ " return",
" if managed_swap_kind == 'symlink':",
" os.symlink(managed_target, managed_path)",
" elif managed_swap_kind == 'fifo':",
From f3b933068925d6e41ce028ba4fa46ce847a34fcd Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 10:41:53 -0700
Subject: [PATCH 47/49] fix(mcp): keep status within file-type scope
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
...idge-adapter-deepagents-projection.test.ts | 16 -----------
...cp-bridge-adapter-deepagents-projection.ts | 7 +----
.../sandbox/mcp-bridge-adapter-status.ts | 3 --
.../actions/sandbox/mcp-bridge-adapters.ts | 1 -
.../mcp-bridge-status-resolution.test.ts | 28 +++++--------------
src/lib/actions/sandbox/mcp-bridge-status.ts | 4 +--
.../mcp-bridge-adapter-deepagents-fixture.ts | 8 +-----
7 files changed, 10 insertions(+), 57 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
index cc50536e7c5..2aa145c7018 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.test.ts
@@ -234,22 +234,6 @@ describe("Deep Agents managed MCP projection safety", () => {
},
);
- it("rejects a projection removed after it is opened (#10754)", () => {
- const result = runDeepAgentsConfigCommand(
- buildDeepAgentsMcpStatusCommand(baseEntry),
- emptyProjection,
- "v2",
- undefined,
- 0o600,
- { removeAfterManagedOpen: true, timeoutMs: managedCommandTimeoutMs },
- );
- expect(result.status).toBe(2);
- expect(result.stdout.trim()).toBe("");
- expect(result.stderr).toContain("Could not inspect managed Deep Agents MCP state");
- expect(result.stderr).toContain("managed MCP projection changed while reading");
- expect(result.configExists).toBe(false);
- });
-
it("rejects a symbolic-link replacement while reading the projection (#10754)", () => {
const result = runDeepAgentsConfigCommand(
buildDeepAgentsMcpStatusCommand(baseEntry),
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index 626d8f399ab..d484ec70a7c 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
export const DEEPAGENTS_MCP_MAX_SERVERS = 64;
-export const DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL =
- "managed MCP projection changed while reading";
export const DEEPAGENTS_STRICT_JSON_HELPERS = [
"def reject_duplicate_keys(pairs):",
@@ -104,11 +102,8 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
" stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
" if remaining or not stable:",
- ` raise ValueError(${JSON.stringify(DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL)})`,
+ " raise ValueError('managed MCP projection changed while reading')",
" return b''.join(chunks), managed_fingerprint(after), descriptor",
- " except FileNotFoundError as exc:",
- " os.close(descriptor)",
- ` raise ValueError(${JSON.stringify(DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL)}) from exc`,
" except Exception:",
" os.close(descriptor)",
" raise",
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
index f292883c6ae..e46d13bbcde 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
@@ -8,13 +8,10 @@ import {
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
} from "./mcp-bridge/deepagents-legacy-config";
import {
- DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
DEEPAGENTS_STRICT_JSON_HELPERS,
} from "./mcp-bridge-adapter-deepagents-projection";
-export { DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL };
-
// NemoClaw owns this dedicated projection. Deep Agents Code's user/project
// `.mcp.json` discovery is disabled in the managed image so user-authored MCP
// state can never be layered over the validated registry projection.
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapters.ts b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
index f58054702ef..afc9657ce6e 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapters.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapters.ts
@@ -61,7 +61,6 @@ export {
buildHermesMcpStatusCommand,
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
- DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_MCP_CONFIG_PATH,
DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
mcporterHeadersMatchExpected,
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 8eec782d354..86c29cd8483 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -216,48 +216,35 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
{
caseName: "a symlink appearing after a missing-path open",
credentialObservation: null,
- expectedConfigExists: true,
+ diagnostic: "symbolic link",
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
- expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
managedOptions: { swapAfterMissingManagedOpen: "symlink", timeoutMs: 30_000 },
},
{
caseName: "a FIFO appearing after a missing-path open",
credentialObservation: "absent",
- expectedConfigExists: true,
+ diagnostic: "FIFO",
expectedConfigIsFifo: true,
expectedConfigIsSymlink: false,
- expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: FIFO",
managedOptions: { swapAfterMissingManagedOpen: "fifo", timeoutMs: 30_000 },
},
{
caseName: "a symlink restored after ELOOP",
credentialObservation: "canonical",
- expectedConfigExists: true,
+ diagnostic: "symbolic link",
expectedConfigIsFifo: false,
expectedConfigIsSymlink: true,
- expectedDiagnostic: "Unsafe managed Deep Agents MCP projection path: symbolic link",
managedOptions: { swapAfterManagedEloop: true, symlink: true, timeoutMs: 30_000 },
},
- {
- caseName: "a projection removed after it is opened",
- credentialObservation: "canonical",
- expectedConfigExists: false,
- expectedConfigIsFifo: false,
- expectedConfigIsSymlink: false,
- expectedDiagnostic: "managed MCP projection changed while reading",
- managedOptions: { removeAfterManagedOpen: true, timeoutMs: 30_000 },
- },
])(
"propagates $caseName through status with credential observation $credentialObservation (#10754)",
{ timeout: 120_000 },
({
credentialObservation,
- expectedConfigExists,
+ diagnostic,
expectedConfigIsFifo,
expectedConfigIsSymlink,
- expectedDiagnostic,
managedOptions,
}) => {
const home = createTempHome("nemoclaw-mcp-status-projection-");
@@ -288,7 +275,6 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stdout: logLines.join("\n"),
stderr: errorLines.join("\n"),
fixtureStatus: lastDeepAgentsFixtureResult?.status ?? null,
- configExists: lastDeepAgentsFixtureResult?.configExists ?? false,
configIsFifo: lastDeepAgentsFixtureResult?.configIsFifo ?? false,
configIsSymlink: lastDeepAgentsFixtureResult?.configIsSymlink ?? false,
}));
@@ -299,7 +285,6 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
stdout: string;
stderr: string;
fixtureStatus: number | null;
- configExists: boolean;
configIsFifo: boolean;
configIsSymlink: boolean;
};
@@ -307,8 +292,9 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
expect(outcome.fixtureStatus).toBe(2);
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
- expect(outcome.stderr).toContain(expectedDiagnostic);
- expect(outcome.configExists).toBe(expectedConfigExists);
+ expect(outcome.stderr).toContain(
+ `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ );
expect(outcome.configIsFifo).toBe(expectedConfigIsFifo);
expect(outcome.configIsSymlink).toBe(expectedConfigIsSymlink);
},
diff --git a/src/lib/actions/sandbox/mcp-bridge-status.ts b/src/lib/actions/sandbox/mcp-bridge-status.ts
index f62cf23f3f4..7d04efa0f55 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status.ts
@@ -8,7 +8,6 @@ import {
buildHermesMcpStatusCommand,
buildOpenClawMcporterInspectCommand,
DEFAULT_OPENCLAW_CONFIG_DIR,
- DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL,
DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX,
openClawMcporterRoot,
} from "./mcp-bridge-adapters";
@@ -140,8 +139,7 @@ function getAdapterRegistration(
const normalizedDetail = detail.trim();
if (
adapter === "deepagents-config" &&
- (normalizedDetail.startsWith(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX) ||
- normalizedDetail.endsWith(`: ${DEEPAGENTS_CHANGED_MCP_PROJECTION_DETAIL}`))
+ normalizedDetail.startsWith(DEEPAGENTS_UNSAFE_MCP_PROJECTION_PREFIX)
) {
throw new McpBridgeError(normalizedDetail, 2);
}
diff --git a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
index e4cfcc20c9b..f7c46dbcfde 100644
--- a/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
+++ b/test/helpers/mcp-bridge-adapter-deepagents-fixture.ts
@@ -45,7 +45,6 @@ export interface DeepAgentsManagedFixtureOptions {
directory?: boolean;
fifo?: boolean;
mode?: number;
- removeAfterManagedOpen?: boolean;
swapAfterManagedEloop?: boolean;
swapAfterManagedOpen?: "fifo" | "symlink";
swapAfterMissingManagedOpen?: "fifo" | "symlink";
@@ -60,7 +59,7 @@ export interface DeepAgentsManagedFixtureOptions {
type ManagedSwap = {
content?: string;
- kind: "absent" | "fifo" | "regular" | "socket" | "symlink";
+ kind: "fifo" | "regular" | "socket" | "symlink";
phase:
| "after-missing-projection-open"
| "after-projection-eloop"
@@ -78,9 +77,6 @@ function resolveManagedSwap(options: DeepAgentsManagedFixtureOptions): ManagedSw
if (options.swapAfterManagedOpen) {
return { kind: options.swapAfterManagedOpen, phase: "after-projection-open" };
}
- if (options.removeAfterManagedOpen) {
- return { kind: "absent", phase: "after-projection-open" };
- }
if (options.swapAfterMissingManagedOpen) {
return {
kind: options.swapAfterMissingManagedOpen,
@@ -202,8 +198,6 @@ function createDeepAgentsFixturePythonExecutable(
" os.unlink(managed_path)",
" except FileNotFoundError:",
" pass",
- " if managed_swap_kind == 'absent':",
- " return",
" if managed_swap_kind == 'symlink':",
" os.symlink(managed_target, managed_path)",
" elif managed_swap_kind == 'fifo':",
From bd3e42ea3b757a40e35a4b70e5f87fe56562beb3 Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 10:57:49 -0700
Subject: [PATCH 48/49] test(mcp): deduplicate unsafe status coverage
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
.../mcp-bridge-status-resolution.test.ts | 50 ++++---------------
1 file changed, 11 insertions(+), 39 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
index 86c29cd8483..d4f0a46c89f 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -212,41 +212,10 @@ ${body}
}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
- it.each([
- {
- caseName: "a symlink appearing after a missing-path open",
- credentialObservation: null,
- diagnostic: "symbolic link",
- expectedConfigIsFifo: false,
- expectedConfigIsSymlink: true,
- managedOptions: { swapAfterMissingManagedOpen: "symlink", timeoutMs: 30_000 },
- },
- {
- caseName: "a FIFO appearing after a missing-path open",
- credentialObservation: "absent",
- diagnostic: "FIFO",
- expectedConfigIsFifo: true,
- expectedConfigIsSymlink: false,
- managedOptions: { swapAfterMissingManagedOpen: "fifo", timeoutMs: 30_000 },
- },
- {
- caseName: "a symlink restored after ELOOP",
- credentialObservation: "canonical",
- diagnostic: "symbolic link",
- expectedConfigIsFifo: false,
- expectedConfigIsSymlink: true,
- managedOptions: { swapAfterManagedEloop: true, symlink: true, timeoutMs: 30_000 },
- },
- ])(
- "propagates $caseName through status with credential observation $credentialObservation (#10754)",
+ it(
+ "propagates an unsafe projection through status when credential observation is unavailable (#10754)",
{ timeout: 120_000 },
- ({
- credentialObservation,
- diagnostic,
- expectedConfigIsFifo,
- expectedConfigIsSymlink,
- managedOptions,
- }) => {
+ () => {
const home = createTempHome("nemoclaw-mcp-status-projection-");
const { stdout } = runHarness(
home,
@@ -265,8 +234,11 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
managedServerNames: ["github"],
},
});
- providerCredentialObservation = ${JSON.stringify(credentialObservation)};
- deepAgentsManagedOptions = ${JSON.stringify(managedOptions)};
+ providerCredentialObservation = null;
+ deepAgentsManagedOptions = ${JSON.stringify({
+ swapAfterMissingManagedOpen: "symlink",
+ timeoutMs: 30_000,
+ })};
await bridge.dispatchMcpBridgeCommand("alpha", ["status", "github", "--no-probe", "--json"]);
const exitCode = process.exitCode ?? 0;
process.exitCode = 0;
@@ -293,10 +265,10 @@ describe("MCP status wire-level credential-resolution probe", { timeout: 15_000
expect(outcome.exitCode).toBe(2);
expect(outcome.stdout).toBe("");
expect(outcome.stderr).toContain(
- `Unsafe managed Deep Agents MCP projection path: ${diagnostic}`,
+ "Unsafe managed Deep Agents MCP projection path: symbolic link",
);
- expect(outcome.configIsFifo).toBe(expectedConfigIsFifo);
- expect(outcome.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(outcome.configIsFifo).toBe(false);
+ expect(outcome.configIsSymlink).toBe(true);
},
);
From c4a0d7c85183d3e7e0bdb9c5e6bf8af8fde7118c Mon Sep 17 00:00:00 2001
From: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
Date: Wed, 2 Sep 2026 14:37:22 -0700
Subject: [PATCH 49/49] refactor(mcp): centralize unsafe projection rejection
Signed-off-by: Rebecca Sliter <571084+rsliter@users.noreply.github.com>
---
...cp-bridge-adapter-deepagents-projection.ts | 23 +++++++------------
1 file changed, 8 insertions(+), 15 deletions(-)
diff --git a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
index d484ec70a7c..f45722618ba 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -28,6 +28,10 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" if not stat.S_ISREG(mode):",
" return 'non-regular file'",
" return None",
+ "def reject_unsafe_managed_projection(metadata, unsafe_path_prefix):",
+ " unsafe_path_kind = managed_projection_path_kind(metadata.st_mode) if unsafe_path_prefix else None",
+ " if unsafe_path_kind:",
+ " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
"def managed_fingerprint(metadata):",
" return (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_mode, metadata.st_nlink, metadata.st_uid)",
"def assert_managed_source_stable(path, identity, unsafe_path_prefix=None):",
@@ -38,9 +42,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" current = managed_fingerprint(current_metadata) if current_metadata is not None else None",
" if identity is None:",
" if current is not None:",
- " unsafe_path_kind = managed_projection_path_kind(current_metadata.st_mode) if unsafe_path_prefix else None",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " reject_unsafe_managed_projection(current_metadata, unsafe_path_prefix)",
" raise ValueError('managed MCP projection appeared during mutation')",
" return",
" if current != identity:",
@@ -48,10 +50,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
"def validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix=None):",
" opened = os.fstat(descriptor)",
" linked = os.stat(path, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " reject_unsafe_managed_projection(linked, unsafe_path_prefix)",
" safe = (stat.S_ISREG(opened.st_mode) and opened.st_uid == os.getuid() and stat.S_IMODE(opened.st_mode) == 0o600 and opened.st_nlink == 1 and (opened.st_dev, opened.st_ino) == (linked.st_dev, linked.st_ino))",
" if not safe:",
" raise ValueError('managed MCP projection has unsafe ownership, mode, type, links, or path identity')",
@@ -79,10 +78,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" raise",
" try:",
" before = os.fstat(descriptor)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(before.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " reject_unsafe_managed_projection(before, unsafe_path_prefix)",
" validate_managed_descriptor_path(path, descriptor, unsafe_path_prefix)",
" if before.st_size < 0 or before.st_size > MANAGED_MCP_MAX_BYTES:",
" raise ValueError('managed MCP projection has invalid size')",
@@ -96,10 +92,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" remaining -= len(chunk)",
" after = os.fstat(descriptor)",
" linked_after = os.stat(path, follow_symlinks=False)",
- " if unsafe_path_prefix:",
- " unsafe_path_kind = managed_projection_path_kind(linked_after.st_mode)",
- " if unsafe_path_kind:",
- " raise ValueError(f'{unsafe_path_prefix}: {unsafe_path_kind}')",
+ " reject_unsafe_managed_projection(linked_after, unsafe_path_prefix)",
" stable = (managed_fingerprint(before) == managed_fingerprint(after) and managed_fingerprint(after) == managed_fingerprint(linked_after))",
" if remaining or not stable:",
" raise ValueError('managed MCP projection changed while reading')",