diff --git a/docs/manage-sandboxes/manage-mcp-servers.mdx b/docs/manage-sandboxes/manage-mcp-servers.mdx
index 9ea4ab8c406..c018783c33c 100644
--- a/docs/manage-sandboxes/manage-mcp-servers.mdx
+++ b/docs/manage-sandboxes/manage-mcp-servers.mdx
@@ -37,6 +37,17 @@ 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.
+
+
+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 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.
+
+
+
## 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..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
@@ -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 },
@@ -74,40 +46,306 @@ describe("Deep Agents managed MCP projection safety", () => {
);
});
- it("fails status inspection closed without following hostile projection paths", () => {
- 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,
+ 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 (#10754)",
+ ({
+ diagnostic,
+ expectedConfigIsDirectory,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ initialConfig,
+ options,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ initialConfig,
+ "v2",
+ undefined,
+ 0o600,
+ { ...options, 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.configIsDirectory).toBe(expectedConfigIsDirectory);
+ expect(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ 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",
+ 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)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
emptyProjection,
"v2",
undefined,
0o600,
- { symlink: true },
+ { swapOnManagedRead: "symlink", timeoutMs: managedCommandTimeoutMs },
);
- expect(symlink.status).toBe(2);
- expect(symlink.stdout.trim()).toBe("");
- expect(symlink.stderr).toContain("Could not inspect managed Deep Agents MCP state");
- expect(symlink.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: symbolic link",
+ );
+ expect(result.configIsSymlink).toBe(true);
+ expect(result.managedSymlinkTargetText).toBe(`${JSON.stringify(emptyProjection, null, 2)}\n`);
+ });
- const fifo = runDeepAgentsConfigCommand(statusCommand, undefined, "v2", undefined, 0o600, {
- fifo: true,
- });
- expect(fifo.status).toBe(2);
- expect(fifo.stdout.trim()).toBe("");
- expect(fifo.stderr).toContain("Could not inspect managed Deep Agents MCP state");
+ 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,
+ },
+ );
+ 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,
+ 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,
+ expectedConfigIsFifo,
+ expectedConfigIsSymlink,
+ expectedTargetText,
+ }) => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ emptyProjection,
+ "v2",
+ undefined,
+ 0o600,
+ { 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(result.configIsFifo).toBe(expectedConfigIsFifo);
+ expect(result.configIsSymlink).toBe(expectedConfigIsSymlink);
+ expect(result.managedSymlinkTargetText).toBe(expectedTargetText);
+ },
+ );
+
+ it("keeps the generic diagnostic for ordinary projection inspection failures (#10754)", () => {
+ const result = runDeepAgentsConfigCommand(
+ buildDeepAgentsMcpStatusCommand(baseEntry),
+ '{"mcpServers":',
+ );
+
+ 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("Expecting value");
+ expect(result.stderr).not.toContain("Traceback");
+ expect(result.configText).toBe('{"mcpServers":');
});
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,
@@ -119,7 +357,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,
});
@@ -127,38 +370,52 @@ 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", () => {
- 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)`,
+ it("preserves a projection that appears during absent publication", () => {
+ 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)`,
+ it("preserves a replacement during a descriptor rewrite", () => {
+ 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", () => {
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("preserves a projection symlink during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedSymlink = runDeepAgentsConfigCommand(
forcedCommand,
emptyProjection,
@@ -169,11 +426,15 @@ 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`,
);
+ });
+ it("preserves an unsafe projection mode during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedUnsafeMode = runDeepAgentsConfigCommand(
forcedCommand,
emptyProjection,
@@ -183,8 +444,12 @@ describe("Deep Agents managed MCP projection safety", () => {
{ mode: 0o644 },
);
expect(forcedUnsafeMode.status).toBe(2);
+ expect(forcedUnsafeMode.configMode).toBe(0o644);
expect(forcedUnsafeMode.config).toEqual(emptyProjection);
+ });
+ it("preserves a projection FIFO during forced removal", () => {
+ const forcedCommand = buildDeepAgentsMcpRemoveCommand(baseEntry, true);
const forcedFifo = runDeepAgentsConfigCommand(
forcedCommand,
undefined,
@@ -195,11 +460,17 @@ 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);
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..f45722618ba 100644
--- a/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-projection.ts
@@ -18,40 +18,68 @@ 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 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 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:",
+ " reject_unsafe_managed_projection(current_metadata, unsafe_path_prefix)",
" raise ValueError('managed MCP projection appeared during mutation')",
" 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)",
+ " 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')",
" 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:",
" 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:",
+ " 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)",
- " validate_managed_descriptor_path(path, descriptor)",
+ " 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')",
" chunks = []",
@@ -64,6 +92,7 @@ export const DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS = [
" remaining -= len(chunk)",
" after = os.fstat(descriptor)",
" linked_after = os.stat(path, follow_symlinks=False)",
+ " 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')",
@@ -87,8 +116,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 03518cab344..e46d13bbcde 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";
@@ -259,10 +261,16 @@ export function buildDeepAgentsMcpStatusCommand(
"is_v2 = runtime_kind == 'v2'",
"config_path = managed_path if is_v2 else legacy_path",
"try:",
- " 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/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 27980f526bd..d4f0a46c89f 100644
--- a/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
+++ b/src/lib/actions/sandbox/mcp-bridge-status-resolution.test.ts
@@ -37,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,
@@ -87,18 +88,33 @@ 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 = () => {
credentialObservationCount += 1;
- return {
- status: 0,
- stdout: providerCredentialObservation,
- stderr: "",
- };
+ return providerCredentialObservation === null
+ ? undefined
+ : {
+ status: 0,
+ stdout: providerCredentialObservation,
+ stderr: "",
+ };
};
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");
@@ -196,6 +212,66 @@ ${body}
}
describe("MCP status wire-level credential-resolution probe", { timeout: 15_000 }, () => {
+ it(
+ "propagates an unsafe projection through status when credential observation is unavailable (#10754)",
+ { timeout: 120_000 },
+ () => {
+ 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"],
+ },
+ });
+ 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;
+ 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 outcome = JSON.parse(stdout) as {
+ exitCode: number;
+ stdout: string;
+ stderr: string;
+ fixtureStatus: number | null;
+ configIsFifo: boolean;
+ configIsSymlink: boolean;
+ };
+
+ 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: symbolic link",
+ );
+ expect(outcome.configIsFifo).toBe(false);
+ expect(outcome.configIsSymlink).toBe(true);
+ },
+ );
+
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..7d04efa0f55 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";
@@ -96,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 }
@@ -119,20 +122,31 @@ 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" };
}
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);
+ }
+ if (credentialInspectionSkip) return credentialInspectionSkip;
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 b6a4a5d62c1..f7c46dbcfde 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";
@@ -25,6 +25,11 @@ export interface DeepAgentsConfigCommandResult {
stdout: string;
stderr: string;
configExists: boolean;
+ configIsDirectory: boolean;
+ configIsFifo: boolean;
+ configIsSocket: boolean;
+ configIsSymlink: boolean;
+ configMode: number | null;
config: Record | null;
configText: string | null;
legacyConfigExists: boolean;
@@ -32,12 +37,260 @@ export interface DeepAgentsConfigCommandResult {
legacyConfigText: string | null;
managedSymlinkTargetExists: boolean;
managedSymlinkTargetText: string | null;
+ managedTargetReadAccessed: boolean;
}
export interface DeepAgentsManagedFixtureOptions {
+ danglingSymlink?: boolean;
+ directory?: boolean;
fifo?: boolean;
mode?: number;
+ 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 });
+ 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,
+ 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 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 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)",
+ "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 });
+ fs.writeFileSync(
+ executablePath,
+ '#!/bin/sh\nexec python3 -I "$DEEPAGENTS_FIXTURE_PYTHON_WRAPPER" "$@"\n',
+ { mode: 0o700 },
+ );
+ return { executablePath, scriptPath };
}
export function runDeepAgentsConfigCommand(
@@ -49,9 +302,13 @@ export function runDeepAgentsConfigCommand(
managedOptions: DeepAgentsManagedFixtureOptions = {},
runtimeEnvironment: NodeJS.ProcessEnv = {},
): DeepAgentsConfigCommandResult {
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "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 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,
@@ -66,66 +323,151 @@ export function runDeepAgentsConfigCommand(
{ mode },
);
};
- const managedInitialPath = managedOptions.symlink ? 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);
+ const managedSymlink = managedOptions.symlink === true || managedOptions.danglingSymlink === true;
+ const managedTargetReadPath = managedOptions.targetReadProbe
+ ? managedSymlink
+ ? managedSymlinkTarget
+ : null
+ : null;
+ if (managedOptions.targetReadProbe && managedTargetReadPath === null) {
+ throw new Error("targetReadProbe requires a projection symlink");
+ }
+ const managedInitialPath = managedSymlink ? managedSymlinkTarget : configPath;
+ if (managedOptions.directory) {
+ fs.mkdirSync(configPath, { recursive: true, mode: managedOptions.mode ?? 0o700 });
+ } else if (managedOptions.fifo) {
+ createFifo(configPath, managedOptions.mode);
} else {
- initializeConfig(managedInitialPath, initialConfig, managedOptions.mode);
- if (initialConfig !== undefined) fs.chmodSync(managedInitialPath, managedOptions.mode ?? 0o600);
- if (managedOptions.symlink && initialConfig !== undefined) {
+ if (managedTargetReadPath === managedSymlinkTarget) {
+ createFifo(managedSymlinkTarget, managedOptions.mode);
+ } 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-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") {
+ initializeConfig(managedSymlinkTarget, initialConfig, managedOptions.mode);
+ }
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,
+ legacyConfigPath,
+ managedSymlinkTarget,
+ runtimeKind,
+ managedSwap,
+ );
const fixtureCommand = command
.replaceAll(DEEPAGENTS_MCP_CONFIG_PATH, configPath)
.replaceAll("/sandbox/.deepagents/.mcp.json", legacyConfigPath)
- .replaceAll("/opt/venv/bin/python3", "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 canonicalEnvironment = Object.fromEntries(
[...command.matchAll(/openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]*)/gu)].map(
([, name]) => [name!, `openshell:resolve:env:${name!}`],
),
);
- 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, ...canonicalEnvironment, ...runtimeEnvironment },
- timeout: 5000,
+ env: {
+ ...process.env,
+ ...canonicalEnvironment,
+ ...runtimeEnvironment,
+ DEEPAGENTS_FIXTURE_PYTHON_WRAPPER: fixturePython.scriptPath,
+ },
+ timeout: managedOptions.timeoutMs ?? 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 configText = configExists && !configIsFifo ? fs.readFileSync(configPath, "utf-8") : null;
+ const configIsFifo = configStat?.isFIFO() === true;
+ 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")
+ : null;
const managedSymlinkTargetExists = fs.existsSync(managedSymlinkTarget);
- const managedSymlinkTargetText = managedSymlinkTargetExists
- ? fs.readFileSync(managedSymlinkTarget, "utf-8")
- : null;
+ const readRegularFile = (target: string): string | null => {
+ let descriptor: number | null = null;
+ try {
+ 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 =
+ managedTargetReadPath === managedSymlinkTarget ? null : readRegularFile(managedSymlinkTarget);
+ 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;
+ try {
+ const parsed: unknown = JSON.parse(text);
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : null;
+ } catch {
+ return null;
+ }
+ };
return {
status: result.status,
stdout: result.stdout,
stderr: result.stderr,
configExists,
- config: configText ? (JSON.parse(configText) as Record) : null,
+ configIsDirectory,
+ configIsFifo,
+ configIsSocket,
+ configIsSymlink,
+ configMode,
+ config: parseConfigText(configText),
configText,
legacyConfigExists,
- legacyConfig: legacyConfigText
- ? (JSON.parse(legacyConfigText) as Record)
- : null,
+ legacyConfig: parseConfigText(legacyConfigText),
legacyConfigText,
managedSymlinkTargetExists,
managedSymlinkTargetText,
+ managedTargetReadAccessed,
};
} finally {
+ targetReadProbe?.kill("SIGTERM");
fs.rmSync(tmp, { recursive: true, force: true });
}
}