11// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22// SPDX-License-Identifier: Apache-2.0
33
4- import { spawnSync } from "node:child_process" ;
4+ import { spawn , spawnSync } from "node:child_process" ;
55import path from "node:path" ;
66import { describe , expect , it } from "vitest" ;
77
@@ -12,7 +12,43 @@ const TRANSACTION = path.resolve(
1212) ;
1313
1414describe ( "Hermes MCP API port resolution" , ( ) => {
15- it ( "accepts only allocated ports from the stable service-manager environment (#8543)" , ( ) => {
15+ it ( "reads the port from a same-identity gateway process environment (#9044)" , ( ) => {
16+ const gateway = spawn ( process . execPath , [ "-e" , "setTimeout(() => {}, 10000)" ] , {
17+ env : { NEMOCLAW_HERMES_API_PORT : "8645" , PATH : process . env . PATH } ,
18+ stdio : "ignore" ,
19+ } ) ;
20+
21+ try {
22+ expect ( gateway . pid ) . toBeTypeOf ( "number" ) ;
23+ const result = spawnSync (
24+ "python3" ,
25+ [
26+ "-c" ,
27+ `
28+ import importlib.util, json, sys, types
29+ sys.modules["yaml"] = types.SimpleNamespace(YAMLError=type("YAMLError", (Exception,), {}))
30+ spec = importlib.util.spec_from_file_location("mcp_tx", sys.argv[1])
31+ module = importlib.util.module_from_spec(spec)
32+ sys.modules[spec.name] = module
33+ spec.loader.exec_module(module)
34+ identity = (int(sys.argv[2]), 333)
35+ module._gateway_identity = lambda: identity
36+ print(json.dumps({"port": module._gateway_environment_public_port(identity)}))
37+ ` ,
38+ TRANSACTION ,
39+ String ( gateway . pid ) ,
40+ ] ,
41+ { encoding : "utf8" } ,
42+ ) ;
43+
44+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
45+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( { port : 8645 } ) ;
46+ } finally {
47+ gateway . kill ( "SIGKILL" ) ;
48+ }
49+ } ) ;
50+
51+ it ( "reads allocated ports from the identity-bound gateway environment (#9044)" , ( ) => {
1652 const result = spawnSync (
1753 "python3" ,
1854 [
@@ -26,17 +62,15 @@ sys.modules[spec.name] = module
2662spec.loader.exec_module(module)
2763identity = (41, 333)
2864module._gateway_identity = lambda: identity
29- module._process_parent_pid = lambda pid: 40
30- module._is_service_manager_process = lambda pid: True
65+ opened = []
3166
3267accepted = []
3368for raw in (b"8642", b"8645", b"8652"):
34- module._read_service_manager_environment = (
35- lambda pid, value=raw: b"PATH=/usr/bin\\0NEMOCLAW_HERMES_API_PORT="
36- + value
37- + b"\\0"
69+ module._read_gateway_environment = (
70+ lambda pid, value=raw: opened.append(pid)
71+ or b"PATH=/usr/bin\\0NEMOCLAW_HERMES_API_PORT=" + value + b"\\0"
3872 )
39- accepted.append(module._service_manager_gateway_public_port (identity))
73+ accepted.append(module._gateway_environment_public_port (identity))
4074
4175rejected = []
4276for raw in (
@@ -45,24 +79,25 @@ for raw in (
4579 "²".encode("utf-8"),
4680 b"8645\\0NEMOCLAW_HERMES_API_PORT=8646",
4781):
48- module._read_service_manager_environment = (
49- lambda pid, value=raw: b"NEMOCLAW_HERMES_API_PORT=" + value + b"\\0"
82+ module._read_gateway_environment = (
83+ lambda pid, value=raw: opened.append(pid)
84+ or b"NEMOCLAW_HERMES_API_PORT=" + value + b"\\0"
5085 )
5186 try:
52- module._service_manager_gateway_public_port (identity)
87+ module._gateway_environment_public_port (identity)
5388 except PermissionError as error:
5489 rejected.append(str(error))
5590
56- module._read_service_manager_environment = lambda pid: b"PATH=/usr/bin\\0"
57- absent = module._service_manager_gateway_public_port (identity)
91+ module._read_gateway_environment = lambda pid: opened.append(pid) or b"PATH=/usr/bin\\0"
92+ absent = module._gateway_environment_public_port (identity)
5893
5994module._gateway_identity = lambda: (41, 999)
60- module._read_service_manager_environment = (
61- lambda pid: b"NEMOCLAW_HERMES_API_PORT=8645\\0"
95+ module._read_gateway_environment = (
96+ lambda pid: opened.append(pid) or b"NEMOCLAW_HERMES_API_PORT=8645\\0"
6297)
6398identity_change = ""
6499try:
65- module._service_manager_gateway_public_port (identity)
100+ module._gateway_environment_public_port (identity)
66101except PermissionError as error:
67102 identity_change = str(error)
68103
@@ -71,6 +106,7 @@ print(json.dumps({
71106 "rejected": rejected,
72107 "absent": absent,
73108 "identity_change": identity_change,
109+ "opened": opened,
74110}))
75111` ,
76112 TRANSACTION ,
@@ -84,11 +120,54 @@ print(json.dumps({
84120 rejected : [
85121 "Hermes API port is outside the allocated range" ,
86122 "Hermes API port is outside the allocated range" ,
87- "Hermes service-manager API port is malformed" ,
88- "Hermes service-manager API port is ambiguous" ,
123+ "Hermes gateway API port is malformed" ,
124+ "Hermes gateway API port is ambiguous" ,
89125 ] ,
90126 absent : 8642 ,
91- identity_change : "Hermes service-manager identity changed while reading" ,
127+ identity_change : "Hermes gateway identity changed while reading" ,
128+ opened : Array ( 9 ) . fill ( 41 ) ,
129+ } ) ;
130+ } ) ;
131+
132+ it ( "rejects an unavailable gateway environment without using another identity (#9044)" , ( ) => {
133+ const result = spawnSync (
134+ "python3" ,
135+ [
136+ "-c" ,
137+ `
138+ import builtins, importlib.util, json, sys, types
139+ sys.modules["yaml"] = types.SimpleNamespace(YAMLError=type("YAMLError", (Exception,), {}))
140+ spec = importlib.util.spec_from_file_location("mcp_tx", sys.argv[1])
141+ module = importlib.util.module_from_spec(spec)
142+ sys.modules[spec.name] = module
143+ spec.loader.exec_module(module)
144+
145+ opened = []
146+ real_open = builtins.open
147+ def denied(path, *args, **kwargs):
148+ opened.append(path)
149+ raise PermissionError("denied")
150+
151+ builtins.open = denied
152+ message = ""
153+ try:
154+ module._read_gateway_environment(41)
155+ except PermissionError as error:
156+ message = str(error)
157+ finally:
158+ builtins.open = real_open
159+
160+ print(json.dumps({"message": message, "opened": opened}))
161+ ` ,
162+ TRANSACTION ,
163+ ] ,
164+ { encoding : "utf8" } ,
165+ ) ;
166+
167+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
168+ expect ( JSON . parse ( result . stdout ) ) . toEqual ( {
169+ message : "Hermes gateway environment is unavailable" ,
170+ opened : [ "/proc/41/environ" ] ,
92171 } ) ;
93172 } ) ;
94173
@@ -227,7 +306,7 @@ print(json.dumps({
227306 } ) ;
228307 } ) ;
229308
230- it ( "prefers the marker over the service-manager environment (#8543)" , ( ) => {
309+ it ( "prefers the root marker over the gateway environment (#8543)" , ( ) => {
231310 const result = spawnSync (
232311 "python3" ,
233312 [
@@ -241,7 +320,7 @@ sys.modules[spec.name] = module
241320spec.loader.exec_module(module)
242321
243322module._gateway_identity = lambda: (41, 333)
244- module._service_manager_gateway_public_port = lambda identity: 8649
323+ module._gateway_environment_public_port = lambda identity: 8649
245324
246325module._root_gateway_public_port_marker = lambda: 8647
247326marker_wins = module._resolve_gateway_public_port()
0 commit comments