signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2025 SignalWire
|
|
3
|
+
|
|
4
|
+
This file is part of the SignalWire AI Agents SDK.
|
|
5
|
+
|
|
6
|
+
Licensed under the MIT License.
|
|
7
|
+
See LICENSE file in the project root for full license information.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import secrets
|
|
12
|
+
from typing import Dict, Any, Optional, Tuple, List, Union
|
|
13
|
+
from signalwire_agents.core.logging_config import get_logger
|
|
14
|
+
from signalwire_agents.core.config_loader import ConfigLoader
|
|
15
|
+
|
|
16
|
+
logger = get_logger("security_config")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecurityConfig:
|
|
20
|
+
"""
|
|
21
|
+
Unified security configuration for SignalWire services.
|
|
22
|
+
|
|
23
|
+
This class provides centralized security settings that can be used by
|
|
24
|
+
both SWML and Search services, ensuring consistent security behavior.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Security environment variable names
|
|
28
|
+
SSL_ENABLED = 'SWML_SSL_ENABLED'
|
|
29
|
+
SSL_CERT_PATH = 'SWML_SSL_CERT_PATH'
|
|
30
|
+
SSL_KEY_PATH = 'SWML_SSL_KEY_PATH'
|
|
31
|
+
SSL_DOMAIN = 'SWML_DOMAIN'
|
|
32
|
+
SSL_VERIFY_MODE = 'SWML_SSL_VERIFY_MODE'
|
|
33
|
+
|
|
34
|
+
# Additional security settings
|
|
35
|
+
ALLOWED_HOSTS = 'SWML_ALLOWED_HOSTS'
|
|
36
|
+
CORS_ORIGINS = 'SWML_CORS_ORIGINS'
|
|
37
|
+
MAX_REQUEST_SIZE = 'SWML_MAX_REQUEST_SIZE'
|
|
38
|
+
RATE_LIMIT = 'SWML_RATE_LIMIT'
|
|
39
|
+
REQUEST_TIMEOUT = 'SWML_REQUEST_TIMEOUT'
|
|
40
|
+
USE_HSTS = 'SWML_USE_HSTS'
|
|
41
|
+
HSTS_MAX_AGE = 'SWML_HSTS_MAX_AGE'
|
|
42
|
+
|
|
43
|
+
# Authentication
|
|
44
|
+
BASIC_AUTH_USER = 'SWML_BASIC_AUTH_USER'
|
|
45
|
+
BASIC_AUTH_PASSWORD = 'SWML_BASIC_AUTH_PASSWORD'
|
|
46
|
+
|
|
47
|
+
# Defaults (secure by default)
|
|
48
|
+
DEFAULTS = {
|
|
49
|
+
SSL_ENABLED: False, # Off by default, but secure when enabled
|
|
50
|
+
SSL_VERIFY_MODE: 'CERT_REQUIRED',
|
|
51
|
+
ALLOWED_HOSTS: '*', # Accept all hosts by default for backward compatibility
|
|
52
|
+
CORS_ORIGINS: '*', # Accept all origins by default for backward compatibility
|
|
53
|
+
MAX_REQUEST_SIZE: 10 * 1024 * 1024, # 10MB
|
|
54
|
+
RATE_LIMIT: 60, # Requests per minute
|
|
55
|
+
REQUEST_TIMEOUT: 30, # Seconds
|
|
56
|
+
USE_HSTS: True, # Enable HSTS when HTTPS is on
|
|
57
|
+
HSTS_MAX_AGE: 31536000, # 1 year
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __init__(self, config_file: Optional[str] = None, service_name: Optional[str] = None):
|
|
61
|
+
"""
|
|
62
|
+
Initialize security configuration.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config_file: Optional path to config file
|
|
66
|
+
service_name: Optional service name for finding service-specific config
|
|
67
|
+
"""
|
|
68
|
+
# First, set defaults
|
|
69
|
+
self._set_defaults()
|
|
70
|
+
|
|
71
|
+
# Then load from environment variables (backward compatibility)
|
|
72
|
+
self.load_from_env()
|
|
73
|
+
|
|
74
|
+
# Finally, apply config file if available (highest priority)
|
|
75
|
+
self._load_config_file(config_file, service_name)
|
|
76
|
+
|
|
77
|
+
def _set_defaults(self):
|
|
78
|
+
"""Set default values for all configuration"""
|
|
79
|
+
# SSL configuration
|
|
80
|
+
self.ssl_enabled = self.DEFAULTS[self.SSL_ENABLED]
|
|
81
|
+
self.ssl_cert_path = None
|
|
82
|
+
self.ssl_key_path = None
|
|
83
|
+
self.domain = None
|
|
84
|
+
self.ssl_verify_mode = self.DEFAULTS[self.SSL_VERIFY_MODE]
|
|
85
|
+
|
|
86
|
+
# Additional settings
|
|
87
|
+
self.allowed_hosts = self._parse_list(self.DEFAULTS[self.ALLOWED_HOSTS])
|
|
88
|
+
self.cors_origins = self._parse_list(self.DEFAULTS[self.CORS_ORIGINS])
|
|
89
|
+
self.max_request_size = self.DEFAULTS[self.MAX_REQUEST_SIZE]
|
|
90
|
+
self.rate_limit = self.DEFAULTS[self.RATE_LIMIT]
|
|
91
|
+
self.request_timeout = self.DEFAULTS[self.REQUEST_TIMEOUT]
|
|
92
|
+
self.use_hsts = self.DEFAULTS[self.USE_HSTS]
|
|
93
|
+
self.hsts_max_age = self.DEFAULTS[self.HSTS_MAX_AGE]
|
|
94
|
+
|
|
95
|
+
# Authentication
|
|
96
|
+
self.basic_auth_user = None
|
|
97
|
+
self.basic_auth_password = None
|
|
98
|
+
|
|
99
|
+
def _load_config_file(self, config_file: Optional[str], service_name: Optional[str]):
|
|
100
|
+
"""Load configuration from config file if available"""
|
|
101
|
+
# Find config file
|
|
102
|
+
if not config_file:
|
|
103
|
+
config_file = ConfigLoader.find_config_file(service_name)
|
|
104
|
+
|
|
105
|
+
if not config_file:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Load config
|
|
109
|
+
config_loader = ConfigLoader([config_file])
|
|
110
|
+
if not config_loader.has_config():
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
logger.info("loading_config_from_file", file=config_file)
|
|
114
|
+
|
|
115
|
+
# Get security section
|
|
116
|
+
security_config = config_loader.get_section('security')
|
|
117
|
+
if not security_config:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Apply security settings (config file takes precedence)
|
|
121
|
+
if 'ssl_enabled' in security_config:
|
|
122
|
+
self.ssl_enabled = security_config['ssl_enabled']
|
|
123
|
+
|
|
124
|
+
if 'ssl_cert_path' in security_config:
|
|
125
|
+
self.ssl_cert_path = security_config['ssl_cert_path']
|
|
126
|
+
|
|
127
|
+
if 'ssl_key_path' in security_config:
|
|
128
|
+
self.ssl_key_path = security_config['ssl_key_path']
|
|
129
|
+
|
|
130
|
+
if 'domain' in security_config:
|
|
131
|
+
self.domain = security_config['domain']
|
|
132
|
+
|
|
133
|
+
if 'ssl_verify_mode' in security_config:
|
|
134
|
+
self.ssl_verify_mode = security_config['ssl_verify_mode']
|
|
135
|
+
|
|
136
|
+
# Additional settings
|
|
137
|
+
if 'allowed_hosts' in security_config:
|
|
138
|
+
self.allowed_hosts = self._parse_list(security_config['allowed_hosts'])
|
|
139
|
+
|
|
140
|
+
if 'cors_origins' in security_config:
|
|
141
|
+
self.cors_origins = self._parse_list(security_config['cors_origins'])
|
|
142
|
+
|
|
143
|
+
if 'max_request_size' in security_config:
|
|
144
|
+
self.max_request_size = int(security_config['max_request_size'])
|
|
145
|
+
|
|
146
|
+
if 'rate_limit' in security_config:
|
|
147
|
+
self.rate_limit = int(security_config['rate_limit'])
|
|
148
|
+
|
|
149
|
+
if 'request_timeout' in security_config:
|
|
150
|
+
self.request_timeout = int(security_config['request_timeout'])
|
|
151
|
+
|
|
152
|
+
if 'use_hsts' in security_config:
|
|
153
|
+
self.use_hsts = security_config['use_hsts']
|
|
154
|
+
|
|
155
|
+
if 'hsts_max_age' in security_config:
|
|
156
|
+
self.hsts_max_age = int(security_config['hsts_max_age'])
|
|
157
|
+
|
|
158
|
+
# Authentication from config
|
|
159
|
+
auth_config = security_config.get('auth', {})
|
|
160
|
+
if isinstance(auth_config, dict):
|
|
161
|
+
basic_auth = auth_config.get('basic', {})
|
|
162
|
+
if isinstance(basic_auth, dict):
|
|
163
|
+
if 'user' in basic_auth:
|
|
164
|
+
self.basic_auth_user = basic_auth['user']
|
|
165
|
+
if 'password' in basic_auth:
|
|
166
|
+
self.basic_auth_password = basic_auth['password']
|
|
167
|
+
|
|
168
|
+
def load_from_env(self):
|
|
169
|
+
"""Load configuration from environment variables"""
|
|
170
|
+
# SSL configuration
|
|
171
|
+
ssl_enabled_env = os.environ.get(self.SSL_ENABLED, '').lower()
|
|
172
|
+
self.ssl_enabled = ssl_enabled_env in ('true', '1', 'yes')
|
|
173
|
+
self.ssl_cert_path = os.environ.get(self.SSL_CERT_PATH)
|
|
174
|
+
self.ssl_key_path = os.environ.get(self.SSL_KEY_PATH)
|
|
175
|
+
self.domain = os.environ.get(self.SSL_DOMAIN)
|
|
176
|
+
self.ssl_verify_mode = os.environ.get(self.SSL_VERIFY_MODE, self.DEFAULTS[self.SSL_VERIFY_MODE])
|
|
177
|
+
|
|
178
|
+
# Additional security settings
|
|
179
|
+
self.allowed_hosts = self._parse_list(os.environ.get(self.ALLOWED_HOSTS, self.DEFAULTS[self.ALLOWED_HOSTS]))
|
|
180
|
+
self.cors_origins = self._parse_list(os.environ.get(self.CORS_ORIGINS, self.DEFAULTS[self.CORS_ORIGINS]))
|
|
181
|
+
self.max_request_size = int(os.environ.get(self.MAX_REQUEST_SIZE, self.DEFAULTS[self.MAX_REQUEST_SIZE]))
|
|
182
|
+
self.rate_limit = int(os.environ.get(self.RATE_LIMIT, self.DEFAULTS[self.RATE_LIMIT]))
|
|
183
|
+
self.request_timeout = int(os.environ.get(self.REQUEST_TIMEOUT, self.DEFAULTS[self.REQUEST_TIMEOUT]))
|
|
184
|
+
|
|
185
|
+
# HSTS settings
|
|
186
|
+
use_hsts_env = os.environ.get(self.USE_HSTS, '').lower()
|
|
187
|
+
self.use_hsts = use_hsts_env != 'false' if use_hsts_env else self.DEFAULTS[self.USE_HSTS]
|
|
188
|
+
self.hsts_max_age = int(os.environ.get(self.HSTS_MAX_AGE, self.DEFAULTS[self.HSTS_MAX_AGE]))
|
|
189
|
+
|
|
190
|
+
# Authentication
|
|
191
|
+
self.basic_auth_user = os.environ.get(self.BASIC_AUTH_USER)
|
|
192
|
+
self.basic_auth_password = os.environ.get(self.BASIC_AUTH_PASSWORD)
|
|
193
|
+
|
|
194
|
+
def _parse_list(self, value: Union[str, list]) -> list:
|
|
195
|
+
"""Parse comma-separated list from environment variable or list from config"""
|
|
196
|
+
if isinstance(value, list):
|
|
197
|
+
return value
|
|
198
|
+
if value == '*':
|
|
199
|
+
return ['*']
|
|
200
|
+
return [item.strip() for item in value.split(',') if item.strip()]
|
|
201
|
+
|
|
202
|
+
def validate_ssl_config(self) -> Tuple[bool, Optional[str]]:
|
|
203
|
+
"""
|
|
204
|
+
Validate SSL configuration.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (is_valid, error_message)
|
|
208
|
+
"""
|
|
209
|
+
if not self.ssl_enabled:
|
|
210
|
+
return True, None
|
|
211
|
+
|
|
212
|
+
if not self.ssl_cert_path:
|
|
213
|
+
return False, "SSL enabled but SWML_SSL_CERT_PATH not set"
|
|
214
|
+
|
|
215
|
+
if not self.ssl_key_path:
|
|
216
|
+
return False, "SSL enabled but SWML_SSL_KEY_PATH not set"
|
|
217
|
+
|
|
218
|
+
if not os.path.exists(self.ssl_cert_path):
|
|
219
|
+
return False, f"SSL certificate file not found: {self.ssl_cert_path}"
|
|
220
|
+
|
|
221
|
+
if not os.path.exists(self.ssl_key_path):
|
|
222
|
+
return False, f"SSL key file not found: {self.ssl_key_path}"
|
|
223
|
+
|
|
224
|
+
return True, None
|
|
225
|
+
|
|
226
|
+
def get_ssl_context_kwargs(self) -> Dict[str, Any]:
|
|
227
|
+
"""
|
|
228
|
+
Get SSL context kwargs for uvicorn.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary of SSL parameters for uvicorn
|
|
232
|
+
"""
|
|
233
|
+
if not self.ssl_enabled:
|
|
234
|
+
return {}
|
|
235
|
+
|
|
236
|
+
is_valid, error = self.validate_ssl_config()
|
|
237
|
+
if not is_valid:
|
|
238
|
+
logger.error("ssl_validation_failed", error=error)
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
'ssl_certfile': self.ssl_cert_path,
|
|
243
|
+
'ssl_keyfile': self.ssl_key_path,
|
|
244
|
+
# Additional SSL options can be added here
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def get_basic_auth(self) -> Tuple[str, str]:
|
|
248
|
+
"""
|
|
249
|
+
Get basic auth credentials, generating if not set.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Tuple of (username, password)
|
|
253
|
+
"""
|
|
254
|
+
username = self.basic_auth_user or "signalwire"
|
|
255
|
+
password = self.basic_auth_password or secrets.token_urlsafe(32)
|
|
256
|
+
|
|
257
|
+
return username, password
|
|
258
|
+
|
|
259
|
+
def get_security_headers(self, is_https: bool = False) -> Dict[str, str]:
|
|
260
|
+
"""
|
|
261
|
+
Get security headers to add to responses.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
is_https: Whether the connection is over HTTPS
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary of security headers
|
|
268
|
+
"""
|
|
269
|
+
headers = {
|
|
270
|
+
'X-Content-Type-Options': 'nosniff',
|
|
271
|
+
'X-Frame-Options': 'DENY',
|
|
272
|
+
'X-XSS-Protection': '1; mode=block',
|
|
273
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Add HSTS header if HTTPS and enabled
|
|
277
|
+
if is_https and self.use_hsts:
|
|
278
|
+
headers['Strict-Transport-Security'] = f'max-age={self.hsts_max_age}; includeSubDomains'
|
|
279
|
+
|
|
280
|
+
return headers
|
|
281
|
+
|
|
282
|
+
def should_allow_host(self, host: str) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Check if a host is allowed.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
host: The host to check
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if the host is allowed
|
|
291
|
+
"""
|
|
292
|
+
if '*' in self.allowed_hosts:
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
return host in self.allowed_hosts
|
|
296
|
+
|
|
297
|
+
def get_cors_config(self) -> Dict[str, Any]:
|
|
298
|
+
"""
|
|
299
|
+
Get CORS configuration for FastAPI.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Dictionary of CORS settings
|
|
303
|
+
"""
|
|
304
|
+
return {
|
|
305
|
+
'allow_origins': self.cors_origins,
|
|
306
|
+
'allow_credentials': True,
|
|
307
|
+
'allow_methods': ['*'],
|
|
308
|
+
'allow_headers': ['*'],
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def get_url_scheme(self) -> str:
|
|
312
|
+
"""Get the URL scheme based on SSL configuration"""
|
|
313
|
+
return 'https' if self.ssl_enabled else 'http'
|
|
314
|
+
|
|
315
|
+
def log_config(self, service_name: str):
|
|
316
|
+
"""Log the current security configuration"""
|
|
317
|
+
logger.info(
|
|
318
|
+
"security_config_loaded",
|
|
319
|
+
service=service_name,
|
|
320
|
+
ssl_enabled=self.ssl_enabled,
|
|
321
|
+
domain=self.domain,
|
|
322
|
+
allowed_hosts=self.allowed_hosts,
|
|
323
|
+
cors_origins=self.cors_origins,
|
|
324
|
+
max_request_size=self.max_request_size,
|
|
325
|
+
rate_limit=self.rate_limit,
|
|
326
|
+
use_hsts=self.use_hsts,
|
|
327
|
+
has_basic_auth=bool(self.basic_auth_user and self.basic_auth_password)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Global instance for easy access (backward compatibility)
|
|
332
|
+
# Services can create their own instances with specific config files
|
|
333
|
+
security_config = SecurityConfig()
|
|
@@ -53,6 +53,26 @@ class SkillBase(ABC):
|
|
|
53
53
|
"""Register SWAIG tools with the agent"""
|
|
54
54
|
pass
|
|
55
55
|
|
|
56
|
+
def define_tool(self, **kwargs) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Wrapper method that automatically includes swaig_fields when defining tools.
|
|
59
|
+
|
|
60
|
+
This method delegates to self.agent.define_tool() but automatically merges
|
|
61
|
+
any swaig_fields configured for this skill. Skills should use this method
|
|
62
|
+
instead of calling self.agent.define_tool() directly.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
**kwargs: All arguments supported by agent.define_tool()
|
|
66
|
+
(name, description, parameters, handler, etc.)
|
|
67
|
+
"""
|
|
68
|
+
# Merge swaig_fields with any explicitly passed fields
|
|
69
|
+
# Explicit fields take precedence over swaig_fields
|
|
70
|
+
merged_kwargs = dict(self.swaig_fields)
|
|
71
|
+
merged_kwargs.update(kwargs)
|
|
72
|
+
|
|
73
|
+
# Call the agent's define_tool with merged arguments
|
|
74
|
+
return self.agent.define_tool(**merged_kwargs)
|
|
75
|
+
|
|
56
76
|
|
|
57
77
|
|
|
58
78
|
def get_hints(self) -> List[str]:
|
|
@@ -114,4 +134,67 @@ class SkillBase(ABC):
|
|
|
114
134
|
|
|
115
135
|
# For multi-instance skills, create key from skill name + tool name
|
|
116
136
|
tool_name = self.params.get('tool_name', self.SKILL_NAME)
|
|
117
|
-
return f"{self.SKILL_NAME}_{tool_name}"
|
|
137
|
+
return f"{self.SKILL_NAME}_{tool_name}"
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
|
141
|
+
"""
|
|
142
|
+
Get the parameter schema for this skill
|
|
143
|
+
|
|
144
|
+
This method returns metadata about all parameters the skill accepts,
|
|
145
|
+
including their types, descriptions, default values, and whether they
|
|
146
|
+
are required or should be hidden (e.g., API keys).
|
|
147
|
+
|
|
148
|
+
The base implementation provides common parameters available to all skills.
|
|
149
|
+
Subclasses should override this method and merge their specific parameters
|
|
150
|
+
with the base schema.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dict[str, Dict[str, Any]]: Parameter schema where keys are parameter names
|
|
154
|
+
and values are dictionaries containing:
|
|
155
|
+
- type: Parameter type ("string", "integer", "number", "boolean", "object", "array")
|
|
156
|
+
- description: Human-readable description
|
|
157
|
+
- default: Default value if not provided (optional)
|
|
158
|
+
- required: Whether the parameter is required (default: False)
|
|
159
|
+
- hidden: Whether to hide this field in UIs (for secrets/keys)
|
|
160
|
+
- env_var: Environment variable that can provide this value (optional)
|
|
161
|
+
- enum: List of allowed values (optional)
|
|
162
|
+
- min/max: Minimum/maximum values for numeric types (optional)
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
{
|
|
166
|
+
"tool_name": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "Name for the tool when using multiple instances",
|
|
169
|
+
"default": "my_skill",
|
|
170
|
+
"required": False
|
|
171
|
+
},
|
|
172
|
+
"api_key": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "API key for the service",
|
|
175
|
+
"required": True,
|
|
176
|
+
"hidden": True,
|
|
177
|
+
"env_var": "MY_API_KEY"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
"""
|
|
181
|
+
schema = {}
|
|
182
|
+
|
|
183
|
+
# Add swaig_fields parameter (available to all skills)
|
|
184
|
+
schema["swaig_fields"] = {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"description": "Additional SWAIG function metadata to merge into tool definitions",
|
|
187
|
+
"default": {},
|
|
188
|
+
"required": False
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add tool_name for multi-instance skills
|
|
192
|
+
if cls.SUPPORTS_MULTIPLE_INSTANCES:
|
|
193
|
+
schema["tool_name"] = {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Custom name for this skill instance (for multiple instances)",
|
|
196
|
+
"default": cls.SKILL_NAME,
|
|
197
|
+
"required": False
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return schema
|
|
@@ -8,7 +8,7 @@ See LICENSE file in the project root for full license information.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from typing import Dict, List, Type, Any, Optional
|
|
11
|
-
import
|
|
11
|
+
from signalwire_agents.core.logging_config import get_logger
|
|
12
12
|
from signalwire_agents.core.skill_base import SkillBase
|
|
13
13
|
|
|
14
14
|
class SkillManager:
|
|
@@ -17,7 +17,7 @@ class SkillManager:
|
|
|
17
17
|
def __init__(self, agent):
|
|
18
18
|
self.agent = agent
|
|
19
19
|
self.loaded_skills: Dict[str, SkillBase] = {}
|
|
20
|
-
self.logger =
|
|
20
|
+
self.logger = get_logger("skill_manager")
|
|
21
21
|
|
|
22
22
|
def load_skill(self, skill_name: str, skill_class: Type[SkillBase] = None, params: Optional[Dict[str, Any]] = None) -> tuple[bool, str]:
|
|
23
23
|
"""
|
|
@@ -45,6 +45,53 @@ class SkillManager:
|
|
|
45
45
|
self.logger.error(error_msg)
|
|
46
46
|
return False, error_msg
|
|
47
47
|
|
|
48
|
+
# Validate that the skill has a proper parameter schema
|
|
49
|
+
if not hasattr(skill_class, 'get_parameter_schema') or not callable(getattr(skill_class, 'get_parameter_schema')):
|
|
50
|
+
error_msg = f"Skill '{skill_name}' must have get_parameter_schema() classmethod"
|
|
51
|
+
self.logger.error(error_msg)
|
|
52
|
+
return False, error_msg
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Validate the parameter schema
|
|
56
|
+
schema = skill_class.get_parameter_schema()
|
|
57
|
+
if not isinstance(schema, dict):
|
|
58
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() must return a dictionary"
|
|
59
|
+
self.logger.error(error_msg)
|
|
60
|
+
return False, error_msg
|
|
61
|
+
|
|
62
|
+
# Ensure it's not an empty schema
|
|
63
|
+
if not schema:
|
|
64
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() returned empty dictionary"
|
|
65
|
+
self.logger.error(error_msg)
|
|
66
|
+
return False, error_msg
|
|
67
|
+
|
|
68
|
+
# Check if the skill has overridden the method
|
|
69
|
+
from signalwire_agents.core.skill_base import SkillBase
|
|
70
|
+
skill_method = getattr(skill_class, 'get_parameter_schema', None)
|
|
71
|
+
base_method = getattr(SkillBase, 'get_parameter_schema', None)
|
|
72
|
+
|
|
73
|
+
if skill_method and base_method:
|
|
74
|
+
# For class methods, check the underlying function
|
|
75
|
+
skill_func = skill_method.__func__ if hasattr(skill_method, '__func__') else skill_method
|
|
76
|
+
base_func = base_method.__func__ if hasattr(base_method, '__func__') else base_method
|
|
77
|
+
|
|
78
|
+
if skill_func is base_func:
|
|
79
|
+
# Get base schema to check if skill added any parameters
|
|
80
|
+
base_schema = SkillBase.get_parameter_schema()
|
|
81
|
+
if set(schema.keys()) == set(base_schema.keys()):
|
|
82
|
+
error_msg = f"Skill '{skill_name}' must override get_parameter_schema() to define its specific parameters"
|
|
83
|
+
self.logger.error(error_msg)
|
|
84
|
+
return False, error_msg
|
|
85
|
+
|
|
86
|
+
except AttributeError as e:
|
|
87
|
+
error_msg = f"Skill '{skill_name}' must properly implement get_parameter_schema() classmethod"
|
|
88
|
+
self.logger.error(error_msg)
|
|
89
|
+
return False, error_msg
|
|
90
|
+
except Exception as e:
|
|
91
|
+
error_msg = f"Skill '{skill_name}'.get_parameter_schema() failed: {e}"
|
|
92
|
+
self.logger.error(error_msg)
|
|
93
|
+
return False, error_msg
|
|
94
|
+
|
|
48
95
|
try:
|
|
49
96
|
# Create skill instance with parameters to get the instance key
|
|
50
97
|
skill_instance = skill_class(self.agent, params)
|
|
@@ -111,6 +158,16 @@ class SkillManager:
|
|
|
111
158
|
self.logger.info(f"Successfully loaded skill instance '{instance_key}' (skill: '{skill_name}')")
|
|
112
159
|
return True, ""
|
|
113
160
|
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
# Check if this is a duplicate tool registration (expected during agent cloning)
|
|
163
|
+
if "already exists" in str(e):
|
|
164
|
+
debug_msg = f"Skill '{skill_name}' already loaded, skipping duplicate registration"
|
|
165
|
+
self.logger.debug(debug_msg)
|
|
166
|
+
return True, "" # Not an error, skill is already available
|
|
167
|
+
else:
|
|
168
|
+
error_msg = f"Error loading skill '{skill_name}': {e}"
|
|
169
|
+
self.logger.error(error_msg)
|
|
170
|
+
return False, error_msg
|
|
114
171
|
except Exception as e:
|
|
115
172
|
error_msg = f"Error loading skill '{skill_name}': {e}"
|
|
116
173
|
self.logger.error(error_msg)
|
|
@@ -134,13 +191,6 @@ class SkillManager:
|
|
|
134
191
|
if skill_identifier in self.loaded_skills:
|
|
135
192
|
instance_key = skill_identifier
|
|
136
193
|
skill_instance = self.loaded_skills[skill_identifier]
|
|
137
|
-
else:
|
|
138
|
-
# Try to find by skill name (for backwards compatibility)
|
|
139
|
-
for key, instance in self.loaded_skills.items():
|
|
140
|
-
if instance.SKILL_NAME == skill_identifier:
|
|
141
|
-
instance_key = key
|
|
142
|
-
skill_instance = instance
|
|
143
|
-
break
|
|
144
194
|
|
|
145
195
|
if skill_instance is None:
|
|
146
196
|
self.logger.warning(f"Skill '{skill_identifier}' is not loaded")
|
|
@@ -173,11 +223,6 @@ class SkillManager:
|
|
|
173
223
|
if skill_identifier in self.loaded_skills:
|
|
174
224
|
return True
|
|
175
225
|
|
|
176
|
-
# Try to find by skill name (for backwards compatibility)
|
|
177
|
-
for instance in self.loaded_skills.values():
|
|
178
|
-
if instance.SKILL_NAME == skill_identifier:
|
|
179
|
-
return True
|
|
180
|
-
|
|
181
226
|
return False
|
|
182
227
|
|
|
183
228
|
def get_skill(self, skill_identifier: str) -> Optional[SkillBase]:
|
|
@@ -194,9 +239,6 @@ class SkillManager:
|
|
|
194
239
|
if skill_identifier in self.loaded_skills:
|
|
195
240
|
return self.loaded_skills[skill_identifier]
|
|
196
241
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return instance
|
|
201
|
-
|
|
202
|
-
return None
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
@@ -30,7 +30,10 @@ class SWAIGFunction:
|
|
|
30
30
|
parameters: Dict[str, Dict] = None,
|
|
31
31
|
secure: bool = False,
|
|
32
32
|
fillers: Optional[Dict[str, List[str]]] = None,
|
|
33
|
+
wait_file: Optional[str] = None,
|
|
34
|
+
wait_file_loops: Optional[int] = None,
|
|
33
35
|
webhook_url: Optional[str] = None,
|
|
36
|
+
required: Optional[List[str]] = None,
|
|
34
37
|
**extra_swaig_fields
|
|
35
38
|
):
|
|
36
39
|
"""
|
|
@@ -42,8 +45,11 @@ class SWAIGFunction:
|
|
|
42
45
|
description: Human-readable description of the function
|
|
43
46
|
parameters: Dictionary of parameters, keys are parameter names, values are param definitions
|
|
44
47
|
secure: Whether this function requires token validation
|
|
45
|
-
fillers: Optional dictionary of filler phrases by language code
|
|
48
|
+
fillers: Optional dictionary of filler phrases by language code (deprecated, use wait_file)
|
|
49
|
+
wait_file: Optional URL to audio file to play while function executes
|
|
50
|
+
wait_file_loops: Optional number of times to loop the wait_file
|
|
46
51
|
webhook_url: Optional external webhook URL to use instead of local handling
|
|
52
|
+
required: Optional list of required parameter names
|
|
47
53
|
**extra_swaig_fields: Additional SWAIG fields to include in function definition
|
|
48
54
|
"""
|
|
49
55
|
self.name = name
|
|
@@ -51,8 +57,11 @@ class SWAIGFunction:
|
|
|
51
57
|
self.description = description
|
|
52
58
|
self.parameters = parameters or {}
|
|
53
59
|
self.secure = secure
|
|
54
|
-
self.fillers = fillers
|
|
60
|
+
self.fillers = fillers # Text phrases to say while processing
|
|
61
|
+
self.wait_file = wait_file # URL to audio/video file to play while waiting
|
|
62
|
+
self.wait_file_loops = wait_file_loops
|
|
55
63
|
self.webhook_url = webhook_url
|
|
64
|
+
self.required = required or []
|
|
56
65
|
self.extra_swaig_fields = extra_swaig_fields
|
|
57
66
|
|
|
58
67
|
# Mark as external if webhook_url is provided
|
|
@@ -73,11 +82,17 @@ class SWAIGFunction:
|
|
|
73
82
|
return self.parameters
|
|
74
83
|
|
|
75
84
|
# Otherwise, wrap the parameters in the expected structure
|
|
76
|
-
|
|
85
|
+
result = {
|
|
77
86
|
"type": "object",
|
|
78
87
|
"properties": self.parameters
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
# Add required fields if specified
|
|
91
|
+
if self.required:
|
|
92
|
+
result["required"] = self.required
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
81
96
|
def __call__(self, *args, **kwargs):
|
|
82
97
|
"""
|
|
83
98
|
Call the underlying handler function
|
|
@@ -179,5 +194,3 @@ class SWAIGFunction:
|
|
|
179
194
|
|
|
180
195
|
return function_def
|
|
181
196
|
|
|
182
|
-
# Add an alias for backward compatibility
|
|
183
|
-
SwaigFunction = SWAIGFunction
|