dtSpark 1.0.4__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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- dtspark-1.0.4.dist-info/top_level.txt +1 -0
dtSpark/web/session.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session manager for web interface.
|
|
3
|
+
|
|
4
|
+
Implements single-session authentication with configurable timeout.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import secrets
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages user sessions for the web interface.
|
|
19
|
+
|
|
20
|
+
Features:
|
|
21
|
+
- Single active session at a time (new login invalidates old session)
|
|
22
|
+
- Configurable inactivity timeout
|
|
23
|
+
- Secure session ID generation
|
|
24
|
+
- Automatic session expiration
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, timeout_minutes: int = 30):
|
|
28
|
+
"""
|
|
29
|
+
Initialise the session manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
timeout_minutes: Minutes of inactivity before session expires
|
|
33
|
+
"""
|
|
34
|
+
self._timeout_minutes = timeout_minutes
|
|
35
|
+
self._session_id: Optional[str] = None
|
|
36
|
+
self._last_activity: Optional[datetime] = None
|
|
37
|
+
self._created_at: Optional[datetime] = None
|
|
38
|
+
|
|
39
|
+
def create_session(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Create a new session.
|
|
42
|
+
|
|
43
|
+
If a session already exists, it is invalidated and replaced.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The new session ID
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
Session IDs are cryptographically secure random strings.
|
|
50
|
+
"""
|
|
51
|
+
# Generate secure random session ID (32 bytes = 64 hex characters)
|
|
52
|
+
self._session_id = secrets.token_hex(32)
|
|
53
|
+
self._created_at = datetime.now()
|
|
54
|
+
self._last_activity = datetime.now()
|
|
55
|
+
|
|
56
|
+
return self._session_id
|
|
57
|
+
|
|
58
|
+
def validate_session(self, session_id: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Validate a session ID and check if it's expired.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session_id: The session ID to validate
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if session is valid and not expired, False otherwise
|
|
67
|
+
|
|
68
|
+
Note:
|
|
69
|
+
If validation succeeds, the last activity timestamp is updated.
|
|
70
|
+
"""
|
|
71
|
+
# Check if session exists
|
|
72
|
+
if self._session_id is None:
|
|
73
|
+
logger.debug("Session validation failed: no active session exists")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Check if session ID matches
|
|
77
|
+
if session_id != self._session_id:
|
|
78
|
+
logger.debug(f"Session validation failed: ID mismatch (provided: {session_id[:8]}..., "
|
|
79
|
+
f"expected: {self._session_id[:8]}...)")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# Check if session has expired
|
|
83
|
+
if self._is_expired():
|
|
84
|
+
logger.info(f"Session expired after {self._timeout_minutes} minutes of inactivity")
|
|
85
|
+
self._invalidate_session()
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
# Update last activity timestamp
|
|
89
|
+
self._last_activity = datetime.now()
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
def invalidate_session(self):
|
|
94
|
+
"""
|
|
95
|
+
Explicitly invalidate the current session.
|
|
96
|
+
|
|
97
|
+
Used when user logs out or when session is replaced.
|
|
98
|
+
"""
|
|
99
|
+
self._invalidate_session()
|
|
100
|
+
|
|
101
|
+
def get_session_info(self) -> Optional[dict]:
|
|
102
|
+
"""
|
|
103
|
+
Get information about the current session.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Dictionary with session information, or None if no active session
|
|
107
|
+
"""
|
|
108
|
+
if self._session_id is None:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
'session_id': self._session_id,
|
|
113
|
+
'created_at': self._created_at,
|
|
114
|
+
'last_activity': self._last_activity,
|
|
115
|
+
'timeout_minutes': self._timeout_minutes,
|
|
116
|
+
'is_expired': self._is_expired(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def update_timeout(self, timeout_minutes: int):
|
|
120
|
+
"""
|
|
121
|
+
Update the session timeout duration.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
timeout_minutes: New timeout duration in minutes
|
|
125
|
+
"""
|
|
126
|
+
self._timeout_minutes = timeout_minutes
|
|
127
|
+
|
|
128
|
+
def get_remaining_time(self) -> Optional[timedelta]:
|
|
129
|
+
"""
|
|
130
|
+
Get the remaining time before session expires.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Timedelta representing remaining time, or None if no active session
|
|
134
|
+
"""
|
|
135
|
+
if self._session_id is None or self._last_activity is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
expiry_time = self._last_activity + timedelta(minutes=self._timeout_minutes)
|
|
139
|
+
remaining = expiry_time - datetime.now()
|
|
140
|
+
|
|
141
|
+
return remaining if remaining.total_seconds() > 0 else timedelta(0)
|
|
142
|
+
|
|
143
|
+
def _is_expired(self) -> bool:
|
|
144
|
+
"""
|
|
145
|
+
Check if the current session has expired.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if session is expired, False otherwise
|
|
149
|
+
|
|
150
|
+
Note:
|
|
151
|
+
If timeout_minutes is 0 or negative, session never expires.
|
|
152
|
+
"""
|
|
153
|
+
if self._last_activity is None:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
# Timeout of 0 or less means session never expires
|
|
157
|
+
if self._timeout_minutes <= 0:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
expiry_time = self._last_activity + timedelta(minutes=self._timeout_minutes)
|
|
161
|
+
return datetime.now() > expiry_time
|
|
162
|
+
|
|
163
|
+
def _invalidate_session(self):
|
|
164
|
+
"""Internal method to clear session data."""
|
|
165
|
+
self._session_id = None
|
|
166
|
+
self._created_at = None
|
|
167
|
+
self._last_activity = None
|
dtSpark/web/ssl_utils.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSL certificate utilities for the web interface.
|
|
3
|
+
|
|
4
|
+
Handles generation of self-signed certificates for HTTPS support.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os.path
|
|
10
|
+
import socket
|
|
11
|
+
import ipaddress
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import Tuple, Optional
|
|
15
|
+
|
|
16
|
+
from dtPyAppFramework.paths import ApplicationPaths
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_self_signed_certificate(
|
|
22
|
+
cert_file: str,
|
|
23
|
+
key_file: str,
|
|
24
|
+
hostname: str = "localhost",
|
|
25
|
+
validity_days: int = 365,
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Generate a self-signed SSL certificate and private key.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
cert_file: Path where the certificate will be saved
|
|
32
|
+
key_file: Path where the private key will be saved
|
|
33
|
+
hostname: Hostname for the certificate (default: localhost)
|
|
34
|
+
validity_days: Number of days the certificate is valid (default: 365)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if generation succeeded, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
from cryptography import x509
|
|
41
|
+
from cryptography.x509.oid import NameOID
|
|
42
|
+
from cryptography.hazmat.primitives import hashes
|
|
43
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
44
|
+
from cryptography.hazmat.primitives import serialization
|
|
45
|
+
except ImportError:
|
|
46
|
+
logger.error("cryptography package is required for SSL certificate generation")
|
|
47
|
+
logger.error("Install it with: pip install cryptography")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Generate private key
|
|
52
|
+
logger.info("Generating RSA private key...")
|
|
53
|
+
private_key = rsa.generate_private_key(
|
|
54
|
+
public_exponent=65537,
|
|
55
|
+
key_size=2048,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Create certificate subject and issuer (same for self-signed)
|
|
59
|
+
subject = issuer = x509.Name([
|
|
60
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "AU"),
|
|
61
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "New South Wales"),
|
|
62
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, "Sydney"),
|
|
63
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Digital-Thought"),
|
|
64
|
+
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
# Build certificate
|
|
68
|
+
logger.info(f"Generating self-signed certificate for {hostname}...")
|
|
69
|
+
cert = (
|
|
70
|
+
x509.CertificateBuilder()
|
|
71
|
+
.subject_name(subject)
|
|
72
|
+
.issuer_name(issuer)
|
|
73
|
+
.public_key(private_key.public_key())
|
|
74
|
+
.serial_number(x509.random_serial_number())
|
|
75
|
+
.not_valid_before(datetime.utcnow())
|
|
76
|
+
.not_valid_after(datetime.utcnow() + timedelta(days=validity_days))
|
|
77
|
+
.add_extension(
|
|
78
|
+
x509.SubjectAlternativeName([
|
|
79
|
+
x509.DNSName(hostname),
|
|
80
|
+
x509.DNSName("localhost"),
|
|
81
|
+
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
|
|
82
|
+
]),
|
|
83
|
+
critical=False,
|
|
84
|
+
)
|
|
85
|
+
.sign(private_key, hashes.SHA256())
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Ensure directory exists
|
|
89
|
+
cert_path = Path(cert_file)
|
|
90
|
+
key_path = Path(key_file)
|
|
91
|
+
cert_path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
# Write private key
|
|
95
|
+
logger.info(f"Writing private key to {key_file}")
|
|
96
|
+
with open(key_file, "wb") as f:
|
|
97
|
+
f.write(
|
|
98
|
+
private_key.private_bytes(
|
|
99
|
+
encoding=serialization.Encoding.PEM,
|
|
100
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
101
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Write certificate
|
|
106
|
+
logger.info(f"Writing certificate to {cert_file}")
|
|
107
|
+
with open(cert_file, "wb") as f:
|
|
108
|
+
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
109
|
+
|
|
110
|
+
logger.info(f"Self-signed certificate generated successfully (valid for {validity_days} days)")
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to generate self-signed certificate: {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def check_certificate_files(cert_file: str, key_file: str) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Check if certificate and key files exist and are readable.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
cert_file: Path to certificate file
|
|
124
|
+
key_file: Path to private key file
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if both files exist and are readable, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
cert_path = Path(cert_file)
|
|
130
|
+
key_path = Path(key_file)
|
|
131
|
+
|
|
132
|
+
if not cert_path.exists():
|
|
133
|
+
logger.debug(f"Certificate file not found: {cert_file}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
if not key_path.exists():
|
|
137
|
+
logger.debug(f"Key file not found: {key_file}")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# Try to read files
|
|
141
|
+
try:
|
|
142
|
+
with open(cert_file, "r") as f:
|
|
143
|
+
cert_content = f.read()
|
|
144
|
+
with open(key_file, "r") as f:
|
|
145
|
+
key_content = f.read()
|
|
146
|
+
|
|
147
|
+
if not cert_content or not key_content:
|
|
148
|
+
logger.warning("Certificate or key file is empty")
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
return True
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to read certificate files: {e}")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def setup_ssl_certificates(
|
|
158
|
+
cert_file: str,
|
|
159
|
+
key_file: str,
|
|
160
|
+
auto_generate: bool = True,
|
|
161
|
+
hostname: str = "localhost",
|
|
162
|
+
) -> Tuple[bool, Optional[str], Optional[str]]:
|
|
163
|
+
"""
|
|
164
|
+
Setup SSL certificates, generating them if needed.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
cert_file: Path to certificate file
|
|
168
|
+
key_file: Path to private key file
|
|
169
|
+
auto_generate: Whether to automatically generate if files don't exist
|
|
170
|
+
hostname: Hostname for the certificate
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Tuple of (success, cert_file_path, key_file_path)
|
|
174
|
+
Returns (False, None, None) if setup failed
|
|
175
|
+
"""
|
|
176
|
+
# Convert to absolute paths
|
|
177
|
+
cert_file = str(Path(ApplicationPaths().usr_data_root_path, cert_file).resolve())
|
|
178
|
+
key_file = str(Path(ApplicationPaths().usr_data_root_path, key_file).resolve())
|
|
179
|
+
|
|
180
|
+
# Check if files exist
|
|
181
|
+
if check_certificate_files(cert_file, key_file):
|
|
182
|
+
logger.info("SSL certificate files found and verified")
|
|
183
|
+
return True, cert_file, key_file
|
|
184
|
+
|
|
185
|
+
# Generate if auto-generate is enabled
|
|
186
|
+
if auto_generate:
|
|
187
|
+
logger.info("SSL certificate files not found, generating self-signed certificate...")
|
|
188
|
+
if generate_self_signed_certificate(cert_file, key_file, hostname):
|
|
189
|
+
return True, cert_file, key_file
|
|
190
|
+
else:
|
|
191
|
+
logger.error("Failed to generate SSL certificates")
|
|
192
|
+
return False, None, None
|
|
193
|
+
else:
|
|
194
|
+
logger.error("SSL certificate files not found and auto-generation is disabled")
|
|
195
|
+
return False, None, None
|