7777logger = logging .getLogger (__name__ )
7878
7979
80+ # Small, deterministic expression subset accepted inside constructor-string
81+ # arguments. The values supplied to these callables have already been reduced
82+ # to plain data by ``_safe_constructor_arg``; callbacks and object attributes
83+ # therefore cannot cross into this compatibility path.
84+ _SAFE_CONSTRUCTOR_CALLS = {
85+ "abs" : abs ,
86+ "all" : all ,
87+ "any" : any ,
88+ "len" : len ,
89+ "max" : max ,
90+ "min" : min ,
91+ "round" : round ,
92+ "sorted" : sorted ,
93+ "sum" : sum ,
94+ }
95+
96+
8097class _ReturnResultSignal (ExecutionSignal ):
8198 """Signal raised when return_result() is called from within execute_python code.
8299
@@ -2009,13 +2026,135 @@ def _corrected_return_args(self, validated: Any, original_args: dict) -> dict:
20092026
20102027 return {"result" : corrected_result }
20112028
2029+ @staticmethod
2030+ def _safe_constructor_arg (node : ast .AST , session_locals : dict [str , Any ]) -> Any :
2031+ """Decode one constructor argument without executing Python code.
2032+
2033+ Literal constants/containers are decoded directly. Bare names retain
2034+ pass-by-reference semantics by resolving directly from the REPL namespace.
2035+ Containers are rebuilt recursively so they can contain those references
2036+ without evaluating Python expressions. A short allowlist covers common
2037+ deterministic expressions emitted by models, such as ``min(...)``; those
2038+ helpers operate only on copied plain data. Everything else, including
2039+ attributes and arbitrary callables, is rejected.
2040+ """
2041+
2042+ if isinstance (node , ast .Name ):
2043+ if node .id not in session_locals :
2044+ raise ValueError (f"unknown constructor argument name: { node .id } " )
2045+ # Dictionary lookup does not invoke any behavior on the referenced
2046+ # object. The trusted return type receives the same object identity.
2047+ return session_locals [node .id ]
2048+
2049+ if isinstance (node , ast .Constant ):
2050+ return CodeActStrategy ._copy_constructor_data (node .value )
2051+
2052+ if isinstance (node , ast .List ):
2053+ return [
2054+ CodeActStrategy ._safe_constructor_arg (item , session_locals ) for item in node .elts
2055+ ]
2056+
2057+ if isinstance (node , ast .Tuple ):
2058+ return tuple (
2059+ CodeActStrategy ._safe_constructor_arg (item , session_locals ) for item in node .elts
2060+ )
2061+
2062+ if isinstance (node , ast .Set ):
2063+ return {
2064+ CodeActStrategy ._copy_constructor_data (
2065+ CodeActStrategy ._safe_constructor_arg (item , session_locals )
2066+ )
2067+ for item in node .elts
2068+ }
2069+
2070+ if isinstance (node , ast .Dict ):
2071+ result : dict [Any , Any ] = {}
2072+ for key_node , value_node in zip (node .keys , node .values , strict = True ):
2073+ if key_node is None :
2074+ expanded = CodeActStrategy ._safe_constructor_arg (value_node , session_locals )
2075+ if type (expanded ) is not dict :
2076+ raise ValueError ("literal ** expansion must be an exact dict" )
2077+ for key , item in expanded .items ():
2078+ result [CodeActStrategy ._copy_constructor_data (key )] = item
2079+ else :
2080+ key = CodeActStrategy ._copy_constructor_data (
2081+ CodeActStrategy ._safe_constructor_arg (key_node , session_locals )
2082+ )
2083+ item = CodeActStrategy ._safe_constructor_arg (value_node , session_locals )
2084+ result [key ] = item
2085+ return result
2086+
2087+ # literal_eval supports numeric signs and complex-number literals without
2088+ # admitting general arithmetic or overloaded session objects.
2089+ if isinstance (node , (ast .UnaryOp , ast .BinOp )):
2090+ try :
2091+ return CodeActStrategy ._copy_constructor_data (ast .literal_eval (node ))
2092+ except (ValueError , TypeError , SyntaxError , MemoryError , RecursionError ) as exc :
2093+ raise ValueError ("unsupported constructor numeric expression" ) from exc
2094+
2095+ if isinstance (node , ast .Call ) and isinstance (node .func , ast .Name ):
2096+ fn = _SAFE_CONSTRUCTOR_CALLS .get (node .func .id )
2097+ if fn is None :
2098+ raise ValueError (f"unsupported constructor argument call: { node .func .id } " )
2099+ args = [
2100+ CodeActStrategy ._copy_constructor_data (
2101+ CodeActStrategy ._safe_constructor_arg (arg , session_locals )
2102+ )
2103+ for arg in node .args
2104+ ]
2105+ kwargs : dict [str , Any ] = {}
2106+ for keyword in node .keywords :
2107+ if keyword .arg is None :
2108+ expanded = CodeActStrategy ._copy_constructor_data (
2109+ CodeActStrategy ._safe_constructor_arg (keyword .value , session_locals )
2110+ )
2111+ if type (expanded ) is not dict or not all (type (key ) is str for key in expanded ):
2112+ raise ValueError ("safe call **kwargs must be a string-keyed dict" )
2113+ kwargs .update (expanded )
2114+ else :
2115+ kwargs [keyword .arg ] = CodeActStrategy ._copy_constructor_data (
2116+ CodeActStrategy ._safe_constructor_arg (keyword .value , session_locals )
2117+ )
2118+ result = fn (* args , ** kwargs )
2119+ return CodeActStrategy ._copy_constructor_data (result )
2120+
2121+ raise ValueError (f"unsupported constructor argument syntax: { type (node ).__name__ } " )
2122+
2123+ @staticmethod
2124+ def _copy_constructor_data (value : Any ) -> Any :
2125+ """Copy an exact built-in scalar/container value or reject it.
2126+
2127+ Used only before invoking a fixed deterministic helper. Exact-type checks
2128+ prevent arbitrary references from reaching those calls through conversion
2129+ or iteration hooks.
2130+ """
2131+
2132+ if value is None or type (value ) in (bool , int , float , complex , str , bytes ):
2133+ return value
2134+ if type (value ) is list :
2135+ return [CodeActStrategy ._copy_constructor_data (item ) for item in value ]
2136+ if type (value ) is tuple :
2137+ return tuple (CodeActStrategy ._copy_constructor_data (item ) for item in value )
2138+ if type (value ) is set :
2139+ return {CodeActStrategy ._copy_constructor_data (item ) for item in value }
2140+ if type (value ) is frozenset :
2141+ return frozenset (CodeActStrategy ._copy_constructor_data (item ) for item in value )
2142+ if type (value ) is dict :
2143+ return {
2144+ CodeActStrategy ._copy_constructor_data (key ): CodeActStrategy ._copy_constructor_data (
2145+ item
2146+ )
2147+ for key , item in value .items ()
2148+ }
2149+ raise ValueError (f"constructor argument { type (value ).__name__ } is not plain data" )
2150+
20122151 def _maybe_eval_constructor_string (self , value : str , return_type : Any , session : Any ) -> Any :
2013- """Eval a string that looks like a Python constructor call.
2152+ """Parse a string that looks like a Python constructor call.
20142153
20152154 Detects patterns like 'ClassName(field=value, ...)' where ClassName
2016- matches the expected return type. Evaluates in the session namespace
2017- so the type is available . Returns the constructed object on success,
2018- or the original string on failure.
2155+ matches the expected return type. Only plain literal/data arguments and
2156+ a small deterministic expression subset are accepted . Returns the
2157+ constructed object on success, or the original string on failure.
20192158
20202159 This handles a common LLM failure mode where the model calls
20212160 return_result as a tool with the constructor as a string argument
@@ -2034,21 +2173,46 @@ def _maybe_eval_constructor_string(self, value: str, return_type: Any, session:
20342173 if not candidate_name .isidentifier ():
20352174 return value
20362175
2037- # Check that the candidate name matches the expected return type
2038- # or is available in session locals
2176+ # Only the trusted return type may be constructed. Preserve safe aliases
2177+ # without allowing an arbitrary session-local factory to become code.
20392178 type_name = getattr (return_type , "__name__" , None )
2040- if candidate_name != type_name and candidate_name not in session .session_locals :
2179+ if not isinstance (return_type , type ):
2180+ return value
2181+ if (
2182+ candidate_name != type_name
2183+ and session .session_locals .get (candidate_name ) is not return_type
2184+ ):
20412185 return value
20422186
2043- # Build eval namespace: session locals + the return type itself (which may
2044- # live in module globals rather than session_locals).
2045- eval_ns = dict (session .session_locals )
2046- if type_name and type_name not in eval_ns and isinstance (return_type , type ):
2047- eval_ns [type_name ] = return_type
2048-
2049- # Try to eval in the combined namespace
20502187 try :
2051- result = eval (stripped , {"__builtins__" : {}}, eval_ns ) # noqa: S307
2188+ parsed = ast .parse (stripped , mode = "eval" ).body
2189+ if not isinstance (parsed , ast .Call ) or not isinstance (parsed .func , ast .Name ):
2190+ return value
2191+ if parsed .func .id != candidate_name :
2192+ return value
2193+
2194+ args : list [Any ] = []
2195+ for arg in parsed .args :
2196+ if isinstance (arg , ast .Starred ):
2197+ expanded = self ._safe_constructor_arg (arg .value , session .session_locals )
2198+ if type (expanded ) not in (list , tuple ):
2199+ raise ValueError ("constructor *args must be an exact list or tuple" )
2200+ args .extend (expanded )
2201+ else :
2202+ args .append (self ._safe_constructor_arg (arg , session .session_locals ))
2203+ kwargs : dict [str , Any ] = {}
2204+ for keyword in parsed .keywords :
2205+ if keyword .arg is None :
2206+ expanded = self ._safe_constructor_arg (keyword .value , session .session_locals )
2207+ if type (expanded ) is not dict or not all (type (key ) is str for key in expanded ):
2208+ raise ValueError ("constructor **kwargs must be a string-keyed dict" )
2209+ kwargs .update (expanded )
2210+ else :
2211+ kwargs [keyword .arg ] = self ._safe_constructor_arg (
2212+ keyword .value , session .session_locals
2213+ )
2214+
2215+ result = return_type (* args , ** kwargs )
20522216 get_harness_metrics ().constructor_string_coerced (candidate_name )
20532217 logger .debug (
20542218 "[CODEACT] Coerced constructor-call string %r into %s instance" ,
@@ -2058,7 +2222,7 @@ def _maybe_eval_constructor_string(self, value: str, return_type: Any, session:
20582222 return result
20592223 except Exception :
20602224 logger .debug (
2061- "[CODEACT] Failed to eval constructor string %r, returning as-is" ,
2225+ "[CODEACT] Failed to parse constructor string %r, returning as-is" ,
20622226 stripped [:80 ],
20632227 )
20642228 return value
0 commit comments