tactus 0.31.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ Lua Sandbox - Safe, restricted Lua execution environment.
3
+
4
+ Provides a sandboxed Lua runtime with:
5
+ - Data format libraries restricted to working directory (Csv, Tsv, Parquet, Hdf5, Excel)
6
+ - File and Json primitives injected separately by runtime
7
+ - require() available but restricted to loading .tac files from working directory only
8
+ - No dangerous operations (debug, io, loadfile, dofile removed)
9
+ - Only whitelisted primitives available
10
+ - Resource limits on CPU time and memory
11
+ """
12
+
13
+ import logging
14
+ import os
15
+ from typing import Dict, Any, Optional
16
+
17
+ try:
18
+ import lupa
19
+ from lupa import LuaRuntime
20
+
21
+ LUPA_AVAILABLE = True
22
+ except ImportError:
23
+ LUPA_AVAILABLE = False
24
+ LuaRuntime = None
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class LuaSandboxError(Exception):
30
+ """Raised when Lua sandbox setup or execution fails."""
31
+
32
+ pass
33
+
34
+
35
+ class LuaSandbox:
36
+ """Sandboxed Lua execution environment for procedure workflows."""
37
+
38
+ def __init__(
39
+ self,
40
+ execution_context: Optional[Any] = None,
41
+ strict_determinism: bool = False,
42
+ base_path: Optional[str] = None,
43
+ ):
44
+ """
45
+ Initialize the Lua sandbox.
46
+
47
+ Args:
48
+ execution_context: Optional ExecutionContext for checkpoint scope tracking
49
+ strict_determinism: If True, raise errors instead of warnings for non-deterministic ops
50
+ base_path: Optional base path for file operations and require(). Defaults to cwd.
51
+ """
52
+ if not LUPA_AVAILABLE:
53
+ raise LuaSandboxError("lupa library not available. Install with: pip install lupa")
54
+
55
+ # Store context for safe libraries
56
+ self.execution_context = execution_context
57
+ self.strict_determinism = strict_determinism
58
+
59
+ # Fix base_path at initialization time to prevent security boundary expansion
60
+ # This ensures file I/O libraries and require() always use the same base path,
61
+ # even if the working directory changes later
62
+ self.base_path = base_path if base_path else os.getcwd()
63
+
64
+ # Create Lua runtime with safety restrictions
65
+ self.lua = LuaRuntime(unpack_returned_tuples=True, attribute_filter=self._attribute_filter)
66
+
67
+ # Remove dangerous modules
68
+ self._remove_dangerous_modules()
69
+
70
+ # Configure safe require/package
71
+ self._setup_safe_require()
72
+
73
+ # Setup safe globals
74
+ self._setup_safe_globals()
75
+
76
+ logger.debug("Lua sandbox initialized successfully")
77
+
78
+ def _attribute_filter(self, obj, attr_name, is_setting):
79
+ """
80
+ Filter attribute access to prevent dangerous operations.
81
+
82
+ This is called by lupa for all attribute access from Lua code.
83
+ """
84
+ # Block access to private/protected attributes
85
+ if attr_name.startswith("_"):
86
+ raise AttributeError(f"Access to private attribute '{attr_name}' is not allowed")
87
+
88
+ # Block access to certain dangerous methods
89
+ blocked_methods = {
90
+ "__import__",
91
+ "__loader__",
92
+ "__spec__",
93
+ "__builtins__",
94
+ "eval",
95
+ "exec",
96
+ "compile",
97
+ "open",
98
+ "__subclasses__",
99
+ }
100
+
101
+ if attr_name in blocked_methods:
102
+ raise AttributeError(f"Access to '{attr_name}' is not allowed in sandbox")
103
+
104
+ return attr_name
105
+
106
+ def _remove_dangerous_modules(self):
107
+ """Remove dangerous Lua standard library modules."""
108
+ # Remove modules that provide file system or system access
109
+ # Note: 'package' and 'require' are kept but restricted in _setup_safe_require()
110
+ dangerous_modules = [
111
+ "io", # File I/O
112
+ "os", # Operating system operations
113
+ "dofile", # Load and execute files
114
+ "loadfile", # Load files
115
+ "load", # Load code
116
+ ]
117
+
118
+ lua_globals = self.lua.globals()
119
+
120
+ for module in dangerous_modules:
121
+ if module in lua_globals:
122
+ lua_globals[module] = None
123
+ logger.debug(f"Removed dangerous module/function: {module}")
124
+
125
+ # Whitelist only safe debug functions for source location tracking
126
+ # Keep debug.getinfo but remove dangerous debug functions
127
+ if "debug" in lua_globals:
128
+ self.lua.execute(
129
+ """
130
+ if debug then
131
+ local safe_debug = {
132
+ getinfo = debug.getinfo
133
+ }
134
+ debug = safe_debug
135
+ end
136
+ """
137
+ )
138
+ logger.debug("Replaced debug module with safe_debug (only getinfo allowed)")
139
+
140
+ def _setup_safe_require(self):
141
+ """Configure require/package to search user's project and stdlib.
142
+
143
+ This allows using Lua's require() mechanism while restricting module
144
+ loading to:
145
+ 1. User's project directory (base_path) - for local modules
146
+ 2. Tactus stdlib directory - for standard library modules
147
+
148
+ Example:
149
+ require("helpers/math") -- loads from base_path/helpers/math.tac
150
+ require("tactus.tools.done") -- loads from stdlib/tac/tactus/tools/done.tac
151
+ """
152
+ import tactus
153
+
154
+ # Get stdlib path from installed package location
155
+ package_root = os.path.dirname(tactus.__file__)
156
+ stdlib_tac_path = os.path.join(package_root, "stdlib", "tac")
157
+
158
+ # Build search paths:
159
+ # 1. User's project directory (existing behavior)
160
+ # 2. Tactus stdlib .tac files
161
+ user_path = os.path.join(self.base_path, "?.tac")
162
+ stdlib_path = os.path.join(stdlib_tac_path, "?.tac")
163
+
164
+ # Normalize backslashes for cross-platform compatibility
165
+ paths = [user_path, stdlib_path]
166
+ paths = [p.replace("\\", "/") for p in paths]
167
+
168
+ # Join with Lua's path separator (semicolon)
169
+ safe_path = ";".join(paths)
170
+
171
+ lua_globals = self.lua.globals()
172
+ package = lua_globals["package"]
173
+
174
+ if package:
175
+ # Set restricted search paths
176
+ package["path"] = safe_path
177
+
178
+ # Disable C module loading entirely
179
+ package["cpath"] = ""
180
+
181
+ # Clear preloaded modules that might provide dangerous access
182
+ if package["preload"]:
183
+ self.lua.execute("for k in pairs(package.preload) do package.preload[k] = nil end")
184
+
185
+ # Add Python stdlib loader
186
+ self._setup_python_stdlib_loader()
187
+
188
+ logger.debug(f"Configured safe require with paths: {safe_path}")
189
+ else:
190
+ logger.warning("package module not available - require will not work")
191
+
192
+ def _setup_python_stdlib_loader(self):
193
+ """Add custom loader for Python stdlib modules."""
194
+ from tactus.stdlib.loader import StdlibModuleLoader
195
+
196
+ # Create loader instance
197
+ self._stdlib_loader = StdlibModuleLoader(self, self.base_path)
198
+ loader_func = self._stdlib_loader.create_loader_function()
199
+
200
+ # Inject loader function into Lua
201
+ self.lua.globals()["_tactus_python_loader"] = loader_func
202
+
203
+ # Add to package.loaders (Lua 5.1) or package.searchers (Lua 5.2+)
204
+ # Lupa uses LuaJIT which follows Lua 5.1 conventions
205
+ self.lua.execute(
206
+ """
207
+ -- Add Python stdlib loader to package.loaders
208
+ -- Insert after the preload loader but before path loader
209
+ local loaders = package.loaders or package.searchers
210
+ if loaders then
211
+ -- Create wrapper that returns a loader function (Lua convention)
212
+ local function python_searcher(modname)
213
+ local result = _tactus_python_loader(modname)
214
+ if result then
215
+ -- Return a loader function that returns the module
216
+ return function() return result end
217
+ end
218
+ return nil
219
+ end
220
+
221
+ -- Insert at position 2 (after preload, before path)
222
+ table.insert(loaders, 2, python_searcher)
223
+ end
224
+ """
225
+ )
226
+
227
+ logger.debug("Python stdlib loader installed")
228
+
229
+ def _setup_safe_globals(self):
230
+ """Setup safe global functions and utilities."""
231
+ # Keep safe standard library functions
232
+ # (These are already available by default, just documenting them)
233
+ safe_functions = {
234
+ # Math
235
+ "math", # Math library (will be replaced with safe version if context available)
236
+ "tonumber", # Convert to number
237
+ "tostring", # Convert to string
238
+ # String operations
239
+ "string", # String library
240
+ # Table operations
241
+ "table", # Table library
242
+ "pairs", # Iterate over tables
243
+ "ipairs", # Iterate over arrays
244
+ "next", # Next element in table
245
+ # Type checking
246
+ "type", # Get type of value
247
+ "assert", # Assertions
248
+ "error", # Raise error
249
+ "pcall", # Protected call (try/catch)
250
+ # Other safe operations
251
+ "select", # Select arguments
252
+ "unpack", # Unpack table (Lua 5.1)
253
+ }
254
+
255
+ # Just log what's available - no need to explicitly set
256
+ logger.debug(f"Safe Lua functions available: {', '.join(safe_functions)}")
257
+
258
+ # Replace math and os libraries with safe versions if context available
259
+ if self.execution_context is not None:
260
+ from tactus.utils.safe_libraries import (
261
+ create_safe_math_library,
262
+ create_safe_os_library,
263
+ )
264
+
265
+ def get_context():
266
+ return self.execution_context
267
+
268
+ safe_math_dict = create_safe_math_library(get_context, self.strict_determinism)
269
+ safe_os_dict = create_safe_os_library(get_context, self.strict_determinism)
270
+
271
+ safe_math_table = self._dict_to_lua_table(safe_math_dict)
272
+ safe_os_table = self._dict_to_lua_table(safe_os_dict)
273
+
274
+ self.lua.globals()["math"] = safe_math_table
275
+ self.lua.globals()["os"] = safe_os_table
276
+
277
+ logger.debug("Installed safe math and os libraries with determinism checking")
278
+ return # Skip default os.date setup below
279
+
280
+ # Add safe subset of os module (only date function for timestamps)
281
+ # This is a fallback when no execution context is available (testing/REPL)
282
+ from datetime import datetime
283
+
284
+ def safe_date(format_str=None):
285
+ """Safe implementation of os.date() for timestamp generation."""
286
+ now = datetime.utcnow()
287
+ if format_str is None:
288
+ # Return default format like Lua's os.date()
289
+ return now.strftime("%a %b %d %H:%M:%S %Y")
290
+ elif format_str == "%Y-%m-%dT%H:%M:%SZ":
291
+ # ISO 8601 format
292
+ return now.strftime("%Y-%m-%dT%H:%M:%SZ")
293
+ else:
294
+ # Support Python strftime formats
295
+ try:
296
+ return now.strftime(format_str)
297
+ except Exception: # noqa: E722
298
+ return now.strftime("%a %b %d %H:%M:%S %Y")
299
+
300
+ # Create safe os table with only date function
301
+ safe_os = self.lua.table(date=safe_date)
302
+ self.lua.globals()["os"] = safe_os
303
+ logger.debug("Added safe os.date() function")
304
+
305
+ def setup_assignment_interception(self, callback: Any):
306
+ """
307
+ Setup assignment interception on global scope to capture variable definitions.
308
+
309
+ This allows capturing assignments like: greeter = Agent {...}
310
+ The callback will be invoked with (name, value) whenever a new global is assigned.
311
+
312
+ Args:
313
+ callback: Python function or Lua function to call on assignment
314
+ Should accept (name: str, value: Any) -> None
315
+
316
+ Example usage:
317
+ sandbox.setup_assignment_interception(lambda name, val: print(f"{name} = {val}"))
318
+ sandbox.execute("greeter = Agent {...}") # Triggers callback
319
+ """
320
+ # Store callback in Lua globals so metatable can access it
321
+ self.lua.globals()["_tactus_intercept_callback"] = callback
322
+
323
+ # Set metatable directly on _G (don't replace _G with proxy table)
324
+ lua_code = """
325
+ local mt = {
326
+ __newindex = function(t, key, value)
327
+ -- Call the Python callback if it exists
328
+ if _tactus_intercept_callback then
329
+ _tactus_intercept_callback(key, value)
330
+ end
331
+ -- Actually set the value
332
+ rawset(t, key, value)
333
+ end
334
+ }
335
+ setmetatable(_G, mt)
336
+ """
337
+
338
+ try:
339
+ self.lua.execute(lua_code)
340
+ logger.debug("Assignment interception enabled with metatable on _G")
341
+ except Exception as e:
342
+ logger.error(f"Failed to setup assignment interception: {e}", exc_info=True)
343
+ raise LuaSandboxError(f"Could not setup assignment interception: {e}")
344
+
345
+ def set_execution_context(self, context: Any):
346
+ """
347
+ Set or update execution context and refresh safe libraries.
348
+
349
+ Args:
350
+ context: ExecutionContext instance
351
+ """
352
+ self.execution_context = context
353
+ # Re-setup safe globals with context
354
+ self._setup_safe_globals()
355
+ logger.debug("ExecutionContext attached to LuaSandbox")
356
+
357
+ def inject_primitive(self, name: str, primitive_obj: Any):
358
+ """
359
+ Inject a Python primitive object into Lua globals.
360
+
361
+ Args:
362
+ name: Name of the primitive in Lua (e.g., "State", "Worker")
363
+ primitive_obj: Python object to expose to Lua
364
+ """
365
+ self.lua.globals()[name] = primitive_obj
366
+ logger.debug(f"Injected primitive '{name}' into Lua sandbox")
367
+
368
+ def set_global(self, name: str, value: Any):
369
+ """
370
+ Set a global variable in Lua.
371
+
372
+ Args:
373
+ name: Name of the global variable
374
+ value: Value to set (can be Python object, dict, etc.)
375
+ """
376
+ # Convert Python dicts to Lua tables if needed
377
+ if isinstance(value, dict):
378
+ lua_table = self.lua.table()
379
+ for k, v in value.items():
380
+ if isinstance(v, dict):
381
+ # Recursively convert nested dicts
382
+ lua_table[k] = self._dict_to_lua_table(v)
383
+ else:
384
+ lua_table[k] = v
385
+ self.lua.globals()[name] = lua_table
386
+ else:
387
+ self.lua.globals()[name] = value
388
+ logger.debug(f"Set global '{name}' in Lua sandbox")
389
+
390
+ def _dict_to_lua_table(self, d: dict):
391
+ """Convert Python dict to Lua table recursively."""
392
+ lua_table = self.lua.table()
393
+ for k, v in d.items():
394
+ if isinstance(v, dict):
395
+ lua_table[k] = self._dict_to_lua_table(v)
396
+ else:
397
+ lua_table[k] = v
398
+ return lua_table
399
+
400
+ def execute(self, lua_code: str) -> Any:
401
+ """
402
+ Execute Lua code in the sandbox.
403
+
404
+ Args:
405
+ lua_code: Lua code string to execute
406
+
407
+ Returns:
408
+ Result of the Lua code execution
409
+
410
+ Raises:
411
+ LuaSandboxError: If execution fails
412
+ """
413
+ try:
414
+ logger.debug(f"Executing Lua code ({len(lua_code)} bytes)")
415
+ result = self.lua.execute(lua_code)
416
+ logger.debug("Lua execution completed successfully")
417
+ return result
418
+
419
+ except lupa.LuaError as e:
420
+ # Lua runtime error
421
+ error_msg = str(e)
422
+ logger.error(f"Lua execution error: {error_msg}")
423
+ raise LuaSandboxError(f"Lua runtime error: {error_msg}")
424
+
425
+ except Exception as e:
426
+ # Other Python exceptions
427
+ logger.error(f"Sandbox execution error: {e}")
428
+ raise LuaSandboxError(f"Sandbox error: {e}")
429
+
430
+ def eval(self, lua_expression: str) -> Any:
431
+ """
432
+ Evaluate a Lua expression and return the result.
433
+
434
+ Args:
435
+ lua_expression: Lua expression to evaluate
436
+
437
+ Returns:
438
+ Result of the expression
439
+
440
+ Raises:
441
+ LuaSandboxError: If evaluation fails
442
+ """
443
+ try:
444
+ result = self.lua.eval(lua_expression)
445
+ return result
446
+
447
+ except lupa.LuaError as e:
448
+ error_msg = str(e)
449
+ logger.error(f"Lua eval error: {error_msg}")
450
+ raise LuaSandboxError(f"Lua eval error: {error_msg}")
451
+
452
+ def get_global(self, name: str) -> Any:
453
+ """Get a value from Lua global scope."""
454
+ return self.lua.globals()[name]
455
+
456
+ def create_lua_table(self, python_dict: Optional[Dict[str, Any]] = None) -> Any:
457
+ """
458
+ Create a Lua table from a Python dictionary.
459
+
460
+ Args:
461
+ python_dict: Python dictionary to convert (or None for empty table)
462
+
463
+ Returns:
464
+ Lua table object
465
+ """
466
+ if python_dict is None:
467
+ # Create empty Lua table
468
+ return self.lua.table()
469
+
470
+ # Create and populate Lua table
471
+ lua_table = self.lua.table()
472
+ for key, value in python_dict.items():
473
+ lua_table[key] = value
474
+
475
+ return lua_table
476
+
477
+ def lua_table_to_dict(self, lua_table: Any) -> Dict[str, Any]:
478
+ """
479
+ Convert a Lua table to a Python dictionary.
480
+
481
+ Args:
482
+ lua_table: Lua table object
483
+
484
+ Returns:
485
+ Python dictionary
486
+ """
487
+ result = {}
488
+
489
+ try:
490
+ # Use Lua's pairs() to iterate
491
+ for key, value in self.lua.globals().pairs(lua_table):
492
+ # Convert Lua values to Python types
493
+ if isinstance(value, self.lua.table_from):
494
+ # Recursively convert nested tables
495
+ result[key] = self.lua_table_to_dict(value)
496
+ else:
497
+ result[key] = value
498
+
499
+ except Exception as e:
500
+ logger.warning(f"Error converting Lua table to dict: {e}")
501
+ # Fallback: try direct iteration
502
+ try:
503
+ for key in lua_table:
504
+ result[key] = lua_table[key]
505
+ except Exception: # noqa: E722
506
+ pass
507
+
508
+ return result