diff --git a/agents/hermes/mcp-config-transaction.py b/agents/hermes/mcp-config-transaction.py index 6ef646c506a..c8cb462113b 100755 --- a/agents/hermes/mcp-config-transaction.py +++ b/agents/hermes/mcp-config-transaction.py @@ -95,7 +95,7 @@ MAX_GATEWAY_PID_RECORD_BYTES = 4096 MCP_RACE_RECOVERY_ATTEMPTS = 3 MAX_GATEWAY_PUBLIC_PORT_RECORD_BYTES = 16 -MAX_SERVICE_MANAGER_ENVIRONMENT_BYTES = 64 * 1024 +MAX_GATEWAY_ENVIRONMENT_BYTES = 64 * 1024 GATEWAY_INTERNAL_PORT = 18642 @@ -1040,36 +1040,24 @@ def _gateway_identity() -> tuple[int, object] | None: return numeric_pid, start_time -def _read_service_manager_environment(pid: int) -> bytes: +def _read_gateway_environment(pid: int) -> bytes: try: with open(f"/proc/{pid}/environ", "rb") as environment_file: - raw = environment_file.read(MAX_SERVICE_MANAGER_ENVIRONMENT_BYTES + 1) - except FileNotFoundError as error: - raise PermissionError( - "Hermes service-manager environment is unavailable" - ) from error - if len(raw) > MAX_SERVICE_MANAGER_ENVIRONMENT_BYTES: - raise PermissionError("Hermes service-manager environment is too large") + raw = environment_file.read(MAX_GATEWAY_ENVIRONMENT_BYTES + 1) + except OSError as error: + raise PermissionError("Hermes gateway environment is unavailable") from error + if len(raw) > MAX_GATEWAY_ENVIRONMENT_BYTES: + raise PermissionError("Hermes gateway environment is too large") return raw -def _service_manager_gateway_public_port( +def _gateway_environment_public_port( identity: tuple[int, object], ) -> int: gateway_pid = identity[0] - manager_pid = _process_parent_pid(gateway_pid) - if manager_pid is None or not _is_service_manager_process(manager_pid): - raise PermissionError( - "Hermes gateway is not running under the managed service lifecycle" - ) - - environment = _read_service_manager_environment(manager_pid) - if ( - _gateway_identity() != identity - or _process_parent_pid(gateway_pid) != manager_pid - or not _is_service_manager_process(manager_pid) - ): - raise PermissionError("Hermes service-manager identity changed while reading") + environment = _read_gateway_environment(gateway_pid) + if _gateway_identity() != identity: + raise PermissionError("Hermes gateway identity changed while reading") prefix = b"NEMOCLAW_HERMES_API_PORT=" values = [ @@ -1078,15 +1066,13 @@ def _service_manager_gateway_public_port( if entry.startswith(prefix) ] if len(values) > 1: - raise PermissionError("Hermes service-manager API port is ambiguous") + raise PermissionError("Hermes gateway API port is ambiguous") if not values or not values[0]: return 8642 try: decoded = values[0].decode("ascii") except UnicodeDecodeError as error: - raise PermissionError( - "Hermes service-manager API port is malformed" - ) from error + raise PermissionError("Hermes gateway API port is malformed") from error return _parse_gateway_public_port(decoded) @@ -1099,7 +1085,7 @@ def _resolve_gateway_public_port() -> int: identity = _gateway_identity() if identity is None: raise PermissionError("Hermes gateway identity is unavailable") - return _service_manager_gateway_public_port(identity) + return _gateway_environment_public_port(identity) def _configure_gateway_public_port() -> None: diff --git a/src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.101.json b/src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.101.json index 1576fc8c269..396afa36fda 100644 --- a/src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.101.json +++ b/src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.101.json @@ -41,7 +41,7 @@ }, { "path": "agents/hermes/mcp-config-transaction.py", - "sha256": "01096bf959d07493ada4525ae824f3fabcde02fa6c02b8b56c16f227489dae0a" + "sha256": "e3e51798c242b7ed54c1dff8203d3e73dbc2b9fcb8c7d271292f6b41f08bdd90" } ] }, diff --git a/test/hermes-mcp-api-port.test.ts b/test/hermes-mcp-api-port.test.ts index ffe8bd87917..8fa7bc6a830 100644 --- a/test/hermes-mcp-api-port.test.ts +++ b/test/hermes-mcp-api-port.test.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 path from "node:path"; import { describe, expect, it } from "vitest"; @@ -12,7 +12,43 @@ const TRANSACTION = path.resolve( ); describe("Hermes MCP API port resolution", () => { - it("accepts only allocated ports from the stable service-manager environment (#8543)", () => { + it("reads the port from a same-identity gateway process environment (#9044)", () => { + const gateway = spawn(process.execPath, ["-e", "setTimeout(() => {}, 10000)"], { + env: { NEMOCLAW_HERMES_API_PORT: "8645", PATH: process.env.PATH }, + stdio: "ignore", + }); + + try { + expect(gateway.pid).toBeTypeOf("number"); + const result = spawnSync( + "python3", + [ + "-c", + ` +import importlib.util, json, sys, types +sys.modules["yaml"] = types.SimpleNamespace(YAMLError=type("YAMLError", (Exception,), {})) +spec = importlib.util.spec_from_file_location("mcp_tx", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) +identity = (int(sys.argv[2]), 333) +module._gateway_identity = lambda: identity +print(json.dumps({"port": module._gateway_environment_public_port(identity)})) +`, + TRANSACTION, + String(gateway.pid), + ], + { encoding: "utf8" }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ port: 8645 }); + } finally { + gateway.kill("SIGKILL"); + } + }); + + it("reads allocated ports from the identity-bound gateway environment (#9044)", () => { const result = spawnSync( "python3", [ @@ -26,17 +62,15 @@ sys.modules[spec.name] = module spec.loader.exec_module(module) identity = (41, 333) module._gateway_identity = lambda: identity -module._process_parent_pid = lambda pid: 40 -module._is_service_manager_process = lambda pid: True +opened = [] accepted = [] for raw in (b"8642", b"8645", b"8652"): - module._read_service_manager_environment = ( - lambda pid, value=raw: b"PATH=/usr/bin\\0NEMOCLAW_HERMES_API_PORT=" - + value - + b"\\0" + module._read_gateway_environment = ( + lambda pid, value=raw: opened.append(pid) + or b"PATH=/usr/bin\\0NEMOCLAW_HERMES_API_PORT=" + value + b"\\0" ) - accepted.append(module._service_manager_gateway_public_port(identity)) + accepted.append(module._gateway_environment_public_port(identity)) rejected = [] for raw in ( @@ -45,24 +79,25 @@ for raw in ( "²".encode("utf-8"), b"8645\\0NEMOCLAW_HERMES_API_PORT=8646", ): - module._read_service_manager_environment = ( - lambda pid, value=raw: b"NEMOCLAW_HERMES_API_PORT=" + value + b"\\0" + module._read_gateway_environment = ( + lambda pid, value=raw: opened.append(pid) + or b"NEMOCLAW_HERMES_API_PORT=" + value + b"\\0" ) try: - module._service_manager_gateway_public_port(identity) + module._gateway_environment_public_port(identity) except PermissionError as error: rejected.append(str(error)) -module._read_service_manager_environment = lambda pid: b"PATH=/usr/bin\\0" -absent = module._service_manager_gateway_public_port(identity) +module._read_gateway_environment = lambda pid: opened.append(pid) or b"PATH=/usr/bin\\0" +absent = module._gateway_environment_public_port(identity) module._gateway_identity = lambda: (41, 999) -module._read_service_manager_environment = ( - lambda pid: b"NEMOCLAW_HERMES_API_PORT=8645\\0" +module._read_gateway_environment = ( + lambda pid: opened.append(pid) or b"NEMOCLAW_HERMES_API_PORT=8645\\0" ) identity_change = "" try: - module._service_manager_gateway_public_port(identity) + module._gateway_environment_public_port(identity) except PermissionError as error: identity_change = str(error) @@ -71,6 +106,7 @@ print(json.dumps({ "rejected": rejected, "absent": absent, "identity_change": identity_change, + "opened": opened, })) `, TRANSACTION, @@ -84,11 +120,54 @@ print(json.dumps({ rejected: [ "Hermes API port is outside the allocated range", "Hermes API port is outside the allocated range", - "Hermes service-manager API port is malformed", - "Hermes service-manager API port is ambiguous", + "Hermes gateway API port is malformed", + "Hermes gateway API port is ambiguous", ], absent: 8642, - identity_change: "Hermes service-manager identity changed while reading", + identity_change: "Hermes gateway identity changed while reading", + opened: Array(9).fill(41), + }); + }); + + it("rejects an unavailable gateway environment without using another identity (#9044)", () => { + const result = spawnSync( + "python3", + [ + "-c", + ` +import builtins, importlib.util, json, sys, types +sys.modules["yaml"] = types.SimpleNamespace(YAMLError=type("YAMLError", (Exception,), {})) +spec = importlib.util.spec_from_file_location("mcp_tx", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) + +opened = [] +real_open = builtins.open +def denied(path, *args, **kwargs): + opened.append(path) + raise PermissionError("denied") + +builtins.open = denied +message = "" +try: + module._read_gateway_environment(41) +except PermissionError as error: + message = str(error) +finally: + builtins.open = real_open + +print(json.dumps({"message": message, "opened": opened})) +`, + TRANSACTION, + ], + { encoding: "utf8" }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ + message: "Hermes gateway environment is unavailable", + opened: ["/proc/41/environ"], }); }); @@ -227,7 +306,7 @@ print(json.dumps({ }); }); - it("prefers the marker over the service-manager environment (#8543)", () => { + it("prefers the root marker over the gateway environment (#8543)", () => { const result = spawnSync( "python3", [ @@ -241,7 +320,7 @@ sys.modules[spec.name] = module spec.loader.exec_module(module) module._gateway_identity = lambda: (41, 333) -module._service_manager_gateway_public_port = lambda identity: 8649 +module._gateway_environment_public_port = lambda identity: 8649 module._root_gateway_public_port_marker = lambda: 8647 marker_wins = module._resolve_gateway_public_port() diff --git a/test/openshell-0.0.101-migration-review.test.ts b/test/openshell-0.0.101-migration-review.test.ts index 9fb1e890c1f..265272b12d9 100644 --- a/test/openshell-0.0.101-migration-review.test.ts +++ b/test/openshell-0.0.101-migration-review.test.ts @@ -230,7 +230,7 @@ describe("OpenShell 0.0.101 migration review", () => { }, { path: "agents/hermes/mcp-config-transaction.py", - sha256: "01096bf959d07493ada4525ae824f3fabcde02fa6c02b8b56c16f227489dae0a", + sha256: "e3e51798c242b7ed54c1dff8203d3e73dbc2b9fcb8c7d271292f6b41f08bdd90", }, ], });