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.
- 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.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS Bedrock provider implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional, Dict
|
|
7
|
+
from tactus.providers.base import ProviderConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BedrockProvider:
|
|
11
|
+
"""AWS Bedrock LLM provider."""
|
|
12
|
+
|
|
13
|
+
PROVIDER_NAME = "bedrock"
|
|
14
|
+
|
|
15
|
+
# Known Bedrock models (Claude via Bedrock)
|
|
16
|
+
KNOWN_MODELS = {
|
|
17
|
+
"anthropic.claude-haiku-4-5-20251001-v1:0", # Claude 4.5 Haiku
|
|
18
|
+
"anthropic.claude-3-5-sonnet-20240620-v1:0",
|
|
19
|
+
"anthropic.claude-3-5-haiku-20241022-v1:0",
|
|
20
|
+
"anthropic.claude-3-sonnet-20240229-v1:0",
|
|
21
|
+
"anthropic.claude-3-haiku-20240307-v1:0",
|
|
22
|
+
"anthropic.claude-v2",
|
|
23
|
+
"anthropic.claude-v2:1",
|
|
24
|
+
"anthropic.claude-instant-v1",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def validate_model(model_id: str) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Validate that a model ID is valid for Bedrock.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
model_id: The model identifier to validate
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if valid (starts with 'anthropic.' or in known models)
|
|
37
|
+
"""
|
|
38
|
+
return (
|
|
39
|
+
model_id in BedrockProvider.KNOWN_MODELS
|
|
40
|
+
or model_id.startswith("anthropic.")
|
|
41
|
+
or model_id.startswith("amazon.")
|
|
42
|
+
or model_id.startswith("meta.")
|
|
43
|
+
or model_id.startswith("cohere.")
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def get_required_credentials() -> list[str]:
|
|
48
|
+
"""
|
|
49
|
+
Get list of required credential keys for Bedrock.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List containing AWS credential keys
|
|
53
|
+
"""
|
|
54
|
+
return [
|
|
55
|
+
"AWS_ACCESS_KEY_ID",
|
|
56
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
57
|
+
"AWS_DEFAULT_REGION", # Optional but recommended
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def check_credentials() -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if Bedrock credentials are available.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if AWS credentials are set
|
|
67
|
+
"""
|
|
68
|
+
return bool(os.environ.get("AWS_ACCESS_KEY_ID") and os.environ.get("AWS_SECRET_ACCESS_KEY"))
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def create_config(
|
|
72
|
+
model_id: str,
|
|
73
|
+
credentials: Optional[Dict[str, str]] = None,
|
|
74
|
+
region: Optional[str] = None,
|
|
75
|
+
**kwargs,
|
|
76
|
+
) -> ProviderConfig:
|
|
77
|
+
"""
|
|
78
|
+
Create a Bedrock provider configuration.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
model_id: Model identifier (e.g., 'anthropic.claude-3-5-sonnet-20240620-v1:0')
|
|
82
|
+
credentials: Optional credentials dict with AWS keys
|
|
83
|
+
region: AWS region (defaults to us-east-1)
|
|
84
|
+
**kwargs: Additional config
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ProviderConfig instance
|
|
88
|
+
"""
|
|
89
|
+
# Get credentials from dict or environment
|
|
90
|
+
creds = {}
|
|
91
|
+
if credentials:
|
|
92
|
+
if "access_key_id" in credentials:
|
|
93
|
+
creds["access_key_id"] = credentials["access_key_id"]
|
|
94
|
+
if "secret_access_key" in credentials:
|
|
95
|
+
creds["secret_access_key"] = credentials["secret_access_key"]
|
|
96
|
+
# Also check uppercase keys
|
|
97
|
+
if "AWS_ACCESS_KEY_ID" in credentials:
|
|
98
|
+
creds["access_key_id"] = credentials["AWS_ACCESS_KEY_ID"]
|
|
99
|
+
if "AWS_SECRET_ACCESS_KEY" in credentials:
|
|
100
|
+
creds["secret_access_key"] = credentials["AWS_SECRET_ACCESS_KEY"]
|
|
101
|
+
|
|
102
|
+
# Get region from parameter, credentials, or environment
|
|
103
|
+
if not region:
|
|
104
|
+
if credentials and "region" in credentials:
|
|
105
|
+
region = credentials["region"]
|
|
106
|
+
elif credentials and "AWS_DEFAULT_REGION" in credentials:
|
|
107
|
+
region = credentials["AWS_DEFAULT_REGION"]
|
|
108
|
+
else:
|
|
109
|
+
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
|
110
|
+
|
|
111
|
+
return ProviderConfig(
|
|
112
|
+
provider_name=BedrockProvider.PROVIDER_NAME,
|
|
113
|
+
model_id=model_id,
|
|
114
|
+
credentials=creds if creds else None,
|
|
115
|
+
region=region,
|
|
116
|
+
additional_config=kwargs,
|
|
117
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Gemini provider implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional, Dict
|
|
7
|
+
from tactus.providers.base import ProviderConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GoogleProvider:
|
|
11
|
+
"""Google Gemini LLM provider."""
|
|
12
|
+
|
|
13
|
+
PROVIDER_NAME = "google-gla" # Google GenAI (not Vertex AI)
|
|
14
|
+
|
|
15
|
+
# Known Gemini models
|
|
16
|
+
# Reference: https://ai.google.dev/gemini-api/docs/models/gemini
|
|
17
|
+
KNOWN_MODELS = {
|
|
18
|
+
# Gemini 3 models (preview)
|
|
19
|
+
"gemini-3-pro-preview",
|
|
20
|
+
"gemini-3-flash-preview",
|
|
21
|
+
"gemini-3-pro-image-preview",
|
|
22
|
+
# Gemini 2.0 models
|
|
23
|
+
"gemini-2.0-flash-exp",
|
|
24
|
+
"gemini-2.0-flash-thinking-exp",
|
|
25
|
+
"gemini-2.0-flash",
|
|
26
|
+
"gemini-2.0-flash-lite",
|
|
27
|
+
# Gemini 1.5 models
|
|
28
|
+
"gemini-1.5-pro",
|
|
29
|
+
"gemini-1.5-flash",
|
|
30
|
+
"gemini-1.5-flash-8b",
|
|
31
|
+
# Experimental models
|
|
32
|
+
"gemini-exp-1206",
|
|
33
|
+
"gemini-exp-1121",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def validate_model(model_id: str) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Validate that a model ID is valid for Google Gemini.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model_id: The model identifier to validate
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if valid (starts with 'gemini' or in known models)
|
|
46
|
+
"""
|
|
47
|
+
return model_id in GoogleProvider.KNOWN_MODELS or model_id.startswith("gemini")
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_required_credentials() -> list[str]:
|
|
51
|
+
"""
|
|
52
|
+
Get list of required credential keys for Google Gemini.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List containing 'GOOGLE_API_KEY'
|
|
56
|
+
"""
|
|
57
|
+
return ["GOOGLE_API_KEY"]
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def check_credentials() -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Check if Google credentials are available.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if GOOGLE_API_KEY is set
|
|
66
|
+
"""
|
|
67
|
+
return bool(os.environ.get("GOOGLE_API_KEY"))
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def create_config(
|
|
71
|
+
model_id: str,
|
|
72
|
+
credentials: Optional[Dict[str, str]] = None,
|
|
73
|
+
region: Optional[str] = None,
|
|
74
|
+
**kwargs,
|
|
75
|
+
) -> ProviderConfig:
|
|
76
|
+
"""
|
|
77
|
+
Create a Google Gemini provider configuration.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
model_id: Model identifier (e.g., 'gemini-2.0-flash-exp', 'gemini-1.5-pro')
|
|
81
|
+
credentials: Optional credentials dict with 'api_key'
|
|
82
|
+
region: Ignored for Google (no regional endpoints)
|
|
83
|
+
**kwargs: Additional config (ignored)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
ProviderConfig instance
|
|
87
|
+
"""
|
|
88
|
+
# Get API key from credentials or environment
|
|
89
|
+
api_key = None
|
|
90
|
+
if credentials and "api_key" in credentials:
|
|
91
|
+
api_key = credentials["api_key"]
|
|
92
|
+
elif not os.environ.get("GOOGLE_API_KEY"):
|
|
93
|
+
# Try to get from credentials dict with uppercase key
|
|
94
|
+
if credentials and "GOOGLE_API_KEY" in credentials:
|
|
95
|
+
api_key = credentials["GOOGLE_API_KEY"]
|
|
96
|
+
|
|
97
|
+
creds = {"api_key": api_key} if api_key else None
|
|
98
|
+
|
|
99
|
+
return ProviderConfig(
|
|
100
|
+
provider_name=GoogleProvider.PROVIDER_NAME,
|
|
101
|
+
model_id=model_id,
|
|
102
|
+
credentials=creds,
|
|
103
|
+
region=None, # Google doesn't use regions
|
|
104
|
+
additional_config=kwargs,
|
|
105
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI provider implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional, Dict
|
|
7
|
+
from tactus.providers.base import ProviderConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenAIProvider:
|
|
11
|
+
"""OpenAI LLM provider."""
|
|
12
|
+
|
|
13
|
+
PROVIDER_NAME = "openai"
|
|
14
|
+
|
|
15
|
+
# Known OpenAI models
|
|
16
|
+
KNOWN_MODELS = {
|
|
17
|
+
"gpt-4o",
|
|
18
|
+
"gpt-4o-mini",
|
|
19
|
+
"gpt-4-turbo",
|
|
20
|
+
"gpt-4-turbo-preview",
|
|
21
|
+
"gpt-4",
|
|
22
|
+
"gpt-3.5-turbo",
|
|
23
|
+
"o1",
|
|
24
|
+
"o1-mini",
|
|
25
|
+
"o1-preview",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def validate_model(model_id: str) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Validate that a model ID is valid for OpenAI.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
model_id: The model identifier to validate
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if valid (starts with 'gpt' or 'o1'), False otherwise
|
|
38
|
+
"""
|
|
39
|
+
# Accept known models or anything starting with gpt/o1
|
|
40
|
+
return model_id in OpenAIProvider.KNOWN_MODELS or model_id.startswith(("gpt", "o1"))
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_required_credentials() -> list[str]:
|
|
44
|
+
"""
|
|
45
|
+
Get list of required credential keys for OpenAI.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List containing 'OPENAI_API_KEY'
|
|
49
|
+
"""
|
|
50
|
+
return ["OPENAI_API_KEY"]
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def check_credentials() -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Check if OpenAI credentials are available.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if OPENAI_API_KEY is set
|
|
59
|
+
"""
|
|
60
|
+
return bool(os.environ.get("OPENAI_API_KEY"))
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def create_config(
|
|
64
|
+
model_id: str,
|
|
65
|
+
credentials: Optional[Dict[str, str]] = None,
|
|
66
|
+
region: Optional[str] = None,
|
|
67
|
+
**kwargs,
|
|
68
|
+
) -> ProviderConfig:
|
|
69
|
+
"""
|
|
70
|
+
Create an OpenAI provider configuration.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
model_id: Model identifier (e.g., 'gpt-4o')
|
|
74
|
+
credentials: Optional credentials dict with 'api_key'
|
|
75
|
+
region: Ignored for OpenAI
|
|
76
|
+
**kwargs: Additional config (ignored)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ProviderConfig instance
|
|
80
|
+
"""
|
|
81
|
+
# Get API key from credentials or environment
|
|
82
|
+
api_key = None
|
|
83
|
+
if credentials and "api_key" in credentials:
|
|
84
|
+
api_key = credentials["api_key"]
|
|
85
|
+
elif not os.environ.get("OPENAI_API_KEY"):
|
|
86
|
+
# Try to get from credentials dict with uppercase key
|
|
87
|
+
if credentials and "OPENAI_API_KEY" in credentials:
|
|
88
|
+
api_key = credentials["OPENAI_API_KEY"]
|
|
89
|
+
|
|
90
|
+
creds = {"api_key": api_key} if api_key else None
|
|
91
|
+
|
|
92
|
+
return ProviderConfig(
|
|
93
|
+
provider_name=OpenAIProvider.PROVIDER_NAME,
|
|
94
|
+
model_id=model_id,
|
|
95
|
+
credentials=creds,
|
|
96
|
+
region=None, # OpenAI doesn't use regions
|
|
97
|
+
additional_config=kwargs,
|
|
98
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker sandbox module for Tactus.
|
|
3
|
+
|
|
4
|
+
Provides container-based isolation for procedure execution, protecting
|
|
5
|
+
the host system from potentially unsafe agent tool operations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from tactus.sandbox import (
|
|
9
|
+
is_docker_available,
|
|
10
|
+
SandboxConfig,
|
|
11
|
+
ContainerRunner,
|
|
12
|
+
SandboxError,
|
|
13
|
+
SandboxUnavailableError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Check if Docker is available
|
|
17
|
+
available, reason = is_docker_available()
|
|
18
|
+
|
|
19
|
+
# Configure sandbox
|
|
20
|
+
config = SandboxConfig(enabled=True)
|
|
21
|
+
|
|
22
|
+
# Run procedure in sandbox
|
|
23
|
+
runner = ContainerRunner(config)
|
|
24
|
+
result = await runner.run(source, params)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .config import SandboxConfig, SandboxLimits, get_default_sandbox_config
|
|
28
|
+
from .docker_manager import (
|
|
29
|
+
DockerManager,
|
|
30
|
+
is_docker_available,
|
|
31
|
+
DEFAULT_IMAGE_NAME,
|
|
32
|
+
DEFAULT_IMAGE_TAG,
|
|
33
|
+
)
|
|
34
|
+
from .container_runner import (
|
|
35
|
+
ContainerRunner,
|
|
36
|
+
SandboxError,
|
|
37
|
+
SandboxUnavailableError,
|
|
38
|
+
)
|
|
39
|
+
from .protocol import (
|
|
40
|
+
ExecutionRequest,
|
|
41
|
+
ExecutionResult,
|
|
42
|
+
ExecutionStatus,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# Config
|
|
47
|
+
"SandboxConfig",
|
|
48
|
+
"SandboxLimits",
|
|
49
|
+
"get_default_sandbox_config",
|
|
50
|
+
# Docker management
|
|
51
|
+
"DockerManager",
|
|
52
|
+
"is_docker_available",
|
|
53
|
+
"DEFAULT_IMAGE_NAME",
|
|
54
|
+
"DEFAULT_IMAGE_TAG",
|
|
55
|
+
# Container execution
|
|
56
|
+
"ContainerRunner",
|
|
57
|
+
"SandboxError",
|
|
58
|
+
"SandboxUnavailableError",
|
|
59
|
+
# Protocol
|
|
60
|
+
"ExecutionRequest",
|
|
61
|
+
"ExecutionResult",
|
|
62
|
+
"ExecutionStatus",
|
|
63
|
+
]
|
tactus/sandbox/config.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sandbox configuration model for Docker-based isolation.
|
|
3
|
+
|
|
4
|
+
Defines the SandboxConfig Pydantic model for controlling container execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, model_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SandboxLimits(BaseModel):
|
|
14
|
+
"""Resource limits for the sandbox container."""
|
|
15
|
+
|
|
16
|
+
memory: str = Field(default="2g", description="Memory limit (e.g., '2g', '512m')")
|
|
17
|
+
cpus: str = Field(default="2", description="CPU limit (e.g., '2', '0.5')")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SandboxConfig(BaseModel):
|
|
21
|
+
"""
|
|
22
|
+
Configuration for Docker sandbox execution.
|
|
23
|
+
|
|
24
|
+
Controls whether and how procedures run in isolated Docker containers.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Core settings
|
|
28
|
+
# Security model:
|
|
29
|
+
# - enabled=None (default): Sandbox AUTO (use if available; otherwise run without isolation)
|
|
30
|
+
# - enabled=True: Sandbox REQUIRED, error if Docker unavailable
|
|
31
|
+
# - enabled=False: Sandbox explicitly disabled (security risk acknowledged)
|
|
32
|
+
enabled: Optional[bool] = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Enable sandbox mode. None=auto, True=required, False=disabled",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Docker image settings
|
|
38
|
+
image: str = Field(
|
|
39
|
+
default="tactus-sandbox:local",
|
|
40
|
+
description="Docker image to use for sandbox execution",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# MCP server settings
|
|
44
|
+
mcp_servers_path: str = Field(
|
|
45
|
+
default="~/.tactus/mcp-servers",
|
|
46
|
+
description="Path to directory containing MCP server code and dependencies",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Additional environment variables to pass to container
|
|
50
|
+
env: Dict[str, str] = Field(
|
|
51
|
+
default_factory=dict,
|
|
52
|
+
description="Additional environment variables to pass to the container",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Volume mount settings
|
|
56
|
+
mount_current_dir: bool = Field(
|
|
57
|
+
default=True,
|
|
58
|
+
description="Mount current directory to /workspace:rw by default. Set false to disable.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Additional volume mounts
|
|
62
|
+
volumes: List[str] = Field(
|
|
63
|
+
default_factory=list,
|
|
64
|
+
description="Additional volume mounts in 'host:container:mode' format",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Network mode
|
|
68
|
+
network: str = Field(
|
|
69
|
+
default="bridge",
|
|
70
|
+
description="Docker network mode (bridge for broker access, none blocks all network)",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Broker transport (how the secretless runtime reaches the host broker)
|
|
74
|
+
# - tcp: Standard mode using TCP sockets (works locally and in K8s/cloud)
|
|
75
|
+
# - tls: TCP with TLS encryption (for production deployments)
|
|
76
|
+
# - stdio: Legacy mode using stdin/stdout (deprecated due to buffering issues)
|
|
77
|
+
broker_transport: str = Field(
|
|
78
|
+
default="tcp",
|
|
79
|
+
description="Broker transport for the runtime container: tcp, tls, or stdio (deprecated)",
|
|
80
|
+
)
|
|
81
|
+
broker_host: str = Field(
|
|
82
|
+
default="host.docker.internal",
|
|
83
|
+
description="Broker hostname for tcp/tls (as seen from inside the container)",
|
|
84
|
+
)
|
|
85
|
+
broker_bind_host: str = Field(
|
|
86
|
+
default="0.0.0.0",
|
|
87
|
+
description="Bind address for the host-side broker server in tcp/tls modes",
|
|
88
|
+
)
|
|
89
|
+
broker_port: int = Field(
|
|
90
|
+
default=0,
|
|
91
|
+
description="Port for the host-side broker server in tcp/tls modes (0=auto)",
|
|
92
|
+
)
|
|
93
|
+
broker_tls_cert_file: Optional[str] = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="TLS certificate file for broker (PEM). Required when broker_transport='tls'",
|
|
96
|
+
)
|
|
97
|
+
broker_tls_key_file: Optional[str] = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="TLS private key file for broker (PEM). Required when broker_transport='tls'",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Resource limits
|
|
103
|
+
limits: SandboxLimits = Field(
|
|
104
|
+
default_factory=SandboxLimits,
|
|
105
|
+
description="Resource limits for the container",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Timeout for container execution (seconds)
|
|
109
|
+
timeout: int = Field(
|
|
110
|
+
default=3600,
|
|
111
|
+
description="Maximum execution time in seconds before container is killed",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Development mode: mount live Tactus source code
|
|
115
|
+
dev_mode: bool = Field(
|
|
116
|
+
default=False,
|
|
117
|
+
description="Enable development mode: mount live Tactus source code instead of using baked-in version",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def get_mcp_servers_path(self) -> Path:
|
|
121
|
+
"""Get the expanded MCP servers path."""
|
|
122
|
+
return Path(self.mcp_servers_path).expanduser()
|
|
123
|
+
|
|
124
|
+
def is_explicitly_disabled(self) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check if sandbox has been explicitly disabled by the user.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if user set enabled=False (acknowledging security risk).
|
|
130
|
+
"""
|
|
131
|
+
return self.enabled is False
|
|
132
|
+
|
|
133
|
+
def should_use_sandbox(self, docker_available: bool) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Determine if sandbox should be used for execution.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
docker_available: Whether Docker is available and running.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if sandbox should be used.
|
|
142
|
+
"""
|
|
143
|
+
if self.is_explicitly_disabled():
|
|
144
|
+
return False
|
|
145
|
+
return docker_available
|
|
146
|
+
|
|
147
|
+
def should_error_if_unavailable(self) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Determine if we should error when Docker is unavailable.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if Docker unavailability should be a fatal error.
|
|
153
|
+
This is True only when the user explicitly requires the sandbox (enabled=True).
|
|
154
|
+
"""
|
|
155
|
+
return self.enabled is True
|
|
156
|
+
|
|
157
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
158
|
+
|
|
159
|
+
@model_validator(mode="after")
|
|
160
|
+
def add_default_volumes(self):
|
|
161
|
+
"""Add default volume mounts based on config flags."""
|
|
162
|
+
if self.mount_current_dir:
|
|
163
|
+
# Insert at beginning so user volumes can override
|
|
164
|
+
if ".:/workspace:rw" not in self.volumes:
|
|
165
|
+
self.volumes.insert(0, ".:/workspace:rw")
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_default_sandbox_config() -> SandboxConfig:
|
|
170
|
+
"""Get the default sandbox configuration."""
|
|
171
|
+
return SandboxConfig()
|