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,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
+ ]
@@ -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()