GT AI OS Community Edition v2.0.33

Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
"""
Assistant Builder Service for GT 2.0
Manages assistant creation, deployment, and lifecycle.
Integrates with template library and file-based storage.
"""
import os
import json
import stat
from typing import List, Optional, Dict, Any
from datetime import datetime
from pathlib import Path
import logging
from app.models.assistant_template import (
AssistantTemplate, AssistantInstance, AssistantBuilder,
AssistantType, PersonalityConfig, ResourcePreferences, MemorySettings,
AssistantTemplateLibrary, BUILTIN_TEMPLATES
)
from app.models.access_group import AccessGroup
from app.core.security import verify_capability_token
from app.services.access_controller import AccessController
logger = logging.getLogger(__name__)
class AssistantBuilderService:
"""
Service for building and managing assistants
Handles both template-based and custom assistant creation
"""
def __init__(self, tenant_domain: str, resource_cluster_url: str = "http://resource-cluster:8004"):
self.tenant_domain = tenant_domain
self.base_path = Path(f"/data/{tenant_domain}/assistants")
self.template_library = AssistantTemplateLibrary(resource_cluster_url)
self.access_controller = AccessController(tenant_domain)
self._ensure_directories()
def _ensure_directories(self):
"""Ensure assistant directories exist with proper permissions"""
self.base_path.mkdir(parents=True, exist_ok=True)
os.chmod(self.base_path, stat.S_IRWXU) # 700
# Create subdirectories
for subdir in ["templates", "instances", "shared"]:
path = self.base_path / subdir
path.mkdir(exist_ok=True)
os.chmod(path, stat.S_IRWXU) # 700
async def create_from_template(
self,
template_id: str,
user_id: str,
instance_name: str,
customizations: Optional[Dict[str, Any]] = None,
capability_token: str = None
) -> AssistantInstance:
"""
Create assistant instance from template
Args:
template_id: Template to use
user_id: User creating the assistant
instance_name: Name for the instance
customizations: Optional customizations
capability_token: JWT capability token
Returns:
Created assistant instance
"""
# Verify capability token
if capability_token:
token_data = verify_capability_token(capability_token)
if not token_data or token_data.get("tenant_id") != self.tenant_domain:
raise PermissionError("Invalid capability token")
# Deploy from template
instance = await self.template_library.deploy_template(
template_id=template_id,
user_id=user_id,
instance_name=instance_name,
tenant_domain=self.tenant_domain,
customizations=customizations
)
# Create file storage
await self._create_assistant_files(instance)
# Save to database (would be SQLite in production)
await self._save_assistant(instance)
logger.info(f"Created assistant {instance.id} from template {template_id} for {user_id}")
return instance
async def create_custom(
self,
builder_config: AssistantBuilder,
user_id: str,
capability_token: str = None
) -> AssistantInstance:
"""
Create custom assistant from builder configuration
Args:
builder_config: Custom assistant configuration
user_id: User creating the assistant
capability_token: JWT capability token
Returns:
Created assistant instance
"""
# Verify capability token
if capability_token:
token_data = verify_capability_token(capability_token)
if not token_data or token_data.get("tenant_id") != self.tenant_domain:
raise PermissionError("Invalid capability token")
# Check if user has required capabilities
user_capabilities = token_data.get("capabilities", [])
for required_cap in builder_config.requested_capabilities:
if not any(required_cap in cap.get("resource", "") for cap in user_capabilities):
raise PermissionError(f"Missing capability: {required_cap}")
# Build instance
instance = builder_config.build_instance(user_id, self.tenant_domain)
# Create file storage
await self._create_assistant_files(instance)
# Save to database
await self._save_assistant(instance)
logger.info(f"Created custom assistant {instance.id} for {user_id}")
return instance
async def get_assistant(
self,
assistant_id: str,
user_id: str
) -> Optional[AssistantInstance]:
"""
Get assistant instance by ID
Args:
assistant_id: Assistant ID
user_id: User requesting the assistant
Returns:
Assistant instance if found and accessible
"""
# Load assistant
instance = await self._load_assistant(assistant_id)
if not instance:
return None
# Check access permission
allowed, _ = await self.access_controller.check_permission(
user_id, instance, "read"
)
if not allowed:
return None
return instance
async def list_user_assistants(
self,
user_id: str,
include_shared: bool = True
) -> List[AssistantInstance]:
"""
List all assistants accessible to user
Args:
user_id: User to list assistants for
include_shared: Include team/org shared assistants
Returns:
List of accessible assistants
"""
assistants = []
# Get owned assistants
owned = await self._get_owned_assistants(user_id)
assistants.extend(owned)
# Get shared assistants if requested
if include_shared:
shared = await self._get_shared_assistants(user_id)
assistants.extend(shared)
return assistants
async def update_assistant(
self,
assistant_id: str,
user_id: str,
updates: Dict[str, Any]
) -> AssistantInstance:
"""
Update assistant configuration
Args:
assistant_id: Assistant to update
user_id: User requesting update
updates: Configuration updates
Returns:
Updated assistant instance
"""
# Load assistant
instance = await self._load_assistant(assistant_id)
if not instance:
raise ValueError(f"Assistant not found: {assistant_id}")
# Check permission
if instance.owner_id != user_id:
raise PermissionError("Only owner can update assistant")
# Apply updates
if "personality" in updates:
instance.personality_config = PersonalityConfig(**updates["personality"])
if "resources" in updates:
instance.resource_preferences = ResourcePreferences(**updates["resources"])
if "memory" in updates:
instance.memory_settings = MemorySettings(**updates["memory"])
if "system_prompt" in updates:
instance.system_prompt = updates["system_prompt"]
instance.updated_at = datetime.utcnow()
# Save changes
await self._save_assistant(instance)
await self._update_assistant_files(instance)
logger.info(f"Updated assistant {assistant_id} by {user_id}")
return instance
async def share_assistant(
self,
assistant_id: str,
user_id: str,
access_group: AccessGroup,
team_members: Optional[List[str]] = None
) -> AssistantInstance:
"""
Share assistant with team or organization
Args:
assistant_id: Assistant to share
user_id: User sharing (must be owner)
access_group: New access level
team_members: Team members if team access
Returns:
Updated assistant instance
"""
# Load assistant
instance = await self._load_assistant(assistant_id)
if not instance:
raise ValueError(f"Assistant not found: {assistant_id}")
# Check ownership
if instance.owner_id != user_id:
raise PermissionError("Only owner can share assistant")
# Update access
instance.access_group = access_group
if access_group == AccessGroup.TEAM:
instance.team_members = team_members or []
else:
instance.team_members = []
instance.updated_at = datetime.utcnow()
# Save changes
await self._save_assistant(instance)
logger.info(f"Shared assistant {assistant_id} with {access_group.value} by {user_id}")
return instance
async def delete_assistant(
self,
assistant_id: str,
user_id: str
) -> bool:
"""
Delete assistant and its files
Args:
assistant_id: Assistant to delete
user_id: User requesting deletion
Returns:
True if deleted
"""
# Load assistant
instance = await self._load_assistant(assistant_id)
if not instance:
return False
# Check ownership
if instance.owner_id != user_id:
raise PermissionError("Only owner can delete assistant")
# Delete files
await self._delete_assistant_files(instance)
# Delete from database
await self._delete_assistant_record(assistant_id)
logger.info(f"Deleted assistant {assistant_id} by {user_id}")
return True
async def get_assistant_statistics(
self,
assistant_id: str,
user_id: str
) -> Dict[str, Any]:
"""
Get usage statistics for assistant
Args:
assistant_id: Assistant ID
user_id: User requesting stats
Returns:
Statistics dictionary
"""
# Load assistant
instance = await self.get_assistant(assistant_id, user_id)
if not instance:
raise ValueError(f"Assistant not found or not accessible: {assistant_id}")
return {
"assistant_id": assistant_id,
"name": instance.name,
"created_at": instance.created_at.isoformat(),
"last_used": instance.last_used.isoformat() if instance.last_used else None,
"conversation_count": instance.conversation_count,
"total_messages": instance.total_messages,
"total_tokens_used": instance.total_tokens_used,
"access_group": instance.access_group.value,
"team_members_count": len(instance.team_members),
"linked_datasets_count": len(instance.linked_datasets),
"linked_tools_count": len(instance.linked_tools)
}
async def _create_assistant_files(self, instance: AssistantInstance):
"""Create file structure for assistant"""
# Get file paths
file_structure = instance.get_file_structure()
# Create directories
for key, path in file_structure.items():
if key in ["memory", "resources"]:
# These are directories
Path(path).mkdir(parents=True, exist_ok=True)
os.chmod(Path(path), stat.S_IRWXU) # 700
else:
# These are files
parent = Path(path).parent
parent.mkdir(parents=True, exist_ok=True)
os.chmod(parent, stat.S_IRWXU) # 700
# Save configuration
config_path = Path(file_structure["config"])
config_data = {
"id": instance.id,
"name": instance.name,
"template_id": instance.template_id,
"personality": instance.personality_config.model_dump(),
"resources": instance.resource_preferences.model_dump(),
"memory": instance.memory_settings.model_dump(),
"created_at": instance.created_at.isoformat(),
"updated_at": instance.updated_at.isoformat()
}
with open(config_path, 'w') as f:
json.dump(config_data, f, indent=2)
os.chmod(config_path, stat.S_IRUSR | stat.S_IWUSR) # 600
# Save prompt
prompt_path = Path(file_structure["prompt"])
with open(prompt_path, 'w') as f:
f.write(instance.system_prompt)
os.chmod(prompt_path, stat.S_IRUSR | stat.S_IWUSR) # 600
# Save capabilities
capabilities_path = Path(file_structure["capabilities"])
with open(capabilities_path, 'w') as f:
json.dump(instance.capabilities, f, indent=2)
os.chmod(capabilities_path, stat.S_IRUSR | stat.S_IWUSR) # 600
# Update instance with file paths
instance.config_file_path = str(config_path)
instance.memory_file_path = str(Path(file_structure["memory"]))
async def _update_assistant_files(self, instance: AssistantInstance):
"""Update assistant files with current configuration"""
if instance.config_file_path:
config_data = {
"id": instance.id,
"name": instance.name,
"template_id": instance.template_id,
"personality": instance.personality_config.model_dump(),
"resources": instance.resource_preferences.model_dump(),
"memory": instance.memory_settings.model_dump(),
"created_at": instance.created_at.isoformat(),
"updated_at": instance.updated_at.isoformat()
}
with open(instance.config_file_path, 'w') as f:
json.dump(config_data, f, indent=2)
async def _delete_assistant_files(self, instance: AssistantInstance):
"""Delete assistant file structure"""
file_structure = instance.get_file_structure()
base_dir = Path(file_structure["config"]).parent
if base_dir.exists():
import shutil
shutil.rmtree(base_dir)
logger.info(f"Deleted assistant files at {base_dir}")
async def _save_assistant(self, instance: AssistantInstance):
"""Save assistant to database (SQLite in production)"""
# This would save to SQLite database
# For now, we'll save to a JSON file as placeholder
db_file = self.base_path / "instances" / f"{instance.id}.json"
with open(db_file, 'w') as f:
json.dump(instance.model_dump(mode='json'), f, indent=2, default=str)
os.chmod(db_file, stat.S_IRUSR | stat.S_IWUSR) # 600
async def _load_assistant(self, assistant_id: str) -> Optional[AssistantInstance]:
"""Load assistant from database"""
db_file = self.base_path / "instances" / f"{assistant_id}.json"
if not db_file.exists():
return None
with open(db_file, 'r') as f:
data = json.load(f)
# Convert datetime strings back to datetime objects
for field in ['created_at', 'updated_at', 'last_used']:
if field in data and data[field]:
data[field] = datetime.fromisoformat(data[field])
return AssistantInstance(**data)
async def _delete_assistant_record(self, assistant_id: str):
"""Delete assistant from database"""
db_file = self.base_path / "instances" / f"{assistant_id}.json"
if db_file.exists():
db_file.unlink()
async def _get_owned_assistants(self, user_id: str) -> List[AssistantInstance]:
"""Get assistants owned by user"""
assistants = []
instances_dir = self.base_path / "instances"
if instances_dir.exists():
for file in instances_dir.glob("*.json"):
instance = await self._load_assistant(file.stem)
if instance and instance.owner_id == user_id:
assistants.append(instance)
return assistants
async def _get_shared_assistants(self, user_id: str) -> List[AssistantInstance]:
"""Get assistants shared with user"""
assistants = []
instances_dir = self.base_path / "instances"
if instances_dir.exists():
for file in instances_dir.glob("*.json"):
instance = await self._load_assistant(file.stem)
if instance and instance.owner_id != user_id:
# Check if user has access
allowed, _ = await self.access_controller.check_permission(
user_id, instance, "read"
)
if allowed:
assistants.append(instance)
return assistants