tactus 0.31.0__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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- tactus-0.31.0.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
|