diff --git a/docs/lsp_configuration.md b/docs/lsp_configuration.md new file mode 100644 index 000000000..1868cdc55 --- /dev/null +++ b/docs/lsp_configuration.md @@ -0,0 +1,129 @@ +# Sidecar LSP Configuration Guide + +## Overview +The sidecar service supports flexible Language Server Protocol (LSP) configuration through JSON configuration files. This allows you to: +- Configure language servers for different programming languages +- Set custom initialization options +- Define server-specific settings +- Manage server lifecycle and capabilities + +## Configuration File +Place your configuration in `lsp_config.json`: + +```json +{ + "language_servers": { + "rust": { + "command": "rust-analyzer", + "args": [], + "initialization_options": { + "checkOnSave": true, + "procMacro": true + }, + "root_markers": ["Cargo.toml"], + "capabilities": [ + "completions", + "diagnostics", + "formatting", + "references", + "definition" + ] + }, + "typescript": { + "command": "typescript-language-server", + "args": ["--stdio"], + "initialization_options": { + "preferences": { + "importModuleSpecifierPreference": "relative" + } + }, + "root_markers": ["package.json", "tsconfig.json"], + "capabilities": [ + "completions", + "diagnostics", + "formatting", + "references" + ] + } + }, + "global_settings": { + "workspace_folders": ["src", "tests"], + "sync_kind": "full", + "completion_trigger_characters": [".", ":", ">"], + "signature_trigger_characters": ["(", ","] + } +} +``` + +## Configuration Options + +### Language Server Configuration +Configure individual language servers: +- `command`: Executable name or path +- `args`: Command line arguments +- `initialization_options`: LSP initialization parameters +- `root_markers`: Files indicating project root +- `capabilities`: Supported LSP features + +### Global Settings +Control LSP behavior across all servers: +- `workspace_folders`: Default workspace directories +- `sync_kind`: Document sync type (none/full/incremental) +- `completion_trigger_characters`: Characters triggering completion +- `signature_trigger_characters`: Characters triggering signature help + +## Supported Languages + +### Built-in Support +- Rust (rust-analyzer) +- TypeScript/JavaScript (typescript-language-server) +- Python (pyright) +- Go (gopls) +- Java (jdtls) +- C/C++ (clangd) +- HTML/CSS (vscode-html-language-server) +- JSON (vscode-json-language-server) +- YAML (yaml-language-server) +- PHP (intelephense) + +### Custom Server Configuration +Example adding a custom language server: +```json +{ + "language_servers": { + "custom_lang": { + "command": "/path/to/custom-ls", + "args": ["--custom-arg"], + "initialization_options": { + "customSetting": true + }, + "root_markers": ["custom.config"], + "capabilities": ["completions", "diagnostics"] + } + } +} +``` + +## Usage Example + +1. Create configuration file: +```bash +echo '{ + "language_servers": { + "rust": { + "command": "rust-analyzer", + "initialization_options": { + "checkOnSave": true + } + } + } +}' > lsp_config.json +``` + +2. Load configuration: +```rust +let state = LspState::new().await?; +state.load_configuration(Path::new("lsp_config.json")).await?; +``` + +The configuration will be applied automatically to all LSP operations. \ No newline at end of file diff --git a/docs/model_configuration.md b/docs/model_configuration.md new file mode 100644 index 000000000..cb73081e4 --- /dev/null +++ b/docs/model_configuration.md @@ -0,0 +1,123 @@ +# Sidecar Model Configuration Guide + +## Overview +The sidecar service supports flexible model configuration through JSON configuration files. This allows you to: +- Enable/disable specific providers or models +- Override model parameters +- Configure custom endpoints +- Set provider-specific settings + +## Configuration File +Place your configuration in `models_config.json`: + +```json +{ + "config_path": "/path/to/config", + "model_overrides": { + "gpt-4": { + "config": { + "temperature": 0.7, + "max_tokens": 4096, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0 + }, + "enabled": true, + "endpoint": "https://custom-endpoint/v1" + }, + "claude-3-opus": { + "config": { + "temperature": 0.8, + "max_tokens": 8192 + }, + "enabled": true + } + }, + "enabled_providers": ["OpenAI", "Anthropic", "TogetherAI"], + "provider_endpoints": { + "OpenAI": "https://api.openai.com/v1", + "Anthropic": "https://api.anthropic.com/v1" + } +} +``` + +## Configuration Options + +### Model Overrides +Override settings for specific models: +- `config`: Model-specific parameters + - `temperature`: Sampling temperature (0.0-1.0) + - `max_tokens`: Maximum tokens to generate + - `top_p`: Nucleus sampling parameter + - `frequency_penalty`: Frequency penalty for token selection + - `presence_penalty`: Presence penalty for token selection +- `enabled`: Enable/disable specific model +- `endpoint`: Custom endpoint for this model + +### Provider Configuration +Control provider availability: +- `enabled_providers`: List of enabled providers (omit to enable all) +- `provider_endpoints`: Custom endpoints for providers + +## Supported Providers & Models + +### OpenAI +- gpt-4-32k +- gpt-4-preview +- gpt-4 +- gpt-3.5-turbo-16k +- gpt-3.5-turbo + +### Anthropic +- claude-3-opus +- claude-3-sonnet +- claude-3-haiku + +### Together AI +- codellama-70b-instruct +- codellama-34b-instruct +- codellama-13b-instruct +- llama2-70b +- llama2-13b + +### Google +- gemini-pro +- gemini-ultra + +### Cohere +- command-r +- command + +### Mistral +- mistral-large +- mistral-medium +- mistral-small + +### Meta +- llama3-70b +- llama3-13b + +## Usage Example + +1. Create configuration file: +```bash +echo '{ + "enabled_providers": ["OpenAI", "Anthropic"], + "model_overrides": { + "gpt-4": { + "config": { + "temperature": 0.5 + }, + "enabled": true + } + } +}' > models_config.json +``` + +2. Load configuration: +```rust +let state = ModelState::new().await?; +state.load_configuration(Path::new("models_config.json")).await?; +``` + +The configuration will be applied automatically to all model operations. \ No newline at end of file diff --git a/examples/lsp_config.json b/examples/lsp_config.json new file mode 100644 index 000000000..4da775acf --- /dev/null +++ b/examples/lsp_config.json @@ -0,0 +1,74 @@ +{ + "language_servers": { + "rust": { + "command": "rust-analyzer", + "args": [], + "initialization_options": { + "checkOnSave": true, + "procMacro": true, + "diagnostics": { + "enable": true, + "warningsAsHint": [] + } + }, + "root_markers": ["Cargo.toml"], + "capabilities": [ + "completions", + "diagnostics", + "formatting", + "references", + "definition" + ] + }, + "typescript": { + "command": "typescript-language-server", + "args": ["--stdio"], + "initialization_options": { + "preferences": { + "importModuleSpecifierPreference": "relative" + }, + "typescript": { + "suggest": { + "completeFunctionCalls": true + } + } + }, + "root_markers": ["package.json", "tsconfig.json"], + "capabilities": [ + "completions", + "diagnostics", + "formatting", + "references" + ] + }, + "python": { + "command": "pyright-langserver", + "args": ["--stdio"], + "initialization_options": { + "python": { + "analysis": { + "typeCheckingMode": "basic", + "autoSearchPaths": true + } + } + }, + "root_markers": ["pyproject.toml", "setup.py"], + "capabilities": [ + "completions", + "diagnostics", + "formatting", + "references" + ] + } + }, + "global_settings": { + "workspace_folders": ["src", "tests"], + "sync_kind": "full", + "completion_trigger_characters": [".", ":", ">"], + "signature_trigger_characters": ["(", ","], + "hover_trigger_characters": [".", ":"], + "code_action_trigger_characters": ["."], + "format_on_save": true, + "max_completion_items": 100 + } +} \ No newline at end of file diff --git a/examples/models_config.json b/examples/models_config.json new file mode 100644 index 000000000..a2474d77f --- /dev/null +++ b/examples/models_config.json @@ -0,0 +1,48 @@ +{ + "enabled_providers": [ + "OpenAI", + "Anthropic", + "TogetherAI", + "Google", + "Cohere", + "Mistral", + "Meta" + ], + "model_overrides": { + "gpt-4": { + "config": { + "temperature": 0.7, + "max_tokens": 4096, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0 + }, + "enabled": true + }, + "claude-3-opus": { + "config": { + "temperature": 0.8, + "max_tokens": 8192, + "top_p": 1.0 + }, + "enabled": true + }, + "codellama-70b-instruct": { + "config": { + "temperature": 0.5, + "max_tokens": 4096, + "top_p": 0.9 + }, + "enabled": true + } + }, + "provider_endpoints": { + "OpenAI": "https://api.openai.com/v1", + "Anthropic": "https://api.anthropic.com/v1", + "TogetherAI": "https://api.together.xyz/v1", + "Google": "https://generativelanguage.googleapis.com/v1", + "Cohere": "https://api.cohere.ai/v1", + "Mistral": "https://api.mistral.ai/v1", + "Meta": "https://llama.meta.ai/v1" + } +} \ No newline at end of file diff --git a/fail.txt b/fail.txt new file mode 100644 index 000000000..bf2c0b433 --- /dev/null +++ b/fail.txt @@ -0,0 +1,2 @@ +No changes were made by the agent. +run_id: codestoryai_sidecar_issue_2081_e29258e0 \ No newline at end of file diff --git a/output.txt b/output.txt new file mode 100644 index 000000000..cd3a2a3f3 --- /dev/null +++ b/output.txt @@ -0,0 +1,556 @@ +# Sidecar HTTP API Documentation + +## Public Routes + +### Health Check +- **Endpoint**: `/health` +- **Method**: GET +- **Response**: +```json +{ + "done": boolean +} +``` +- **Description**: Basic health check endpoint to verify service status + +### Configuration +- **Endpoint**: `/config` +- **Method**: GET +- **Response**: +```json +{ + "response": string +} +``` +- **Description**: Returns current configuration information + +### Version +- **Endpoint**: `/version` +- **Method**: GET +- **Response**: +```json +{ + "version_hash": string, + "package_version": string +} +``` +- **Description**: Returns version information of the sidecar service + +## Tree-sitter Operations + +### Extract Documentation Strings +- **Endpoint**: `/tree-sitter/extract-documentation` +- **Method**: POST +- **Request**: +```json +{ + "language": string, + "source": string +} +``` +- **Response**: +```json +{ + "documentation": string[] +} +``` +- **Description**: Extracts documentation strings from source code + +### Extract Diagnostics Range +- **Endpoint**: `/tree-sitter/extract-diagnostics-range` +- **Method**: POST +- **Request**: +```json +{ + "range": { + "start": { "line": number, "character": number }, + "end": { "line": number, "character": number } + }, + "text_document_web": { + "text": string, + "language": string + }, + "threshold_to_expand": number +} +``` +- **Response**: +```json +{ + "range": { + "start": { "line": number, "character": number }, + "end": { "line": number, "character": number } + } +} +``` +- **Description**: Extracts and expands diagnostic ranges from code + +### Validate Tree-sitter Node +- **Endpoint**: `/tree-sitter/validate` +- **Method**: POST +- **Request**: +```json +{ + "language": string, + "source": string +} +``` +- **Response**: +```json +{ + "valid": boolean +} +``` +- **Description**: Validates if source code is parseable by tree-sitter + +### Validate XML +- **Endpoint**: `/tree-sitter/validate-xml` +- **Method**: POST +- **Request**: +```json +{ + "input": string +} +``` +- **Response**: +```json +{ + "valid": boolean +} +``` +- **Description**: Validates XML syntax + +## File Operations + +### Edit File +- **Endpoint**: `/file/edit` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string, + "file_content": string, + "new_content": string, + "language": string, + "user_query": string, + "session_id": string, + "code_block_index": number, + "model_config": { + // LLM client configuration + } +} +``` +- **Response**: Server-Sent Events (SSE) stream with following event types: +```json +{ + "type": "Message" | "Action" | "TextEdit" | "TextEditStreaming" | "Status", + "data": { + // Varies based on type + } +} +``` +- **Description**: Handles file editing operations with streaming updates + +## Inline Completion Operations + +### Get Inline Completion +- **Endpoint**: `/inline-completion` +- **Method**: POST +- **Request**: +```json +{ + "filepath": string, + "language": string, + "text": string, + "position": { + "line": number, + "character": number + }, + "indentation": string?, + "model_config": object, + "id": string, + "clipboard_content": string?, + "type_identifiers": array, + "user_id": string? +} +``` +- **Response**: Server-Sent Events (SSE) stream of completion suggestions +- **Description**: Provides real-time code completion suggestions + +### Cancel Inline Completion +- **Endpoint**: `/inline-completion/cancel` +- **Method**: POST +- **Request**: +```json +{ + "id": string +} +``` +- **Description**: Cancels an ongoing inline completion request + +### Document Open +- **Endpoint**: `/inline-completion/document-open` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string, + "file_content": string, + "language": string +} +``` +- **Description**: Notifies when a document is opened for inline completion + +### File Content Change +- **Endpoint**: `/inline-completion/file-content-change` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string, + "language": string, + "file_content": string, + "events": [{ + "range": { + "start_line": number, + "end_line": number, + "start_column": number, + "end_column": number + }, + "text": string + }] +} +``` +- **Description**: Handles file content changes for inline completion + +### Get File Content +- **Endpoint**: `/inline-completion/file-content` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string +} +``` +- **Response**: +```json +{ + "file_content": string? +} +``` +- **Description**: Retrieves current file content + +### Get Edited Lines +- **Endpoint**: `/inline-completion/edited-lines` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string +} +``` +- **Response**: +```json +{ + "edited_lines": number[] +} +``` +- **Description**: Returns list of edited line numbers + +### Get Identifier Nodes +- **Endpoint**: `/inline-completion/identifier-nodes` +- **Method**: POST +- **Request**: +```json +{ + "file_path": string, + "language": string, + "file_content": string, + "cursor_line": number, + "cursor_column": number +} +``` +- **Response**: +```json +{ + "identifier_nodes": [{ + "name": string, + "range": { + "start": { "line": number, "character": number }, + "end": { "line": number, "character": number } + } + }], + "function_parameters": array, + "import_nodes": array +} +``` +- **Description**: Returns identifier nodes at cursor position + +### Get Symbol History +- **Endpoint**: `/inline-completion/symbol-history` +- **Method**: POST +- **Response**: +```json +{ + "symbols": [[string, number[]]], + "symbol_content": object, + "timestamps": number[] +} +``` +- **Description**: Returns history of symbol changes + +## Agentic Routes + +### Append Plan +- **Endpoint**: `/agent/plan/append` +- **Method**: POST +- **Request**: +```json +{ + "user_query": string, + "thread_id": UUID, + "editor_url": string, + "user_context": { + // User context information + }, + "is_deep_reasoning": boolean, + "with_lsp_enrichment": boolean, + "access_token": string +} +``` +- **Description**: Appends a plan to an existing agent thread + +## Agent Session Operations + +### Chat Session +- **Endpoint**: `/agent/session/chat` +- **Method**: POST +- **Request**: +```json +{ + "session_id": string, + "exchange_id": string, + "editor_url": string, + "query": string, + "user_context": object, + "repo_ref": object, + "project_labels": string[], + "root_directory": string, + "codebase_search": boolean, + "access_token": string, + "model_configuration": object, + "all_files": string[], + "open_files": string[], + "shell": string, + "aide_rules": string?, + "reasoning": boolean, + "semantic_search": boolean, + "is_devtools_context": boolean +} +``` +- **Response**: Server-Sent Events (SSE) stream +- **Description**: Initiates or continues a chat session with the agent + +### Tool Use +- **Endpoint**: `/agent/session/tool-use` +- **Method**: POST +- **Request**: Same as chat session +- **Response**: Server-Sent Events (SSE) stream +- **Description**: Executes tool-based operations in agent session + +### Plan Generation +- **Endpoint**: `/agent/session/plan` +- **Method**: POST +- **Request**: Same as chat session +- **Response**: Server-Sent Events (SSE) stream +- **Description**: Generates a plan for complex operations + +### Plan Iteration +- **Endpoint**: `/agent/session/plan/iterate` +- **Method**: POST +- **Request**: Same as chat session +- **Response**: Server-Sent Events (SSE) stream +- **Description**: Iterates on an existing plan + +### Cancel Running Exchange +- **Endpoint**: `/agent/session/cancel` +- **Method**: POST +- **Request**: +```json +{ + "exchange_id": string, + "session_id": string, + "editor_url": string, + "access_token": string, + "model_configuration": object +} +``` +- **Description**: Cancels an ongoing exchange in the session + +### Verify Model Configuration +- **Endpoint**: `/agent/verify-model-config` +- **Method**: POST +- **Request**: +```json +{ + "model_configuration": object +} +``` +- **Response**: +```json +{ + "valid": boolean, + "error": string? +} +``` +- **Description**: Validates LLM model configuration + +## Code Sculpting Operations + +### Code Sculpting Request +- **Endpoint**: `/code-sculpting` +- **Method**: POST +- **Request**: +```json +{ + "request_id": string, + "instruction": string +} +``` +- **Response**: +```json +{ + "done": boolean +} +``` +- **Description**: Handles code sculpting operations + +### Code Sculpting Heal +- **Endpoint**: `/code-sculpting/heal` +- **Method**: POST +- **Request**: +```json +{ + "request_id": string +} +``` +- **Response**: +```json +{ + "done": boolean +} +``` +- **Description**: Heals/repairs code after sculpting operations + +### Push Diagnostics +- **Endpoint**: `/diagnostics/push` +- **Method**: POST +- **Request**: +```json +{ + "fs_file_path": string, + "diagnostics": [{ + "message": string, + "range": { + "start": { "line": number, "character": number }, + "end": { "line": number, "character": number } + }, + "range_content": string + }], + "source": string? +} +``` +- **Response**: +```json +{ + "done": boolean +} +``` +- **Description**: Pushes diagnostic information for code analysis + +## Notes +- All endpoints return appropriate HTTP status codes +- Authentication may be required for certain endpoints +- Error responses include descriptive messages +- Some endpoints support streaming responses using Server-Sent Events (SSE) + +## Required Enhancements for Full Independence + +To make Sidecar a fully independent self-sufficient unit, the following additional capabilities and endpoints would be needed: + +### Authentication & Authorization +1. **Token Management** + - `/auth/token` - Generate and manage authentication tokens + - `/auth/validate` - Validate token authenticity + - `/auth/refresh` - Refresh expired tokens + +### Model Management +1. **LLM Integration** + - `/models/list` - List available language models + - `/models/configure` - Configure model parameters + - `/models/status` - Check model availability and health + +### File System Operations +1. **Extended File Management** + - `/fs/watch` - File system watch capabilities + - `/fs/search` - Advanced file search with filters + - `/fs/git` - Git integration endpoints + - `/fs/workspace` - Workspace management + +### Language Server Integration +1. **LSP Management** + - `/lsp/start` - Start language servers + - `/lsp/status` - Check LSP status + - `/lsp/configure` - Configure language servers + - `/lsp/capabilities` - Query LSP capabilities + +### Caching & Performance +1. **Cache Management** + - `/cache/clear` - Clear various caches + - `/cache/status` - Check cache status + - `/cache/configure` - Configure caching behavior + +### Metrics & Monitoring +1. **Observability** + - `/metrics` - Prometheus-compatible metrics endpoint + - `/traces` - Distributed tracing endpoint + - `/logs` - Log aggregation and query + +### Configuration Management +1. **Dynamic Configuration** + - `/config/update` - Update runtime configuration + - `/config/reload` - Reload configuration from disk + - `/config/validate` - Validate configuration changes + +### Resource Management +1. **System Resources** + - `/resources/status` - CPU, memory, disk usage + - `/resources/limits` - Set resource limits + - `/resources/scale` - Scale resources up/down + +### Plugin System +1. **Plugin Management** + - `/plugins/list` - List available plugins + - `/plugins/install` - Install new plugins + - `/plugins/configure` - Configure plugin settings + - `/plugins/status` - Check plugin health + +### Session Management +1. **Enhanced Session Control** + - `/session/cleanup` - Cleanup stale sessions + - `/session/transfer` - Transfer session state + - `/session/backup` - Backup session data + +### Security +1. **Security Controls** + - `/security/scan` - Security scanning endpoints + - `/security/audit` - Audit logging + - `/security/policy` - Security policy management + +These enhancements would make Sidecar: +1. Independently deployable without external dependencies +2. Self-managing with proper resource control +3. Secure with robust authentication +4. Observable with comprehensive monitoring +5. Extensible through plugin system +6. Resilient with proper session management +7. Configurable for different deployment scenarios \ No newline at end of file diff --git a/sidecar/Cargo.toml b/sidecar/Cargo.toml index 6be288c9f..bdb5aba94 100644 --- a/sidecar/Cargo.toml +++ b/sidecar/Cargo.toml @@ -26,8 +26,11 @@ anyhow = "1.0.75" serde_json = "1.0.107" serde = { version = "1.0.188", features = ["derive"] } once_cell = "1.18.0" -regex = ">= 1.9, < 1.9.5" +regex = "1.10.2" memchr = "2.5.0" +walkdir = "2.4.0" +tower-lsp = "0.20.0" +libloading = "0.8.1" axum = { version = "0.6.20", features = ["http2", "headers", "macros"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } @@ -40,6 +43,7 @@ erased-serde = "0.3.31" tower = "0.4.13" tower-http = { version = "0.4.1", features = ["auth", "cors", "catch-panic", "fs"] } thiserror = "1.0.49" +time = { version = "0.3", features = ["serde"] } gix = "0.54.1" rand = "0.8.5" flume = "0.11.0" @@ -78,6 +82,11 @@ serde-xml-rs = "0.6.0" async-recursion = "1.1.1" tree_magic_mini = "3.0.2" quick-xml = { version = "0.31.0", features = [ "serialize" ] } +jsonwebtoken = "9.2.0" +notify = "6.1.1" +prometheus = "0.13.3" +opentelemetry = { version = "0.21.0", features = ["rt-tokio"] } +opentelemetry-jaeger = "0.20.0" derivative = "2.2.0" similar = "2.6.0" globset = "0.4.15" diff --git a/sidecar/src/application/application.rs b/sidecar/src/application/application.rs index c76a22c37..cbd108b53 100644 --- a/sidecar/src/application/application.rs +++ b/sidecar/src/application/application.rs @@ -143,6 +143,7 @@ impl Application { tool_box, anchored_request_tracker, session_service, + lsp_state, }) } @@ -179,4 +180,4 @@ impl Application { // We need at the very least 1 thread to do background work fn minimum_parallelism() -> usize { 1 -} +} \ No newline at end of file diff --git a/sidecar/src/application/config/configuration.rs b/sidecar/src/application/config/configuration.rs index d0d67c064..1d7731c3d 100644 --- a/sidecar/src/application/config/configuration.rs +++ b/sidecar/src/application/config/configuration.rs @@ -79,6 +79,42 @@ impl Configuration { pub fn scratch_pad(&self) -> PathBuf { self.index_dir.join("scratch_pad") } + + pub fn models_config_path(&self) -> Option { + self.model_config_path.clone().or_else(|| { + self.index_dir.join("models_config.json").exists().then(|| { + self.index_dir.join("models_config.json") + }) + }) + } + + pub async fn load_model_config(&mut self) -> anyhow::Result<()> { + if let Some(path) = self.models_config_path() { + if path.exists() { + let content = tokio::fs::read_to_string(path).await?; + self.model_configuration = Some(serde_json::from_str(&content)?); + } + } + Ok(()) + } + + pub fn lsp_config_path(&self) -> Option { + self.lsp_config_path.clone().or_else(|| { + self.index_dir.join("lsp_config.json").exists().then(|| { + self.index_dir.join("lsp_config.json") + }) + }) + } + + pub async fn load_lsp_config(&mut self) -> anyhow::Result<()> { + if let Some(path) = self.lsp_config_path() { + if path.exists() { + let content = tokio::fs::read_to_string(path).await?; + self.lsp_configuration = Some(serde_json::from_str(&content)?); + } + } + Ok(()) + } } fn default_index_dir() -> PathBuf { @@ -111,4 +147,4 @@ fn default_collection_name() -> String { fn default_user_id() -> String { let username = whoami::username(); username -} +} \ No newline at end of file diff --git a/sidecar/src/auth/mod.rs b/sidecar/src/auth/mod.rs new file mode 100644 index 000000000..997b2c9df --- /dev/null +++ b/sidecar/src/auth/mod.rs @@ -0,0 +1,97 @@ +use axum::{ + routing::{post, get}, + Router, + Json, + http::StatusCode, +}; +use serde::{Deserialize, Serialize}; +use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation, errors::Error as JwtError}; +use time::{Duration, OffsetDateTime}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + sub: String, + exp: i64, + iat: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenRequest { + username: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenResponse { + token: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshRequest { + token: String, +} + +const JWT_SECRET: &[u8] = b"sidecar_secret_key"; +const TOKEN_DURATION_HOURS: i64 = 24; + +pub fn router() -> Router { + Router::new() + .route("/auth/token", post(generate_token)) + .route("/auth/validate", post(validate_token)) + .route("/auth/refresh", post(refresh_token)) +} + +async fn generate_token(Json(req): Json) -> Result, StatusCode> { + let now = OffsetDateTime::now_utc(); + let exp = now + Duration::hours(TOKEN_DURATION_HOURS); + + let claims = Claims { + sub: req.username, + exp: exp.unix_timestamp(), + iat: now.unix_timestamp(), + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(JWT_SECRET), + ).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(TokenResponse { token })) +} + +async fn validate_token(Json(req): Json) -> Result { + match decode::( + &req.token, + &DecodingKey::from_secret(JWT_SECRET), + &Validation::default(), + ) { + Ok(_) => Ok(StatusCode::OK), + Err(JwtError::ExpiredSignature) => Err(StatusCode::UNAUTHORIZED), + Err(_) => Err(StatusCode::BAD_REQUEST), + } +} + +async fn refresh_token(Json(req): Json) -> Result, StatusCode> { + let token_data = decode::( + &req.token, + &DecodingKey::from_secret(JWT_SECRET), + &Validation::default(), + ).map_err(|_| StatusCode::UNAUTHORIZED)?; + + let now = OffsetDateTime::now_utc(); + let exp = now + Duration::hours(TOKEN_DURATION_HOURS); + + let new_claims = Claims { + sub: token_data.claims.sub, + exp: exp.unix_timestamp(), + iat: now.unix_timestamp(), + }; + + let new_token = encode( + &Header::default(), + &new_claims, + &EncodingKey::from_secret(JWT_SECRET), + ).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(TokenResponse { token: new_token })) +} \ No newline at end of file diff --git a/sidecar/src/bin/webserver.rs b/sidecar/src/bin/webserver.rs index fc2277c04..ed0b3087a 100644 --- a/sidecar/src/bin/webserver.rs +++ b/sidecar/src/bin/webserver.rs @@ -143,7 +143,6 @@ pub async fn start(app: Application) -> anyhow::Result<()> { let protected_routes = Router::new() .nest("/agentic", agentic_router()) .nest("/plan", plan_router()); - // .layer(from_fn(auth_middleware)); // routes through middleware // no middleware check let public_routes = Router::new() @@ -161,13 +160,16 @@ pub async fn start(app: Application) -> anyhow::Result<()> { api = api.route("/health", get(sidecar::webserver::health::health)); + // Create the router with application state + let router = sidecar::webserver::create_router(app.clone()).await?; + let api = api + .merge(router) .layer(Extension(app.clone())) .with_state(app.clone()) .with_state(app.clone()) .layer(CorsLayer::permissive()) .layer(CatchPanicLayer::new()) - // I want to set the bytes limit here to 20 MB .layer(DefaultBodyLimit::max(20 * 1024 * 1024)); let router = Router::new().nest("/api", api); diff --git a/sidecar/src/cache/mod.rs b/sidecar/src/cache/mod.rs new file mode 100644 index 000000000..71e8d2c4b --- /dev/null +++ b/sidecar/src/cache/mod.rs @@ -0,0 +1,122 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::State, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc, time::{SystemTime, Duration}}; +use tokio::sync::RwLock; +use tokio::time::interval; + +#[derive(Debug, Clone)] +struct CacheEntry { + value: Vec, + expiry: SystemTime, +} + +pub struct CacheState { + entries: RwLock>, + config: RwLock, + stats: RwLock, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CacheStats { + size: u64, + items: u64, + hit_rate: f32, + miss_rate: f32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CacheConfig { + max_size: u64, + ttl_seconds: u64, + compression_enabled: bool, +} + +#[derive(Debug, Deserialize)] +pub struct ClearRequest { + cache_type: String, +} + +impl CacheState { + pub fn new(config: CacheConfig) -> Arc { + let state = Arc::new(Self { + entries: RwLock::new(HashMap::new()), + config: RwLock::new(config), + stats: RwLock::new(CacheStats { + size: 0, + items: 0, + hit_rate: 0.0, + miss_rate: 0.0, + }), + }); + + // Start background cleanup task + let state_clone = state.clone(); + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(60)); + loop { + interval.tick().await; + state_clone.cleanup_expired().await; + } + }); + + state + } + + async fn cleanup_expired(&self) { + let now = SystemTime::now(); + let mut entries = self.entries.write().await; + entries.retain(|_, entry| entry.expiry > now); + + let mut stats = self.stats.write().await; + stats.items = entries.len() as u64; + } + + async fn clear_cache(&self, cache_type: &str) { + let mut entries = self.entries.write().await; + if cache_type == "*" { + entries.clear(); + } else { + entries.retain(|key, _| !key.starts_with(cache_type)); + } + + let mut stats = self.stats.write().await; + stats.items = entries.len() as u64; + } +} + +pub fn router() -> Router> { + Router::new() + .route("/cache/clear", post(clear_cache)) + .route("/cache/status", get(cache_status)) + .route("/cache/configure", post(configure_cache)) +} + +async fn clear_cache( + State(state): State>, + Json(req): Json +) -> Result { + state.clear_cache(&req.cache_type).await; + Ok(StatusCode::OK) +} + +async fn cache_status( + State(state): State> +) -> Json { + let stats = state.stats.read().await; + Json(stats.clone()) +} + +async fn configure_cache( + State(state): State>, + Json(config): Json +) -> Result { + let mut current_config = state.config.write().await; + *current_config = config; + Ok(StatusCode::OK) +} \ No newline at end of file diff --git a/sidecar/src/fs/mod.rs b/sidecar/src/fs/mod.rs new file mode 100644 index 000000000..1b53f5bc0 --- /dev/null +++ b/sidecar/src/fs/mod.rs @@ -0,0 +1,105 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::Query, +}; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, collections::HashMap}; +use tokio::fs; +use ignore::WalkBuilder; + +#[derive(Debug, Serialize)] +pub struct FileInfo { + path: String, + is_dir: bool, + size: u64, + modified: u64, +} + +#[derive(Debug, Deserialize)] +pub struct SearchQuery { + pattern: String, + #[serde(default)] + recursive: bool, +} + +#[derive(Debug, Deserialize)] +pub struct WatchRequest { + path: String, + recursive: bool, +} + +pub fn router() -> Router { + Router::new() + .route("/fs/watch", post(watch_directory)) + .route("/fs/search", get(search_files)) + .route("/fs/workspace", get(get_workspace_info)) +} + +async fn watch_directory( + State(state): State>, + Json(req): Json +) -> Result { + let path = PathBuf::from(&req.path); + if !path.exists() { + return Err(StatusCode::BAD_REQUEST); + } + + let tx = state.event_tx.clone(); + let mut watcher = notify::recommended_watcher(move |res: Result| { + if let Ok(event) = res { + let _ = tx.send(event); + } + }).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let mode = if req.recursive { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }; + + watcher.watch(&path, mode) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut watchers = state.watchers.write().await; + watchers.insert(req.path, watcher); + + Ok(StatusCode::OK) +} + +async fn search_files(Query(query): Query) -> Json> { + let mut files = Vec::new(); + + if let Ok(walker) = WalkBuilder::new(".") + .hidden(false) + .build() { + for entry in walker.filter_map(Result::ok) { + if let Some(path) = entry.path().to_str() { + if path.contains(&query.pattern) { + if let Ok(metadata) = entry.metadata() { + files.push(FileInfo { + path: path.to_string(), + is_dir: metadata.is_dir(), + size: metadata.len(), + modified: metadata.modified() + .map(|time| time.duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default().as_secs()) + .unwrap_or(0), + }); + } + } + } + } + } + + Json(files) +} + +async fn get_workspace_info() -> Json> { + let mut info = HashMap::new(); + info.insert("root".to_string(), ".".to_string()); + info.insert("git_enabled".to_string(), "true".to_string()); + Json(info) +} \ No newline at end of file diff --git a/sidecar/src/lib.rs b/sidecar/src/lib.rs index 77bdb2411..594c971b3 100644 --- a/sidecar/src/lib.rs +++ b/sidecar/src/lib.rs @@ -1,18 +1,26 @@ pub mod agent; pub mod agentic; pub mod application; +pub mod auth; +pub mod cache; pub mod chunking; pub mod db; pub mod file_analyser; +pub mod fs; pub mod git; pub mod in_line_agent; pub mod inline_completion; +pub mod lsp; pub mod mcts; +pub mod metrics; +pub mod models; +pub mod plugins; pub mod repo; pub mod repomap; pub mod reporting; pub mod reranking; +pub mod security; pub mod state; pub mod tree_printer; pub mod user_context; -pub mod webserver; +pub mod webserver; \ No newline at end of file diff --git a/sidecar/src/lsp/mod.rs b/sidecar/src/lsp/mod.rs new file mode 100644 index 000000000..9bb1d9064 --- /dev/null +++ b/sidecar/src/lsp/mod.rs @@ -0,0 +1,214 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::State, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc, path::{Path, PathBuf}, process::{Child, Command}}; +use tokio::sync::RwLock; +use tower_lsp::lsp_types::*; +use anyhow::Result; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LspServerConfig { + command: String, + args: Vec, + initialization_options: HashMap, + root_markers: Vec, + capabilities: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GlobalSettings { + workspace_folders: Vec, + sync_kind: String, + completion_trigger_characters: Vec, + signature_trigger_characters: Vec, + hover_trigger_characters: Vec, + code_action_trigger_characters: Vec, + format_on_save: bool, + max_completion_items: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LspConfiguration { + language_servers: HashMap, + global_settings: GlobalSettings, +} + +#[derive(Debug)] +pub struct LspServerInstance { + config: LspServerConfig, + process: Child, + status: LspStatus, + workspace_folders: Vec, +} + +pub struct LspState { + servers: RwLock>, + config: RwLock, + capabilities: RwLock>>, +} + +impl LspState { + pub async fn new() -> Result> { + let default_config = LspConfiguration { + language_servers: HashMap::new(), + global_settings: GlobalSettings { + workspace_folders: vec!["src".to_string(), "tests".to_string()], + sync_kind: "full".to_string(), + completion_trigger_characters: vec![".".to_string(), ":".to_string()], + signature_trigger_characters: vec!["(".to_string(), ",".to_string()], + hover_trigger_characters: vec![".".to_string()], + code_action_trigger_characters: vec![".".to_string()], + format_on_save: true, + max_completion_items: 100, + }, + }; + + Ok(Arc::new(Self { + servers: RwLock::new(HashMap::new()), + config: RwLock::new(default_config), + capabilities: RwLock::new(HashMap::new()), + })) + } + + pub async fn load_configuration(&self, path: &Path) -> Result<()> { + let content = tokio::fs::read_to_string(path).await?; + let config: LspConfiguration = serde_json::from_str(&content)?; + + let mut current_config = self.config.write().await; + *current_config = config; + + // Update capabilities + let mut caps = self.capabilities.write().await; + for (lang, server_config) in current_config.language_servers.iter() { + caps.insert(lang.clone(), server_config.capabilities.clone()); + } + + Ok(()) + } + + pub async fn start_server(&self, language: &str, workspace_root: PathBuf) -> Result<()> { + let config = self.config.read().await; + let server_config = config.language_servers.get(language) + .ok_or_else(|| anyhow::anyhow!("Language server not configured: {}", language))?; + + // Check if root markers exist + let has_root_marker = server_config.root_markers.iter().any(|marker| { + workspace_root.join(marker).exists() + }); + + if !has_root_marker { + return Err(anyhow::anyhow!("No root markers found for {}", language)); + } + + // Start the language server process + let mut process = Command::new(&server_config.command) + .args(&server_config.args) + .current_dir(&workspace_root) + .spawn()?; + + let instance = LspServerInstance { + config: server_config.clone(), + process, + status: LspStatus::Running, + workspace_folders: vec![workspace_root], + }; + + let mut servers = self.servers.write().await; + servers.insert(language.to_string(), instance); + + Ok(()) + } + + pub async fn stop_server(&self, language: &str) -> Result<()> { + let mut servers = self.servers.write().await; + if let Some(mut instance) = servers.remove(language) { + instance.process.kill()?; + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LspServerInfo { + language: String, + status: LspStatus, + pid: Option, + capabilities: Vec, + workspace_folders: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum LspStatus { + Running, + Stopped, + Failed, +} + +#[derive(Debug, Deserialize)] +pub struct StartRequest { + language: String, + workspace_root: PathBuf, +} + +pub fn router() -> Router> { + Router::new() + .route("/lsp/start", post(start_server)) + .route("/lsp/stop", post(stop_server)) + .route("/lsp/status", get(server_status)) + .route("/lsp/capabilities", get(get_capabilities)) +} + +async fn start_server( + State(state): State>, + Json(req): Json +) -> Result { + state.start_server(&req.language, req.workspace_root) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::OK) +} + +async fn stop_server( + State(state): State>, + Json(req): Json +) -> Result { + state.stop_server(&req.language) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::OK) +} + +async fn server_status( + State(state): State> +) -> Json> { + let servers = state.servers.read().await; + let mut status = HashMap::new(); + + for (language, instance) in servers.iter() { + status.insert( + language.clone(), + LspServerInfo { + language: language.clone(), + status: instance.status.clone(), + pid: instance.process.id(), + capabilities: instance.config.capabilities.clone(), + workspace_folders: instance.workspace_folders.clone(), + }, + ); + } + + Json(status) +} + +async fn get_capabilities( + State(state): State> +) -> Json>> { + let capabilities = state.capabilities.read().await; + Json(capabilities.clone()) +} \ No newline at end of file diff --git a/sidecar/src/metrics/mod.rs b/sidecar/src/metrics/mod.rs new file mode 100644 index 000000000..d23b699ea --- /dev/null +++ b/sidecar/src/metrics/mod.rs @@ -0,0 +1,117 @@ +use axum::{ + routing::get, + Router, + response::{IntoResponse, Response}, + http::{header, StatusCode}, + extract::State, +}; +use prometheus::{Registry, Counter, Histogram, register_counter, register_histogram}; +use opentelemetry::{ + trace::{Tracer, TracerProvider}, + global, + sdk::{trace::TracerProvider as SdkTracerProvider, Resource}, +}; +use opentelemetry_jaeger::new_pipeline; +use std::sync::Arc; + +pub struct MetricsState { + registry: Registry, + request_counter: Counter, + error_counter: Counter, + request_duration: Histogram, + tracer: Arc, +} + +impl MetricsState { + pub fn new() -> Arc { + let registry = Registry::new(); + + let request_counter = register_counter!( + "sidecar_requests_total", + "Total number of requests processed" + ).unwrap(); + + let error_counter = register_counter!( + "sidecar_errors_total", + "Total number of errors encountered" + ).unwrap(); + + let request_duration = register_histogram!( + "sidecar_request_duration_seconds", + "Request duration in seconds", + vec![0.01, 0.05, 0.1, 0.5, 1.0, 5.0] + ).unwrap(); + + // Initialize OpenTelemetry tracer + let tracer_provider = new_pipeline() + .with_service_name("sidecar") + .with_collector_endpoint("http://localhost:14268/api/traces") + .build_simple() + .unwrap(); + + global::set_tracer_provider(tracer_provider.clone()); + let tracer = tracer_provider.tracer("sidecar"); + + Arc::new(Self { + registry, + request_counter, + error_counter, + request_duration, + tracer: Arc::new(tracer), + }) + } + + pub fn increment_request(&self) { + self.request_counter.inc(); + } + + pub fn increment_error(&self) { + self.error_counter.inc(); + } + + pub fn observe_duration(&self, duration: f64) { + self.request_duration.observe(duration); + } +} + +pub fn router() -> Router> { + Router::new() + .route("/metrics", get(metrics_handler)) + .route("/traces", get(traces_handler)) +} + +async fn metrics_handler( + State(state): State> +) -> Response { + let mut buffer = Vec::new(); + let encoder = prometheus::TextEncoder::new(); + + encoder.encode(&state.registry.gather(), &mut buffer) + .unwrap_or_default(); + + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, prometheus::TEXT_FORMAT) + .body(buffer.into()) + .unwrap() + .into_response() +} + +async fn traces_handler( + State(state): State> +) -> Response { + let tracer = state.tracer.clone(); + let span = tracer.start("traces_handler"); + + // Collect active traces + let trace_data = span.span_context() + .trace_id() + .to_string(); + + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/json") + .body(trace_data.into()) + .unwrap() + .into_response() +} \ No newline at end of file diff --git a/sidecar/src/models/mod.rs b/sidecar/src/models/mod.rs new file mode 100644 index 000000000..7bc972c12 --- /dev/null +++ b/sidecar/src/models/mod.rs @@ -0,0 +1,325 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::State, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum LLMProvider { + OpenAI, + Anthropic, + TogetherAI, + Google, + Cohere, + Mistral, + Meta, +} + +#[derive(Debug, Clone)] +pub enum LLMType { + // OpenAI Models + Gpt4_32k, + Gpt4_Preview, + Gpt4, + GPT3_5_16k, + GPT3_5_Turbo, + + // Anthropic Models + ClaudeOpus, + Claude3Sonnet, + Claude3Haiku, + + // Together AI Models + CodeLLama70BInstruct, + CodeLLama34BInstruct, + CodeLLama13BInstruct, + LLaMA2_70B, + LLaMA2_13B, + + // Google Models + GeminiPro, + GeminiUltra, + + // Cohere Models + CommandR, + Command, + + // Mistral Models + MistralLarge, + MistralMedium, + MistralSmall, + + // Meta Models + LLaMA3_70B, + LLaMA3_13B, +} + +impl ToString for LLMType { + fn to_string(&self) -> String { + match self { + // OpenAI + Self::Gpt4_32k => "gpt-4-32k".to_string(), + Self::Gpt4_Preview => "gpt-4-preview".to_string(), + Self::Gpt4 => "gpt-4".to_string(), + Self::GPT3_5_16k => "gpt-3.5-turbo-16k".to_string(), + Self::GPT3_5_Turbo => "gpt-3.5-turbo".to_string(), + + // Anthropic + Self::ClaudeOpus => "claude-3-opus".to_string(), + Self::Claude3Sonnet => "claude-3-sonnet".to_string(), + Self::Claude3Haiku => "claude-3-haiku".to_string(), + + // Together AI + Self::CodeLLama70BInstruct => "codellama-70b-instruct".to_string(), + Self::CodeLLama34BInstruct => "codellama-34b-instruct".to_string(), + Self::CodeLLama13BInstruct => "codellama-13b-instruct".to_string(), + Self::LLaMA2_70B => "llama2-70b".to_string(), + Self::LLaMA2_13B => "llama2-13b".to_string(), + + // Google + Self::GeminiPro => "gemini-pro".to_string(), + Self::GeminiUltra => "gemini-ultra".to_string(), + + // Cohere + Self::CommandR => "command-r".to_string(), + Self::Command => "command".to_string(), + + // Mistral + Self::MistralLarge => "mistral-large".to_string(), + Self::MistralMedium => "mistral-medium".to_string(), + Self::MistralSmall => "mistral-small".to_string(), + + // Meta + Self::LLaMA3_70B => "llama3-70b".to_string(), + Self::LLaMA3_13B => "llama3-13b".to_string(), + } + } +} + +impl LLMType { + fn provider(&self) -> LLMProvider { + match self { + Self::Gpt4_32k | Self::Gpt4_Preview | Self::Gpt4 | + Self::GPT3_5_16k | Self::GPT3_5_Turbo => LLMProvider::OpenAI, + + Self::ClaudeOpus | Self::Claude3Sonnet | + Self::Claude3Haiku => LLMProvider::Anthropic, + + Self::CodeLLama70BInstruct | Self::CodeLLama34BInstruct | + Self::CodeLLama13BInstruct | Self::LLaMA2_70B | + Self::LLaMA2_13B => LLMProvider::TogetherAI, + + Self::GeminiPro | Self::GeminiUltra => LLMProvider::Google, + + Self::CommandR | Self::Command => LLMProvider::Cohere, + + Self::MistralLarge | Self::MistralMedium | + Self::MistralSmall => LLMProvider::Mistral, + + Self::LLaMA3_70B | Self::LLaMA3_13B => LLMProvider::Meta, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ModelConfig { + temperature: f32, + max_tokens: u32, + top_p: f32, + frequency_penalty: f32, + presence_penalty: f32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ModelInfo { + name: String, + status: ModelStatus, + config: ModelConfig, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ModelStatus { + Available, + Busy, + Offline, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigureRequest { + model_name: String, + config: ModelConfig, +} + +pub fn router() -> Router> { + Router::new() + .route("/models/list", get(list_models)) + .route("/models/configure", post(configure_model)) + .route("/models/status", get(check_status)) +} + +async fn list_models() -> Json> { + let models = state.list_available_models().await; + Json(models) +} + +// Example model configuration file documentation +/// ```json +/// { +/// "config_path": "/path/to/config", +/// "model_overrides": { +/// "gpt-4": { +/// "config": { +/// "temperature": 0.7, +/// "max_tokens": 4096, +/// "top_p": 1.0, +/// "frequency_penalty": 0.0, +/// "presence_penalty": 0.0 +/// }, +/// "enabled": true, +/// "endpoint": "https://custom-endpoint/v1" +/// } +/// }, +/// "enabled_providers": ["OpenAI", "Anthropic"], +/// "provider_endpoints": { +/// "OpenAI": "https://api.openai.com/v1", +/// "Anthropic": "https://api.anthropic.com/v1" +/// } +/// } +/// ``` + +fn get_default_models() -> Vec { + vec![ + // OpenAI + LLMType::Gpt4_32k.to_string(), + LLMType::Gpt4_Preview.to_string(), + LLMType::Gpt4.to_string(), + LLMType::GPT3_5_16k.to_string(), + LLMType::GPT3_5_Turbo.to_string(), + + // Anthropic + LLMType::ClaudeOpus.to_string(), + LLMType::Claude3Sonnet.to_string(), + LLMType::Claude3Haiku.to_string(), + + // Together AI + LLMType::CodeLLama70BInstruct.to_string(), + LLMType::CodeLLama34BInstruct.to_string(), + LLMType::CodeLLama13BInstruct.to_string(), + LLMType::LLaMA2_70B.to_string(), + LLMType::LLaMA2_13B.to_string(), + + // Google + LLMType::GeminiPro.to_string(), + LLMType::GeminiUltra.to_string(), + + // Cohere + LLMType::CommandR.to_string(), + LLMType::Command.to_string(), + + // Mistral + LLMType::MistralLarge.to_string(), + LLMType::MistralMedium.to_string(), + LLMType::MistralSmall.to_string(), + + // Meta + LLMType::LLaMA3_70B.to_string(), + LLMType::LLaMA3_13B.to_string(), + ]; + Json(models) +} + +async fn configure_model( + State(state): State>, + Json(req): Json, +) -> Result { + let mut configs = state.configs.write().await; + configs.insert(req.model_name, req.config); + Ok(StatusCode::OK) +} + +async fn check_status( + State(state): State>, +) -> Json> { + let mut status = HashMap::new(); + let configs = state.configs.read().await; + let status_cache = state.status_cache.read().await; + + // Check each provider's status + for provider in state.broker.providers.keys() { + match provider { + LLMProvider::OpenAI => { + add_model_status(&mut status, &LLMType::Gpt4_32k.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::Gpt4_Preview.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::Gpt4.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::GPT3_5_16k.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::GPT3_5_Turbo.to_string(), &configs, &status_cache); + } + LLMProvider::Anthropic => { + add_model_status(&mut status, &LLMType::ClaudeOpus.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::Claude3Sonnet.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::Claude3Haiku.to_string(), &configs, &status_cache); + } + LLMProvider::TogetherAI => { + add_model_status(&mut status, &LLMType::CodeLLama70BInstruct.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::CodeLLama34BInstruct.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::CodeLLama13BInstruct.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::LLaMA2_70B.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::LLaMA2_13B.to_string(), &configs, &status_cache); + } + LLMProvider::Google => { + add_model_status(&mut status, &LLMType::GeminiPro.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::GeminiUltra.to_string(), &configs, &status_cache); + } + LLMProvider::Cohere => { + add_model_status(&mut status, &LLMType::CommandR.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::Command.to_string(), &configs, &status_cache); + } + LLMProvider::Mistral => { + add_model_status(&mut status, &LLMType::MistralLarge.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::MistralMedium.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::MistralSmall.to_string(), &configs, &status_cache); + } + LLMProvider::Meta => { + add_model_status(&mut status, &LLMType::LLaMA3_70B.to_string(), &configs, &status_cache); + add_model_status(&mut status, &LLMType::LLaMA3_13B.to_string(), &configs, &status_cache); + } + } + } + + Json(status) +} + +fn add_model_status( + status: &mut HashMap, + model_name: &str, + configs: &HashMap, + status_cache: &HashMap, +) { + let default_config = ModelConfig { + temperature: 0.7, + max_tokens: 2048, + top_p: 1.0, + frequency_penalty: 0.0, + presence_penalty: 0.0, + }; + + let config = configs.get(model_name).cloned().unwrap_or(default_config); + let model_status = status_cache + .get(model_name) + .cloned() + .unwrap_or(ModelStatus::Available); + + status.insert( + model_name.to_string(), + ModelInfo { + name: model_name.to_string(), + status: model_status, + config, + }, + ); +} \ No newline at end of file diff --git a/sidecar/src/plugins/mod.rs b/sidecar/src/plugins/mod.rs new file mode 100644 index 000000000..570658906 --- /dev/null +++ b/sidecar/src/plugins/mod.rs @@ -0,0 +1,152 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::State, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc, path::PathBuf}; +use tokio::sync::RwLock; +use async_trait::async_trait; +use libloading::{Library, Symbol}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PluginInfo { + name: String, + version: String, + status: PluginStatus, + config: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PluginStatus { + Active, + Inactive, + Error, +} + +#[derive(Debug, Deserialize)] +pub struct InstallRequest { + name: String, + version: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ConfigureRequest { + name: String, + config: HashMap, +} + +#[async_trait] +pub trait Plugin: Send + Sync { + async fn initialize(&self) -> Result<(), String>; + async fn shutdown(&self) -> Result<(), String>; + async fn execute(&self, input: serde_json::Value) -> Result; +} + +pub struct PluginManager { + plugins: RwLock>>, + libraries: RwLock>, + configs: RwLock>, +} + +impl PluginManager { + pub fn new() -> Arc { + Arc::new(Self { + plugins: RwLock::new(HashMap::new()), + libraries: RwLock::new(HashMap::new()), + configs: RwLock::new(HashMap::new()), + }) + } + + async fn load_plugin(&self, name: &str, path: &PathBuf) -> Result<(), String> { + unsafe { + let library = Library::new(path) + .map_err(|e| format!("Failed to load plugin library: {}", e))?; + + let constructor: Symbol Box> = library + .get(b"_plugin_create") + .map_err(|e| format!("Plugin entry point not found: {}", e))?; + + let plugin = constructor(); + plugin.initialize().await?; + + let mut plugins = self.plugins.write().await; + let mut libraries = self.libraries.write().await; + + plugins.insert(name.to_string(), plugin); + libraries.insert(name.to_string(), library); + } + Ok(()) + } + + async fn unload_plugin(&self, name: &str) -> Result<(), String> { + let mut plugins = self.plugins.write().await; + let mut libraries = self.libraries.write().await; + + if let Some(plugin) = plugins.remove(name) { + plugin.shutdown().await?; + libraries.remove(name); + } + Ok(()) + } +} + +pub fn router() -> Router> { + Router::new() + .route("/plugins/list", get(list_plugins)) + .route("/plugins/install", post(install_plugin)) + .route("/plugins/configure", post(configure_plugin)) + .route("/plugins/status", get(plugin_status)) +} + +async fn install_plugin( + State(manager): State>, + Json(req): Json, +) -> Result { + let plugin_path = PathBuf::from("plugins").join(&req.name); + manager.load_plugin(&req.name, &plugin_path) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::OK) +} + +async fn configure_plugin( + State(manager): State>, + Json(req): Json, +) -> Result { + let mut configs = manager.configs.write().await; + configs.insert(req.name.clone(), serde_json::to_value(req.config).unwrap()); + Ok(StatusCode::OK) +} + +async fn list_plugins( + State(manager): State>, +) -> Json> { + let plugins = manager.plugins.read().await; + Json(plugins.keys().cloned().collect()) +} + +async fn plugin_status( + State(manager): State>, +) -> Json> { + let plugins = manager.plugins.read().await; + let configs = manager.configs.read().await; + + let mut status = HashMap::new(); + for (name, _) in plugins.iter() { + let config = configs.get(name).cloned().unwrap_or_default(); + status.insert( + name.clone(), + PluginInfo { + name: name.clone(), + version: "1.0.0".to_string(), + status: PluginStatus::Active, + config: serde_json::from_value(config).unwrap_or_default(), + }, + ); + } + Json(status) +} \ No newline at end of file diff --git a/sidecar/src/security/mod.rs b/sidecar/src/security/mod.rs new file mode 100644 index 000000000..3964d1b98 --- /dev/null +++ b/sidecar/src/security/mod.rs @@ -0,0 +1,194 @@ +use axum::{ + routing::{get, post}, + Router, + Json, + http::StatusCode, + extract::State, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc, path::PathBuf}; +use tokio::sync::RwLock; +use chrono::{DateTime, Utc}; +use walkdir::WalkDir; +use regex::Regex; +use std::fs; + +#[derive(Debug, Serialize)] +pub struct SecurityScan { + id: String, + timestamp: DateTime, + findings: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SecurityFinding { + severity: Severity, + description: String, + location: String, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Severity { + High, + Medium, + Low, + Info, +} + +#[derive(Debug, Serialize, Clone)] +pub struct AuditLog { + timestamp: DateTime, + action: String, + user: String, + resource: String, + status: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SecurityPolicy { + name: String, + enabled: bool, + rules: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SecurityRule { + name: String, + description: String, + enabled: bool, + action: String, +} + +pub struct SecurityManager { + policies: RwLock>, + audit_logs: RwLock>, + vulnerability_patterns: Vec<(Severity, Regex)>, +} + +impl SecurityManager { + pub fn new() -> Arc { + let vulnerability_patterns = vec![ + (Severity::High, Regex::new(r"(?i)password\s*=\s*['\"]\w+['\"]").unwrap()), + (Severity::High, Regex::new(r"(?i)api_key\s*=\s*['\"]\w+['\"]").unwrap()), + (Severity::Medium, Regex::new(r"(?i)TODO:|FIXME:").unwrap()), + (Severity::Low, Regex::new(r"(?i)console\.log").unwrap()), + ]; + + Arc::new(Self { + policies: RwLock::new(HashMap::new()), + audit_logs: RwLock::new(Vec::new()), + vulnerability_patterns, + }) + } + + async fn scan_directory(&self, path: &PathBuf) -> Vec { + let mut findings = Vec::new(); + + for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) { + if !entry.file_type().is_file() { + continue; + } + + if let Ok(content) = fs::read_to_string(entry.path()) { + for (severity, pattern) in &self.vulnerability_patterns { + for mat in pattern.find_iter(&content) { + findings.push(SecurityFinding { + severity: severity.clone(), + description: format!("Found pattern: {}", mat.as_str()), + location: entry.path().to_string_lossy().into_owned(), + }); + } + } + } + } + + findings + } + + async fn log_audit_event(&self, event: AuditLog) { + let mut logs = self.audit_logs.write().await; + logs.push(event); + } + + async fn enforce_policy(&self, policy_name: &str, resource: &str) -> bool { + let policies = self.policies.read().await; + if let Some(policy) = policies.get(policy_name) { + for rule in &policy.rules { + if !rule.enabled { + continue; + } + // Add your policy enforcement logic here + match rule.action.as_str() { + "deny" => return false, + "allow" => return true, + _ => continue, + } + } + } + true + } +} + +pub fn router() -> Router> { + Router::new() + .route("/security/scan", post(security_scan)) + .route("/security/audit", get(audit_logs)) + .route("/security/policy", get(get_policies)) + .route("/security/policy", post(update_policy)) +} + +async fn security_scan( + State(manager): State>, +) -> Json { + let path = PathBuf::from("."); + let findings = manager.scan_directory(&path).await; + + let scan = SecurityScan { + id: uuid::Uuid::new_v4().to_string(), + timestamp: Utc::now(), + findings, + }; + + manager.log_audit_event(AuditLog { + timestamp: Utc::now(), + action: "security_scan".to_string(), + user: "system".to_string(), + resource: "codebase".to_string(), + status: "completed".to_string(), + }).await; + + Json(scan) +} + +async fn audit_logs( + State(manager): State>, +) -> Json> { + let logs = manager.audit_logs.read().await; + Json(logs.clone()) +} + +async fn get_policies( + State(manager): State>, +) -> Json> { + let policies = manager.policies.read().await; + Json(policies.values().cloned().collect()) +} + +async fn update_policy( + State(manager): State>, + Json(policy): Json, +) -> Result { + let mut policies = manager.policies.write().await; + policies.insert(policy.name.clone(), policy); + + manager.log_audit_event(AuditLog { + timestamp: Utc::now(), + action: "update_policy".to_string(), + user: "system".to_string(), + resource: "security_policy".to_string(), + status: "success".to_string(), + }).await; + + Ok(StatusCode::OK) +} \ No newline at end of file diff --git a/sidecar/src/webserver/mod.rs b/sidecar/src/webserver/mod.rs index f77f79f1c..c2d15dd17 100644 --- a/sidecar/src/webserver/mod.rs +++ b/sidecar/src/webserver/mod.rs @@ -9,6 +9,45 @@ pub mod in_line_agent; pub mod in_line_agent_stream; pub mod inline_completion; pub mod model_selection; -pub(crate) mod plan; +pub mod plan; pub mod tree_sitter; pub mod types; + +use axum::Router; +use std::sync::Arc; +use crate::{ + auth, fs, lsp, cache, metrics, plugins, security, + webserver::{ + health, config, tree_sitter, agent, agentic, + }, + application::Application, +}; + +pub async fn create_router(app: Arc) -> anyhow::Result { + // Initialize states for independent services + let cache_state = cache::CacheState::new(cache::CacheConfig { + max_size: 1024 * 1024 * 100, // 100MB + ttl_seconds: 3600, + compression_enabled: true, + }); + + let metrics_state = metrics::MetricsState::new(); + let plugin_manager = plugins::PluginManager::new(); + let security_manager = security::SecurityManager::new(); + + Ok(Router::new() + // Existing routes + .merge(health::router()) + .merge(config::router()) + .merge(tree_sitter::router()) + .merge(agent::router()) + .merge(agentic::router()) + // Independent service routes with state + .merge(auth::router()) + .merge(fs::router()) + .merge(lsp::router().with_state(app.lsp_state.clone())) + .merge(cache::router().with_state(cache_state)) + .merge(metrics::router().with_state(metrics_state)) + .merge(plugins::router().with_state(plugin_manager)) + .merge(security::router().with_state(security_manager))) +} \ No newline at end of file