devops-copilot 0.1.0__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.
@@ -0,0 +1,30 @@
1
+ from sqlmodel import SQLModel, Field
2
+ from typing import Optional
3
+
4
+ class Server(SQLModel, table=True):
5
+ """
6
+ SQLModel representation of the target managed servers.
7
+ Stores SSH access configurations with encrypted credentials.
8
+ """
9
+ id: Optional[int] = Field(default=None, primary_key=True)
10
+
11
+ # User-friendly identifier (e.g. "staging-vps")
12
+ name: str = Field(index=True, unique=True, nullable=False)
13
+
14
+ # Target machine host/IP address
15
+ ip_address: str = Field(nullable=False)
16
+
17
+ # Port target machine is listening to SSH (usually 22)
18
+ ssh_port: int = Field(default=22)
19
+
20
+ # Username for SSH login (e.g. "root" or "deploy")
21
+ ssh_username: str = Field(default="root")
22
+
23
+ # Either "password" or "key" (SSH private key authorization)
24
+ ssh_auth_type: str = Field(default="password")
25
+
26
+ # AES-encrypted password string or private key string
27
+ encrypted_credential: str = Field(nullable=False)
28
+
29
+ # Owner relation to user table
30
+ owner_id: int = Field(foreign_key="user.id", nullable=False)
@@ -0,0 +1,67 @@
1
+ from fastapi import APIRouter, Depends, status
2
+ from typing import List
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from app.core.database import get_session
6
+ from app.modules.auth import User
7
+ from app.modules.auth.service import get_current_user
8
+ from app.modules.servers.schema import ServerCreate, ServerUpdate, ServerResponse, CommandRequest, CommandResponse
9
+ from app.modules.servers.service import servers_service
10
+
11
+ router = APIRouter()
12
+
13
+ @router.post("/", response_model=ServerResponse, status_code=status.HTTP_201_CREATED)
14
+ async def create_server(
15
+ server_in: ServerCreate,
16
+ current_user: User = Depends(get_current_user),
17
+ session: AsyncSession = Depends(get_session)
18
+ ):
19
+ """Register a new server connection under the logged-in administrator."""
20
+ return await servers_service.create_server(session, server_in, current_user.id)
21
+
22
+ @router.get("/", response_model=List[ServerResponse])
23
+ async def list_servers(
24
+ current_user: User = Depends(get_current_user),
25
+ session: AsyncSession = Depends(get_session)
26
+ ):
27
+ """Retrieve all managed servers registered to the current administrator."""
28
+ return await servers_service.get_all_servers(session, current_user.id)
29
+
30
+ @router.get("/{server_id}", response_model=ServerResponse)
31
+ async def get_server(
32
+ server_id: int,
33
+ current_user: User = Depends(get_current_user),
34
+ session: AsyncSession = Depends(get_session)
35
+ ):
36
+ """Retrieve specific server credentials configuration metadata."""
37
+ return await servers_service.get_server_by_id(session, server_id, current_user.id)
38
+
39
+ @router.put("/{server_id}", response_model=ServerResponse)
40
+ async def update_server(
41
+ server_id: int,
42
+ server_in: ServerUpdate,
43
+ current_user: User = Depends(get_current_user),
44
+ session: AsyncSession = Depends(get_session)
45
+ ):
46
+ """Update registry configuration metadata for a managed server."""
47
+ return await servers_service.update_server(session, server_id, server_in, current_user.id)
48
+
49
+ @router.delete("/{server_id}")
50
+ async def delete_server(
51
+ server_id: int,
52
+ current_user: User = Depends(get_current_user),
53
+ session: AsyncSession = Depends(get_session)
54
+ ):
55
+ """Delete a managed server registry."""
56
+ return await servers_service.delete_server(session, server_id, current_user.id)
57
+
58
+ @router.post("/{server_id}/execute", response_model=CommandResponse)
59
+ async def execute_command(
60
+ server_id: int,
61
+ payload: CommandRequest,
62
+ current_user: User = Depends(get_current_user),
63
+ session: AsyncSession = Depends(get_session)
64
+ ):
65
+ """Execute arbitrary terminal commands asynchronously on target server via secure SSH connection."""
66
+ server = await servers_service.get_server_by_id(session, server_id, current_user.id)
67
+ return await servers_service.execute_ssh_command(server, payload.command)
@@ -0,0 +1,45 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Literal
3
+
4
+ class ServerCreate(BaseModel):
5
+ """DTO for creating a new managed server connection."""
6
+ name: str = Field(..., min_length=3, max_length=50)
7
+ ip_address: str = Field(...)
8
+ ssh_port: int = Field(default=22, ge=1, le=65535)
9
+ ssh_username: str = Field(default="root")
10
+ ssh_auth_type: Literal["password", "key"] = Field(default="password")
11
+
12
+ # Raw password or private key string. Will be encrypted before database storage.
13
+ credential: str = Field(..., min_length=1)
14
+
15
+ class ServerUpdate(BaseModel):
16
+ """DTO for updating an existing managed server connection."""
17
+ name: str | None = Field(None, min_length=3, max_length=50)
18
+ ip_address: str | None = None
19
+ ssh_port: int | None = Field(None, ge=1, le=65535)
20
+ ssh_username: str | None = None
21
+ ssh_auth_type: Literal["password", "key"] | None = None
22
+ credential: str | None = None
23
+
24
+ class ServerResponse(BaseModel):
25
+ """Safe database projection returning public server parameters (excludes credentials)."""
26
+ id: int
27
+ name: str
28
+ ip_address: str
29
+ ssh_port: int
30
+ ssh_username: str
31
+ ssh_auth_type: str
32
+ owner_id: int
33
+
34
+ class Config:
35
+ from_attributes = True
36
+
37
+ class CommandRequest(BaseModel):
38
+ """Input payload to execute custom commands on target server."""
39
+ command: str = Field(..., min_length=1)
40
+
41
+ class CommandResponse(BaseModel):
42
+ """Response returned after running SSH commands."""
43
+ stdout: str
44
+ stderr: str
45
+ exit_code: int
@@ -0,0 +1,164 @@
1
+ import asyncio
2
+ import asyncssh
3
+ from typing import List, Optional
4
+ from fastapi import HTTPException, status
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlmodel import select
7
+
8
+ from app.core.security import encrypt_data, decrypt_data
9
+ from app.modules.servers.models import Server
10
+ from app.modules.servers.schema import ServerCreate, ServerUpdate
11
+
12
+ class ServersService:
13
+ async def create_server(self, session: AsyncSession, server_in: ServerCreate, owner_id: int) -> Server:
14
+ """Create a server registry after encrypting credentials."""
15
+ # Ensure server name is unique
16
+ statement = select(Server).where(Server.name == server_in.name)
17
+ result = await session.exec(statement)
18
+ if result.first():
19
+ raise HTTPException(
20
+ status_code=status.HTTP_400_BAD_REQUEST,
21
+ detail="Server name already registered"
22
+ )
23
+
24
+ # Encrypt password or private key before saving to DB
25
+ encrypted_cred = encrypt_data(server_in.credential)
26
+
27
+ # Instantiate Server DB record
28
+ new_server = Server(
29
+ name=server_in.name,
30
+ ip_address=server_in.ip_address,
31
+ ssh_port=server_in.ssh_port,
32
+ ssh_username=server_in.ssh_username,
33
+ ssh_auth_type=server_in.ssh_auth_type,
34
+ encrypted_credential=encrypted_cred,
35
+ owner_id=owner_id
36
+ )
37
+
38
+ session.add(new_server)
39
+ await session.commit()
40
+ await session.refresh(new_server)
41
+ return new_server
42
+
43
+ async def get_all_servers(self, session: AsyncSession, owner_id: int) -> List[Server]:
44
+ """Fetch all servers owned by the user."""
45
+ statement = select(Server).where(Server.owner_id == owner_id)
46
+ result = await session.exec(statement)
47
+ return result.all()
48
+
49
+ async def get_server_by_id(self, session: AsyncSession, server_id: int, owner_id: int) -> Optional[Server]:
50
+ """Fetch a specific server by ID and verify ownership."""
51
+ statement = select(Server).where(Server.id == server_id, Server.owner_id == owner_id)
52
+ result = await session.exec(statement)
53
+ server = result.first()
54
+ if not server:
55
+ raise HTTPException(
56
+ status_code=status.HTTP_404_NOT_FOUND,
57
+ detail="Server not found"
58
+ )
59
+ return server
60
+
61
+ async def get_server_by_name(self, session: AsyncSession, name: str, owner_id: int) -> Optional[Server]:
62
+ """Fetch a specific server by name and verify ownership."""
63
+ statement = select(Server).where(Server.name == name, Server.owner_id == owner_id)
64
+ result = await session.exec(statement)
65
+ server = result.first()
66
+ if not server:
67
+ raise HTTPException(
68
+ status_code=status.HTTP_404_NOT_FOUND,
69
+ detail="Server not found"
70
+ )
71
+ return server
72
+
73
+ async def update_server(self, session: AsyncSession, server_id: int, server_in: ServerUpdate, owner_id: int) -> Server:
74
+ """Update an existing server registry configuration."""
75
+ server = await self.get_server_by_id(session, server_id, owner_id)
76
+
77
+ if server_in.name is not None and server_in.name != server.name:
78
+ # Ensure name is unique if changed
79
+ statement = select(Server).where(Server.name == server_in.name)
80
+ result = await session.exec(statement)
81
+ if result.first():
82
+ raise HTTPException(
83
+ status_code=status.HTTP_400_BAD_REQUEST,
84
+ detail="Server name already registered"
85
+ )
86
+ server.name = server_in.name
87
+
88
+ if server_in.ip_address is not None:
89
+ server.ip_address = server_in.ip_address
90
+ if server_in.ssh_port is not None:
91
+ server.ssh_port = server_in.ssh_port
92
+ if server_in.ssh_username is not None:
93
+ server.ssh_username = server_in.ssh_username
94
+ if server_in.ssh_auth_type is not None:
95
+ server.ssh_auth_type = server_in.ssh_auth_type
96
+ if server_in.credential is not None:
97
+ server.encrypted_credential = encrypt_data(server_in.credential)
98
+
99
+ session.add(server)
100
+ await session.commit()
101
+ await session.refresh(server)
102
+ return server
103
+
104
+ async def delete_server(self, session: AsyncSession, server_id: int, owner_id: int) -> dict:
105
+ """Delete an existing server registry."""
106
+ server = await self.get_server_by_id(session, server_id, owner_id)
107
+ await session.delete(server)
108
+ await session.commit()
109
+ return {"status": "success", "message": f"Server {server_id} deleted successfully"}
110
+
111
+ async def execute_ssh_command(self, server: Server, command: str) -> dict:
112
+ """
113
+ Decrypt server connection credentials, establish SSH tunnel,
114
+ execute bash command, and return outputs.
115
+ """
116
+ # Decrypt password or SSH Private Key
117
+ credential = decrypt_data(server.encrypted_credential)
118
+
119
+ # Build connection arguments
120
+ conn_args = {
121
+ "host": server.ip_address,
122
+ "port": server.ssh_port,
123
+ "username": server.ssh_username,
124
+ "known_hosts": None, # Bypass host key verification for dynamic servers (warning: MITM vulnerable)
125
+ }
126
+
127
+ if server.ssh_auth_type == "password":
128
+ conn_args["password"] = credential
129
+ else:
130
+ try:
131
+ # Import private key object from decrypted PEM string
132
+ key = asyncssh.import_private_key(credential)
133
+ conn_args["client_keys"] = [key]
134
+ except Exception as e:
135
+ raise HTTPException(
136
+ status_code=status.HTTP_400_BAD_REQUEST,
137
+ detail=f"Invalid SSH private key format: {str(e)}"
138
+ )
139
+
140
+ try:
141
+ # Connect and execute command using asyncssh with 30s timeout
142
+ async def run_command_with_ssh():
143
+ async with asyncssh.connect(**conn_args) as conn:
144
+ result = await conn.run(command, check=False)
145
+ return {
146
+ "stdout": result.stdout,
147
+ "stderr": result.stderr,
148
+ "exit_code": result.exit_status
149
+ }
150
+
151
+ return await asyncio.wait_for(run_command_with_ssh(), timeout=30.0)
152
+ except asyncio.TimeoutError:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_504_GATEWAY_TIMEOUT,
155
+ detail=f"SSH execution timed out after 30 seconds on {server.ip_address}"
156
+ )
157
+ except (asyncssh.Error, OSError) as e:
158
+ raise HTTPException(
159
+ status_code=status.HTTP_502_BAD_GATEWAY,
160
+ detail=f"SSH Connection Failed to {server.ip_address}:{server.ssh_port} - {str(e)}"
161
+ )
162
+
163
+ # Single instance representing the service provider
164
+ servers_service = ServersService()
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: devops-copilot
3
+ Version: 0.1.0
4
+ Summary: An AI-driven DevOps Copilot and CLI Client for managing bare-metal servers securely
5
+ Author: irzix
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.12.0
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: websockets>=12.0
11
+ Provides-Extra: server
12
+ Requires-Dist: fastapi>=0.111.0; extra == "server"
13
+ Requires-Dist: uvicorn>=0.30.0; extra == "server"
14
+ Requires-Dist: sqlmodel>=0.0.22; extra == "server"
15
+ Requires-Dist: pyjwt>=2.8.0; extra == "server"
16
+ Requires-Dist: aiosqlite>=0.20.0; extra == "server"
17
+ Requires-Dist: cryptography>=42.0.0; extra == "server"
18
+ Requires-Dist: langchain>=0.2.0; extra == "server"
19
+ Requires-Dist: chromadb>=0.5.0; extra == "server"
20
+ Requires-Dist: pydantic-settings>=2.2.0; extra == "server"
21
+ Requires-Dist: bcrypt>=4.1.0; extra == "server"
22
+ Requires-Dist: python-multipart>=0.0.9; extra == "server"
23
+ Requires-Dist: greenlet>=3.0.0; extra == "server"
24
+ Requires-Dist: langchain-openai>=0.1.0; extra == "server"
25
+ Requires-Dist: asyncssh>=2.14.0; extra == "server"
26
+
27
+ # DevOps-Copilot 🚀
28
+
29
+ An open-source, AI-driven DevOps Copilot and CLI Client designed to manage raw root/bare-metal servers securely. It features persistent credential encryption, real-time terminal streaming, and semantic security guardrails using a local vector database.
30
+
31
+ ---
32
+
33
+ ## Key Features
34
+ - **Lean RAG Knowledge Base:** Automatically chunks and indexes executed SSH command outputs, logs, and server configs into separate **ChromaDB** collections, enabling the agent to search past server history before executing new commands.
35
+ - **Semantic Guardrails:** Uses local vector search to intercept and block dangerous terminal commands.
36
+ - **Human-in-the-Loop (HITL):** Enforces admin approval (`[y/N]`) inside the terminal for any state-modifying actions with clean prompt synchronization.
37
+ - **CLI Connection Resilience:** Automatically reconnects to the WebSocket server using exponential backoff if the network drops or the server restarts.
38
+ - **Real-Time Streaming:** Streams LLM thoughts and active SSH `stdout`/`stderr` line-by-line using WebSockets with 30s execution timeouts.
39
+ - **Encrypted Credentials:** Securely encrypts passwords and SSH private keys using Fernet (AES-256).
40
+ - **Server & Session CRUD:** Full REST API support for updating/deleting server connections and deleting chat sessions (with cascade cleanup).
41
+ - **Flexible AI Models:** Powered by **OpenRouter** (supports Llama 3, Gemini, GPT, etc.).
42
+
43
+ ---
44
+
45
+ ## 📦 Quick Start (Backend Server)
46
+
47
+ ### 1. Configure Settings
48
+ Copy the env file and populate keys:
49
+ ```bash
50
+ cp .env.example .env
51
+ ```
52
+ Make sure to add your `OPENROUTER_API_KEY` and a custom base64 `ENCRYPTION_KEY` in `.env`.
53
+
54
+ ### 2. Run with Docker Compose
55
+ ```bash
56
+ docker compose up -d --build
57
+ ```
58
+ The server will boot on port `8000`. Database tables and security blacklist vectors are automatically seeded on startup.
59
+
60
+ ---
61
+
62
+ ## 💻 Quick Start (CLI Client)
63
+
64
+ ### 1. Install Globally
65
+ Install the package in editable mode from your local repository root:
66
+ ```bash
67
+ uv pip install -e .
68
+ ```
69
+
70
+ ### 2. Authenticate
71
+ Configure the server URL and log in to get your JWT access token:
72
+ ```bash
73
+ devops-copilot login
74
+ ```
75
+
76
+ ### 3. Interactive Chat
77
+ Start the real-time DevOps chat session:
78
+ ```bash
79
+ devops-copilot chat
80
+ ```
81
+ *Ask the agent to check stats or run actions. Approve state-modifying commands directly in the prompt.*
@@ -0,0 +1,33 @@
1
+ app/__init__.py,sha256=prAfDw-CuIvjA35TsMKTzgRHKOsvlHYhpthqzg7gtQc,45
2
+ app/main.py,sha256=oHGIJKeGghwCJU5xMkU0OetTcFrcC7CM3xjuLl_ns9g,1078
3
+ app/cli/__init__.py,sha256=2dEVPQKy6c3qcm1D2j981g3B9KxXamfwQpTfF_X309Q,49
4
+ app/cli/config.py,sha256=SIoNL7v-TicY7dFgekSgjgNNufOv8upAXbGoOKPgcOo,1483
5
+ app/cli/main.py,sha256=ybZ4-b5_16tzQZ0ljNsY8He4hNKSlzWh51Q10_fsmYA,11196
6
+ app/core/config.py,sha256=XEx9PG49BLVAC7NPhD58aTOb9pHnmiYeJ0Nuv0Cc1ag,708
7
+ app/core/security.py,sha256=l3jLCc7UujYQwInghM1d_VS8R8jjlPsyhtAvAdebytc,595
8
+ app/core/database/__init__.py,sha256=SUnd9pdtT2JFDb2AiQ13LW-z00A_B1aNWolmFAvyZs0,790
9
+ app/modules/__init__.py,sha256=NmdHNlZ_Beg05caUEHllKj1TIIlXdoMMVLmZ74hr4KQ,45
10
+ app/modules/auth/__init__.py,sha256=J4mhJsBHQRhZpEvORz2opYAACOQcyhnUNzVlu5kYhOA,336
11
+ app/modules/auth/router.py,sha256=mBbmYjuhGkpzOgVy9vt39rtuMwRLKLC8X3U2AWFihm4,3341
12
+ app/modules/auth/schema.py,sha256=iPE2hzj5yNM4v_4rC0i8oUC3W09wOs5o_cL3_FDHYbE,980
13
+ app/modules/auth/service.py,sha256=hPE_CRKzBXchAhWKO7kvEkrCYKk3_eMpvtDIANFs7dQ,2797
14
+ app/modules/chat/__init__.py,sha256=e5afKCCktlS6c0PHDejf7XTt-A5ifO3ziU34h4JoO68,46
15
+ app/modules/chat/agent.py,sha256=Xx9lX77dZmvYf2nkQWvLWl8tUknemCY04Zr4AIA5ons,18406
16
+ app/modules/chat/models.py,sha256=6g9d82vnNtYVnc1JiLgqPUGNlrEkCKTkoC-0biR4VkM,1827
17
+ app/modules/chat/router.py,sha256=ioky2_ffORQoOX7y5OaO6Poht--S-kEkQZcnWvmoHx8,16241
18
+ app/modules/chat/schema.py,sha256=vl6sSCp4F3GxLMIcHV2vtrKT9Oldw6qJ1S3_AEvx-gg,962
19
+ app/modules/guardrails/__init__.py,sha256=E_i6gXiXTvsyZSwvplJsAii06Tw8MHQLTPrPfcx4CL0,45
20
+ app/modules/guardrails/schema.py,sha256=q4xQJ9v0XXceclvcQujneawBnh14tFJuRBZPFeVZ09Q,775
21
+ app/modules/guardrails/service.py,sha256=s2gwHlA5weOdyVEZoCB2oxOQIv-Pvm0Np5TvCCE9b7k,4692
22
+ app/modules/knowledge/__init__.py,sha256=rydpZI76T8AnuAULTQOsMfcYu0BKNZ0D3RciBEmvU-c,51
23
+ app/modules/knowledge/service.py,sha256=Ut73BOVxVz5nE1iZ2iAUuI8slNLpDtnwOsIa0VBhxmQ,3794
24
+ app/modules/servers/__init__.py,sha256=z20A6YQjS8e3q7nD7eQTk3ziImlH8Rv8eJoXup0ZV-0,27
25
+ app/modules/servers/models.py,sha256=fxdq6y4lS_232pxkZ6QjH7A6PsgnXdav8pfOhk3PQc0,1061
26
+ app/modules/servers/router.py,sha256=OOqTM4d1c2xuyjgd65OeY4eaEOWmvMzswGu8uXAe3aQ,2787
27
+ app/modules/servers/schema.py,sha256=YC2iut3_QgyyCAsYgwPGUfIBA7FM-RpkEr_HcSagStE,1532
28
+ app/modules/servers/service.py,sha256=vmTiJQSSmOUrfdIi20YllKNCHsj9r2GlzFhDmFFjIYY,6992
29
+ devops_copilot-0.1.0.dist-info/METADATA,sha256=qsbmYxYIUcQXLA5OBhTqIUn_tYuubcxyT7wtMnB1QXE,3410
30
+ devops_copilot-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
31
+ devops_copilot-0.1.0.dist-info/entry_points.txt,sha256=pJGQfofbOaBpIy2QK7-u4fGLVT3rS6bteGs4SeSaT-U,52
32
+ devops_copilot-0.1.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
33
+ devops_copilot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devops-copilot = app.cli.main:app
@@ -0,0 +1 @@
1
+ app