88
99import asyncio
1010import functools
11+ from typing import Any
1112from typing import Optional
1213
1314from ._audit import AuditLogger
1415from ._filter import ToolSafetyFilter
1516from ._policy import PolicyConfig
1617from ._scanner import SafetyScanner
1718from ._types import Decision
19+ from ._types import RiskLevel
1820from ._types import ScanInput
1921
2022
@@ -25,22 +27,25 @@ def wrap_tool(tool, policy: PolicyConfig, *, audit_path: Optional[str] = None):
2527 return tool
2628
2729
28- def _load_create_code_execution_result () :
29- """Load create_code_execution_result without importing code_executors package.
30+ class _Outcome :
31+ """Minimal stand-in for trpc_agent_sdk.types.Outcome used in deny results."""
3032
31- ``import trpc_agent_sdk.code_executors`` pulls optional docker deps via
32- package ``__init__``. Load the leaf module by file path instead.
33- """
34- import importlib .util
35- from pathlib import Path
33+ def __init__ (self , name : str ):
34+ self .name = name
35+
36+
37+ class _CodeExecutionResult :
38+ """Minimal CodeExecutionResult-compatible object (no code_executors import)."""
39+
40+ def __init__ (self , * , output : str , outcome_name : str ):
41+ self .output = output
42+ self .outcome = _Outcome (outcome_name )
3643
37- path = Path (__file__ ).resolve ().parents [1 ] / "code_executors" / "_types.py"
38- spec = importlib .util .spec_from_file_location ("_safety_code_exec_types" , path )
39- if spec is None or spec .loader is None :
40- raise ImportError (f"cannot load code executor types from { path } " )
41- mod = importlib .util .module_from_spec (spec )
42- spec .loader .exec_module (mod )
43- return mod .create_code_execution_result
44+
45+ def _deny_code_result (rule_ids : list [str ]) -> _CodeExecutionResult :
46+ """Build a failed code-execution result without importing code_executors."""
47+ msg = f"Code execution error:\n TOOL_SAFETY_DENY: { rule_ids } \n "
48+ return _CodeExecutionResult (output = msg , outcome_name = "OUTCOME_FAILED" )
4449
4550
4651def _normalize_block_language (raw_lang : str | None , code : str ) -> str :
@@ -59,8 +64,6 @@ def _normalize_block_language(raw_lang: str | None, code: str) -> str:
5964
6065def _scan_code_input (scanner : SafetyScanner , input_data , * , block_on_review : bool ):
6166 """Scan code / code_blocks with per-block language; return worst report."""
62- from ._types import RiskLevel
63-
6467 order = {
6568 RiskLevel .NONE : 0 ,
6669 RiskLevel .LOW : 1 ,
@@ -77,7 +80,15 @@ def _scan_code_input(scanner: SafetyScanner, input_data, *, block_on_review: boo
7780 worst = None
7881 for block in blocks :
7982 code = getattr (block , "code" , None ) or ""
80- lang = _normalize_block_language (getattr (block , "language" , None ), code )
83+ declared = (getattr (block , "language" , None ) or "" ).strip ().lower ()
84+ # Mislabeled bash-as-python must still hit bash rules via content check.
85+ if declared in ("sh" , "shell" , "bash" ):
86+ lang = "bash"
87+ elif declared in ("python" , ) or "py" in declared :
88+ inferred = _normalize_block_language ("" , code )
89+ lang = "bash" if inferred == "bash" else "python"
90+ else :
91+ lang = _normalize_block_language (declared , code )
8192 report = scanner .scan (ScanInput (script = code , language = lang , tool_name = "code_executor" ))
8293 if worst is None :
8394 worst = report
@@ -98,71 +109,64 @@ def _should_block_report(report, block_on_review: bool) -> bool:
98109class SafetyGuardedCodeExecutor :
99110 """Code-executor wrapper that scans code before delegating.
100111
101- Prefer this over the factory-style :func:`safe_code_executor` when you need
102- an explicit class. Constructed lazily so optional docker deps are only
103- imported when used.
112+ Does **not** import ``trpc_agent_sdk.code_executors`` (avoids optional docker).
113+ Deny results use a minimal object with ``.output`` / ``.outcome.name``.
104114 """
105115
106- def __init__ (self , inner , policy : PolicyConfig , * , audit_path : Optional [str ] = None , block_on_review : bool = False ):
116+ def __init__ (
117+ self ,
118+ inner ,
119+ policy : PolicyConfig ,
120+ * ,
121+ audit_path : Optional [str ] = None ,
122+ block_on_review : bool = False ,
123+ ):
107124 self ._inner = inner
108125 self ._scanner = SafetyScanner (policy = policy )
109126 self ._audit = AuditLogger (audit_path )
110127 self ._block_on_review = block_on_review or policy .block_on_review
111128
112129 async def execute_code (self , invocation_context , input_data ):
113- create_code_execution_result = _load_create_code_execution_result ()
114-
115130 report = _scan_code_input (self ._scanner , input_data , block_on_review = self ._block_on_review )
116131 should_block = _should_block_report (report , self ._block_on_review )
117132 if report is not None :
118133 self ._audit .log (report , intercepted = should_block )
119134 if should_block and report is not None :
120- return create_code_execution_result ( stderr = f"TOOL_SAFETY_DENY: { report .rule_ids } " )
135+ return _deny_code_result ( report .rule_ids )
121136 return await self ._inner .execute_code (invocation_context , input_data )
122137
123138
124- def safe_code_executor (inner , policy : PolicyConfig , * , audit_path : Optional [str ] = None , block_on_review : bool = False ):
139+ def safe_code_executor (
140+ inner ,
141+ policy : PolicyConfig ,
142+ * ,
143+ audit_path : Optional [str ] = None ,
144+ block_on_review : bool = False ,
145+ ):
125146 """Create a code-executor wrapper that scans code before delegating.
126147
127- Returns an instance of a dynamically built ``BaseCodeExecutor `` subclass so
128- it remains type-compatible with the executor hierarchy .
148+ Returns a simple object with ``execute_code `` (does not subclass
149+ BaseCodeExecutor, to avoid importing optional code_executors deps) .
129150 """
130- import importlib .util
131- from pathlib import Path
132-
133- base_path = Path (__file__ ).resolve ().parents [1 ] / "code_executors" / "_base_code_executor.py"
134- types_path = Path (__file__ ).resolve ().parents [1 ] / "code_executors" / "_types.py"
135- base_spec = importlib .util .spec_from_file_location ("_safety_base_code_executor" , base_path )
136- types_spec = importlib .util .spec_from_file_location ("_safety_code_exec_types" , types_path )
137- if base_spec is None or base_spec .loader is None or types_spec is None or types_spec .loader is None :
138- raise ImportError ("cannot load code executor helpers without package init" )
139- base_mod = importlib .util .module_from_spec (base_spec )
140- types_mod = importlib .util .module_from_spec (types_spec )
141- base_spec .loader .exec_module (base_mod )
142- types_spec .loader .exec_module (types_mod )
143- BaseCodeExecutor = base_mod .BaseCodeExecutor
144- create_code_execution_result = types_mod .create_code_execution_result
145-
146151 scanner = SafetyScanner (policy = policy )
147152 audit = AuditLogger (audit_path )
148153 block_review = block_on_review or policy .block_on_review
149154
150- class _SafeCodeExecutor ( BaseCodeExecutor ) :
155+ class _SafeCodeExecutor :
151156
152157 async def execute_code (self , invocation_context , input_data ):
153158 report = _scan_code_input (scanner , input_data , block_on_review = block_review )
154159 should_block = _should_block_report (report , block_review )
155160 if report is not None :
156161 audit .log (report , intercepted = should_block )
157162 if should_block and report is not None :
158- return create_code_execution_result ( stderr = f"TOOL_SAFETY_DENY: { report .rule_ids } " )
163+ return _deny_code_result ( report .rule_ids )
159164 return await inner .execute_code (invocation_context , input_data )
160165
161166 return _SafeCodeExecutor ()
162167
163168
164- # Backwards-compatible factory alias (returns a BaseCodeExecutor instance).
165- # Prefer SafetyGuardedCodeExecutor for an explicit class, or safe_code_executor().
169+ # Backwards-compatible factory alias (returns an object with execute_code).
166170SafeCodeExecutor = safe_code_executor
167171
168172
0 commit comments